Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dotnet/aspnetcore.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitmodules24
-rw-r--r--build/buildorder.props14
-rw-r--r--build/submodules.props12
m---------modules/Antiforgery22
m---------modules/CORS6
m---------modules/HttpSysServer6
m---------modules/ResponseCaching22
m---------modules/Routing18
m---------modules/Security22
-rw-r--r--src/AADIntegration/Directory.Build.props2
-rw-r--r--src/Antiforgery/.gitignore40
-rw-r--r--src/Antiforgery/Antiforgery.sln35
-rw-r--r--src/Antiforgery/Directory.Build.props24
-rw-r--r--src/Antiforgery/Directory.Build.targets7
-rw-r--r--src/Antiforgery/NuGetPackageVerifier.json7
-rw-r--r--src/Antiforgery/README.md14
-rw-r--r--src/Antiforgery/build/Key.snkbin0 -> 596 bytes
-rw-r--r--src/Antiforgery/build/dependencies.props35
-rw-r--r--src/Antiforgery/build/repo.props14
-rw-r--r--src/Antiforgery/build/sources.props17
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryOptions.cs150
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryServiceCollectionExtensions.cs76
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryTokenSet.cs57
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/AntiforgeryValidationException.cs34
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/IAntiforgery.cs65
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/IAntiforgeryAdditionalDataProvider.cs39
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryFeature.cs34
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryLoggerExtensions.cs109
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryOptionsSetup.cs38
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgerySerializationContext.cs141
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgerySerializationContextPooledObjectPolicy.cs23
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/AntiforgeryToken.cs53
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/BinaryBlob.cs117
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/CryptographyAlgorithms.cs25
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgery.cs497
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryAdditionalDataProvider.cs26
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenGenerator.cs238
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenSerializer.cs188
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenStore.cs90
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultClaimUidExtractor.cs149
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryFeature.cs25
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenGenerator.cs50
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenSerializer.cs12
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IAntiforgeryTokenStore.cs22
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Internal/IClaimUidExtractor.cs21
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Microsoft.AspNetCore.Antiforgery.csproj19
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Properties/AssemblyInfo.cs7
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Properties/Resources.Designer.cs254
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/Resources.resx169
-rw-r--r--src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery/baseline.netcore.json456
-rw-r--r--src/Antiforgery/test/Directory.Build.props10
-rw-r--r--src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/AntiforgeryOptionsSetupTest.cs73
-rw-r--r--src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/AntiforgeryTokenTest.cs132
-rw-r--r--src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/BinaryBlobTest.cs129
-rw-r--r--src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTest.cs1497
-rw-r--r--src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenGeneratorTest.cs628
-rw-r--r--src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenSerializerTest.cs189
-rw-r--r--src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultAntiforgeryTokenStoreTest.cs457
-rw-r--r--src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Internal/DefaultClaimUidExtractorTest.cs261
-rw-r--r--src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/Microsoft.AspNetCore.Antiforgery.Test.csproj24
-rw-r--r--src/Antiforgery/test/Microsoft.AspNetCore.Antiforgery.Test/TestOptionsManager.cs21
-rw-r--r--src/Antiforgery/version.props12
-rw-r--r--src/AuthSamples/Directory.Build.props2
-rw-r--r--src/AzureIntegration/Directory.Build.props2
-rw-r--r--src/CORS/.gitignore41
-rw-r--r--src/CORS/CORS.sln89
-rw-r--r--src/CORS/Directory.Build.props21
-rw-r--r--src/CORS/Directory.Build.targets7
-rw-r--r--src/CORS/NuGetPackageVerifier.json7
-rw-r--r--src/CORS/README.md9
-rw-r--r--src/CORS/build/Key.snkbin0 -> 596 bytes
-rw-r--r--src/CORS/build/buildpipeline/linux.groovy10
-rw-r--r--src/CORS/build/buildpipeline/osx.groovy10
-rw-r--r--src/CORS/build/buildpipeline/pipeline.groovy18
-rw-r--r--src/CORS/build/buildpipeline/windows.groovy12
-rw-r--r--src/CORS/build/dependencies.props35
-rw-r--r--src/CORS/build/repo.props15
-rw-r--r--src/CORS/build/sources.props17
-rw-r--r--src/CORS/samples/README.md38
-rw-r--r--src/CORS/samples/SampleDestination/Program.cs25
-rw-r--r--src/CORS/samples/SampleDestination/SampleDestination.csproj16
-rw-r--r--src/CORS/samples/SampleDestination/Startup.cs39
-rw-r--r--src/CORS/samples/SampleOrigin/Program.cs25
-rw-r--r--src/CORS/samples/SampleOrigin/SampleOrigin.csproj12
-rw-r--r--src/CORS/samples/SampleOrigin/Startup.cs29
-rw-r--r--src/CORS/samples/SampleOrigin/wwwroot/Index.html89
-rw-r--r--src/CORS/src/Directory.Build.props8
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/CorsServiceCollectionExtensions.cs59
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/DisableCorsAttribute.cs14
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/EnableCorsAttribute.cs34
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsConstants.cs95
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddleware.cs131
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddlewareExtensions.cs70
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsOptions.cs119
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicy.cs164
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyBuilder.cs225
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyExtensions.cs36
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsResult.cs94
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs313
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/DefaultCorsPolicyProvider.cs36
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsPolicyProvider.cs22
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsService.cs31
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IDisableCorsAttribute.cs12
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IEnableCorsAttribute.cs16
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/UriHelpers.cs19
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs103
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Microsoft.AspNetCore.Cors.csproj22
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Properties/AssemblyInfo.cs6
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Resources.Designer.cs71
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/Resources.resx123
-rw-r--r--src/CORS/src/Microsoft.AspNetCore.Cors/baseline.netcore.json1250
-rw-r--r--src/CORS/test/Directory.Build.props15
-rw-r--r--src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareFunctionalTest.cs98
-rw-r--r--src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareTests.cs353
-rw-r--r--src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsOptionsTest.cs67
-rw-r--r--src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyBuilderTests.cs292
-rw-r--r--src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyExtensionsTests.cs85
-rw-r--r--src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyTests.cs74
-rw-r--r--src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsResultTests.cs69
-rw-r--r--src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs1167
-rw-r--r--src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsTestFixtureOfT.cs33
-rw-r--r--src/CORS/test/Microsoft.AspNetCore.Cors.Test/DefaultCorsPolicyProviderTests.cs56
-rw-r--r--src/CORS/test/Microsoft.AspNetCore.Cors.Test/Microsoft.AspNetCore.Cors.Test.csproj22
-rw-r--r--src/CORS/test/Microsoft.AspNetCore.Cors.Test/TestCorsOptions.cs12
-rw-r--r--src/CORS/test/Microsoft.AspNetCore.Cors.Test/UriHelpersTests.cs66
-rw-r--r--src/CORS/test/WebSites/CorsMiddlewareWebSite/CorsMiddlewareWebSite.csproj16
-rw-r--r--src/CORS/test/WebSites/CorsMiddlewareWebSite/EchoMiddleware.cs38
-rw-r--r--src/CORS/test/WebSites/CorsMiddlewareWebSite/Startup.cs33
-rw-r--r--src/CORS/test/WebSites/CorsMiddlewareWebSite/readme.md4
-rw-r--r--src/CORS/test/WebSites/CorsMiddlewareWebSite/web.config9
-rw-r--r--src/CORS/version.props12
-rw-r--r--src/HttpSysServer/.gitignore32
-rw-r--r--src/HttpSysServer/Directory.Build.props20
-rw-r--r--src/HttpSysServer/Directory.Build.targets7
-rw-r--r--src/HttpSysServer/HttpSysServer.sln176
-rw-r--r--src/HttpSysServer/NuGetPackageVerifier.json13
-rw-r--r--src/HttpSysServer/README.md10
-rw-r--r--src/HttpSysServer/build/Key.snkbin0 -> 596 bytes
-rw-r--r--src/HttpSysServer/build/dependencies.props31
-rw-r--r--src/HttpSysServer/build/repo.props15
-rw-r--r--src/HttpSysServer/build/sources.props17
-rw-r--r--src/HttpSysServer/samples/HotAddSample/HotAddSample.csproj16
-rw-r--r--src/HttpSysServer/samples/HotAddSample/Properties/launchSettings.json13
-rw-r--r--src/HttpSysServer/samples/HotAddSample/Startup.cs110
-rw-r--r--src/HttpSysServer/samples/SelfHostServer/App.config6
-rw-r--r--src/HttpSysServer/samples/SelfHostServer/Properties/launchSettings.json12
-rw-r--r--src/HttpSysServer/samples/SelfHostServer/Public/1kb.txt1
-rw-r--r--src/HttpSysServer/samples/SelfHostServer/SelfHostServer.csproj16
-rw-r--r--src/HttpSysServer/samples/SelfHostServer/Startup.cs48
-rw-r--r--src/HttpSysServer/samples/TestClient/App.config6
-rw-r--r--src/HttpSysServer/samples/TestClient/Program.cs78
-rw-r--r--src/HttpSysServer/samples/TestClient/Properties/AssemblyInfo.cs53
-rw-r--r--src/HttpSysServer/samples/TestClient/TestClient.csproj63
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/Constants.cs21
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/CookedUrl.cs55
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HeapAllocHandle.cs25
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HttpApiTypes.cs694
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HttpSysRequestHeader.cs51
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HttpSysResponseHeader.cs40
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/NclUtilities.cs19
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SafeLocalFreeChannelBinding.cs47
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SafeLocalMemHandle.cs27
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SafeNativeOverlapped.cs62
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SocketAddress.cs371
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/UnsafeNativeMethods.cs155
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HeaderCollection.cs243
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HeaderEncoding.cs34
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HeaderParser.cs58
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HttpKnownHeaderNames.cs72
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/NativeRequestContext.cs455
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RawUrlHelper.cs151
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RequestHeaders.Generated.cs2542
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RequestHeaders.cs265
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RequestUriBuilder.cs345
-rw-r--r--src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/SslStatus.cs12
-rw-r--r--src/HttpSysServer/src/Directory.Build.props7
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AsyncAcceptContext.cs260
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AuthenticationHandler.cs53
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AuthenticationManager.cs137
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AuthenticationSchemes.cs19
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/FeatureContext.cs594
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Helpers.cs126
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Http503VerbosityLevel .cs26
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysDefaults.cs13
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysException.cs39
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysListener.cs435
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs199
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/LogHelper.cs106
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/MessagePump.cs329
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Microsoft.AspNetCore.Server.HttpSys.csproj24
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/ComNetOS.cs22
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/DisconnectListener.cs157
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpApi.cs133
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpRequestQueueV2Handle.cs23
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpServerSessionHandle.cs46
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpSysSettings.cs113
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/IntPtrHelper.cs20
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/RequestQueue.cs137
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/ServerSession.cs35
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/TokenBindingUtil.cs82
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/UrlGroup.cs134
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Properties/AssemblyInfo.cs6
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Properties/Resources.Designer.cs206
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/BoundaryType.cs15
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/ClientCertLoader.cs433
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/HttpReasonPhrase.cs101
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/OpaqueStream.cs165
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/Request.cs344
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestContext.cs222
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestHeaders.Generated.tt216
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStream.cs472
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStreamAsyncResult.cs194
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/Response.cs627
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/ResponseBody.cs705
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/ResponseStreamAsyncResult.cs341
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Resources.resx153
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/ResponseStream.cs115
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/StandardFeatureCollection.cs106
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/TimeoutManager.cs235
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/UrlPrefix.cs170
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/UrlPrefixCollection.cs162
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/ValidationHelper.cs64
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/WebHostBuilderHttpSysExtensions.cs50
-rw-r--r--src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/baseline.netcore.json881
-rw-r--r--src/HttpSysServer/test/Directory.Build.props18
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/AuthenticationTests.cs383
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/DummyApplication.cs38
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/HttpsTests.cs166
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/AuthenticationTests.cs220
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/HttpsTests.cs172
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/OpaqueUpgradeTests.cs217
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/RequestBodyTests.cs436
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/RequestHeaderTests.cs184
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/RequestTests.cs312
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseBodyTests.cs663
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseCachingTests.cs1168
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseHeaderTests.cs541
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseSendFileTests.cs593
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseTests.cs137
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ServerTests.cs345
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/Utilities.cs112
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/MessagePumpTests.cs122
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj15
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/OSDontSkipConditionAttribute.cs99
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/OpaqueUpgradeTests.cs367
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Properties/AssemblyInfo.cs9
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestBodyLimitTests.cs428
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestBodyTests.cs247
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestHeaderTests.cs98
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestTests.cs388
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseBodyTests.cs289
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseCachingTests.cs225
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseHeaderTests.cs322
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseSendFileTests.cs359
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseTests.cs223
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs695
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Utilities.cs146
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.Tests/Microsoft.AspNetCore.Server.HttpSys.Tests.csproj11
-rw-r--r--src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.Tests/UrlPrefixTests.cs85
-rw-r--r--src/HttpSysServer/version.props12
-rw-r--r--src/Installers/Windows/AspNetCoreModule-Setup/Directory.Build.props2
-rw-r--r--src/JavaScriptServices/Directory.Build.props2
-rw-r--r--src/MetaPackages/Directory.Build.props2
-rw-r--r--src/ResponseCaching/.gitignore33
-rw-r--r--src/ResponseCaching/Directory.Build.props21
-rw-r--r--src/ResponseCaching/Directory.Build.targets7
-rw-r--r--src/ResponseCaching/NuGetPackageVerifier.json7
-rw-r--r--src/ResponseCaching/README.md9
-rw-r--r--src/ResponseCaching/ResponseCaching.sln66
-rw-r--r--src/ResponseCaching/build/Key.snkbin0 -> 596 bytes
-rw-r--r--src/ResponseCaching/build/dependencies.props32
-rw-r--r--src/ResponseCaching/build/repo.props15
-rw-r--r--src/ResponseCaching/build/sources.props17
-rw-r--r--src/ResponseCaching/samples/ResponseCachingSample/README.md6
-rw-r--r--src/ResponseCaching/samples/ResponseCachingSample/ResponseCachingSample.csproj16
-rw-r--r--src/ResponseCaching/samples/ResponseCachingSample/Startup.cs49
-rw-r--r--src/ResponseCaching/src/Directory.Build.props7
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCachingFeature.cs16
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj14
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json34
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs88
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs20
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedVaryByRules.cs16
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs79
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs18
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs17
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheEntry.cs9
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingKeyProvider.cs31
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs43
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs310
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.cs22
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs93
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs134
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs219
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs248
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs28
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs24
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs24
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj23
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs6
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs22
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs34
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs528
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs32
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs58
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs200
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs230
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs206
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs41
-rw-r--r--src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json252
-rw-r--r--src/ResponseCaching/test/Directory.Build.props14
-rw-r--r--src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj19
-rw-r--r--src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs59
-rw-r--r--src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs218
-rw-r--r--src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs940
-rw-r--r--src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs794
-rw-r--r--src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs849
-rw-r--r--src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs285
-rw-r--r--src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs113
-rw-r--r--src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs396
-rw-r--r--src/ResponseCaching/version.props12
-rw-r--r--src/Routing/.gitignore41
-rw-r--r--src/Routing/Directory.Build.props20
-rw-r--r--src/Routing/Directory.Build.targets7
-rw-r--r--src/Routing/NuGetPackageVerifier.json13
-rw-r--r--src/Routing/README.md10
-rw-r--r--src/Routing/Routing.sln165
-rw-r--r--src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Configs/CoreConfig.cs31
-rw-r--r--src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Microsoft.AspNetCore.Routing.Performance.csproj22
-rw-r--r--src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Program.cs17
-rw-r--r--src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs114
-rw-r--r--src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/readme.md11
-rw-r--r--src/Routing/build/Key.snkbin0 -> 596 bytes
-rw-r--r--src/Routing/build/dependencies.props46
-rw-r--r--src/Routing/build/repo.props15
-rw-r--r--src/Routing/build/sources.props17
-rw-r--r--src/Routing/samples/RoutingSample.Web/PrefixRoute.cs62
-rw-r--r--src/Routing/samples/RoutingSample.Web/Program.cs50
-rw-r--r--src/Routing/samples/RoutingSample.Web/RouteBuilderExtensions.cs43
-rw-r--r--src/Routing/samples/RoutingSample.Web/RoutingSample.Web.csproj17
-rw-r--r--src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterion.cs14
-rw-r--r--src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValue.cs20
-rw-r--r--src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValueEqualityComparer.cs27
-rw-r--r--src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeBuilder.cs225
-rw-r--r--src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeNode.cs20
-rw-r--r--src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/IClassifier.cs14
-rw-r--r--src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/ItemDescriptor.cs16
-rw-r--r--src/Routing/src/Directory.Build.props7
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteConstraint.cs33
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteHandler.cs24
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouter.cs14
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRoutingFeature.cs16
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj18
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/AssemblyInfo.cs6
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs58
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx126
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteContext.cs58
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteData.cs296
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteDirection.cs21
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs790
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RoutingHttpContextExtensions.cs53
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathContext.cs66
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathData.cs99
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/baseline.netcore.json849
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/AlphaRouteConstraint.cs18
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/BoolRouteConstraint.cs59
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/CompositeRouteConstraint.cs73
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DateTimeRouteConstraint.cs65
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DecimalRouteConstraint.cs59
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DoubleRouteConstraint.cs63
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/FloatRouteConstraint.cs63
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/GuidRouteConstraint.cs61
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/HttpMethodRouteConstraint.cs96
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/IntRouteConstraint.cs59
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LengthRouteConstraint.cs111
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LongRouteConstraint.cs59
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxLengthRouteConstraint.cs73
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxRouteConstraint.cs71
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinLengthRouteConstraint.cs73
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinRouteConstraint.cs71
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/OptionalRouteConstraint.cs66
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RangeRouteConstraint.cs85
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexInlineRouteConstraint.cs20
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexRouteConstraint.cs80
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RequiredRouteConstraint.cs58
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/StringRouteConstraint.cs67
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs156
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs79
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/IInlineConstraintResolver.cs18
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/INamedRouter.cs10
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/IRouteBuilder.cs42
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/IRouteCollection.cs10
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs243
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Internal/BufferValue.cs18
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs163
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Internal/OutboundMatchResult.cs20
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Internal/PathTokenizer.cs205
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Internal/RoutingMarkerService.cs15
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Internal/SegmentState.cs17
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuilderContextPooledObjectPolicy.cs22
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs205
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouteConstraintMatcherExtensions.cs31
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouterMiddlewareLoggerExtensions.cs26
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Logging/TreeRouterLoggerExtensions.cs29
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs126
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj29
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Properties/AssemblyInfo.cs6
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs422
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/RequestDelegateRouteBuilderExtensions.cs295
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Resources.resx204
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Route.cs76
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/RouteBase.cs272
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/RouteBuilder.cs61
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/RouteCollection.cs181
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs191
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintMatcher.cs67
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/RouteCreationException.cs33
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/RouteHandler.cs35
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/RouteOptions.cs73
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs53
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/RouterMiddleware.cs52
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/RoutingBuilderExtensions.cs78
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/RoutingFeature.cs10
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs32
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs131
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs82
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs459
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs456
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs540
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs67
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs22
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateValuesResult.cs29
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs30
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs56
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundMatch.cs23
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundRouteEntry.cs62
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs454
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs423
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs81
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingTree.cs30
-rw-r--r--src/Routing/src/Microsoft.AspNetCore.Routing/baseline.netcore.json4579
-rw-r--r--src/Routing/test/Directory.Build.props20
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj11
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteDataTest.cs157
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs1572
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/VirtualPathDataTests.cs65
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs224
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj15
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj16
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingSampleTest.cs84
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingTestFixture.cs34
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/WebHostBuilderExtensionsTest.cs101
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/BuilderExtensionsTest.cs35
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/ConstraintMatcherTest.cs252
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/AlphaRouteConstraintTests.cs32
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/BoolRouteConstraintTests.cs40
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/CompositeRouteConstraintTests.cs54
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/ConstraintsTestHelper.cs30
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DateTimeRouteConstraintTests.cs53
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DecimalRouteConstraintTests.cs43
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DoubleRouteConstraintTests.cs30
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/FloatRouteConstraintTests.cs29
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/GuidRouteConstraintTests.cs37
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/HttpMethodRouteConstraintTests.cs94
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/IntRouteConstraintsTests.cs29
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LengthRouteConstraintTests.cs103
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LongRouteConstraintTests.cs31
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxLengthRouteConstraintTests.cs43
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxRouteConstraintTests.cs27
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinLengthRouteConstraintTests.cs43
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinRouteConstraintTests.cs27
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RangeRouteConstraintTests.cs48
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexInlineRouteConstraintTests.cs92
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexRouteConstraintTests.cs134
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RequiredRouteConstraintTests.cs93
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/StringRouteConstraintTest.cs157
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultInlineConstraintResolverTest.cs372
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/InlineRouteParameterParserTests.cs992
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs339
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/PathTokenizerTest.cs117
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Logging/WriteContext.cs25
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Microsoft.AspNetCore.Routing.Tests.csproj19
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RequestDelegateRouteBuilderExtensionsTest.cs159
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteBuilderTest.cs55
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteCollectionTest.cs675
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteConstraintBuilderTest.cs190
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteOptionsTests.cs49
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs1863
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouterMiddlewareTest.cs106
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RoutingBuilderExtensionsTest.cs115
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs121
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs1266
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateMatcherTests.cs1142
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs922
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs149
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouteBuilderTest.cs266
-rw-r--r--src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs2209
-rw-r--r--src/Routing/version.props12
-rw-r--r--src/Security/.gitignore32
-rw-r--r--src/Security/Directory.Build.props20
-rw-r--r--src/Security/Directory.Build.targets7
-rw-r--r--src/Security/NuGetPackageVerifier.json13
-rw-r--r--src/Security/README.md17
-rw-r--r--src/Security/Security.sln556
-rw-r--r--src/Security/build/Key.snkbin0 -> 596 bytes
-rw-r--r--src/Security/build/dependencies.props58
-rw-r--r--src/Security/build/repo.props18
-rw-r--r--src/Security/build/sources.props17
-rw-r--r--src/Security/samples/CookiePolicySample/CookiePolicySample.csproj18
-rw-r--r--src/Security/samples/CookiePolicySample/Program.cs26
-rw-r--r--src/Security/samples/CookiePolicySample/Properties/launchSettings.json27
-rw-r--r--src/Security/samples/CookiePolicySample/Startup.cs118
-rw-r--r--src/Security/samples/CookieSample/CookieSample.csproj20
-rw-r--r--src/Security/samples/CookieSample/Program.cs26
-rw-r--r--src/Security/samples/CookieSample/Properties/launchSettings.json27
-rw-r--r--src/Security/samples/CookieSample/Startup.cs45
-rw-r--r--src/Security/samples/CookieSessionSample/CookieSessionSample.csproj20
-rw-r--r--src/Security/samples/CookieSessionSample/MemoryCacheTicketStore.cs55
-rw-r--r--src/Security/samples/CookieSessionSample/Program.cs26
-rw-r--r--src/Security/samples/CookieSessionSample/Properties/launchSettings.json27
-rw-r--r--src/Security/samples/CookieSessionSample/Startup.cs54
-rw-r--r--src/Security/samples/JwtBearerSample/JwtBearerSample.csproj21
-rw-r--r--src/Security/samples/JwtBearerSample/Program.cs21
-rw-r--r--src/Security/samples/JwtBearerSample/Properties/launchSettings.json27
-rw-r--r--src/Security/samples/JwtBearerSample/Startup.cs111
-rw-r--r--src/Security/samples/JwtBearerSample/Todo.cs8
-rw-r--r--src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/app.js28
-rw-r--r--src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/homeCtrl.js13
-rw-r--r--src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/indexCtrl.js5
-rw-r--r--src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/todoListCtrl.js71
-rw-r--r--src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/todoListSvc.js24
-rw-r--r--src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/userDataCtrl.js6
-rw-r--r--src/Security/samples/JwtBearerSample/wwwroot/App/Views/Home.html3
-rw-r--r--src/Security/samples/JwtBearerSample/wwwroot/App/Views/TodoList.html24
-rw-r--r--src/Security/samples/JwtBearerSample/wwwroot/App/Views/UserData.html23
-rw-r--r--src/Security/samples/JwtBearerSample/wwwroot/index.html68
-rw-r--r--src/Security/samples/OpenIdConnect.AzureAdSample/AuthPropertiesTokenCache.cs97
-rw-r--r--src/Security/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.csproj23
-rw-r--r--src/Security/samples/OpenIdConnect.AzureAdSample/Program.cs27
-rw-r--r--src/Security/samples/OpenIdConnect.AzureAdSample/Properties/launchSettings.json27
-rw-r--r--src/Security/samples/OpenIdConnect.AzureAdSample/Readme.md20
-rw-r--r--src/Security/samples/OpenIdConnect.AzureAdSample/Startup.cs203
-rw-r--r--src/Security/samples/OpenIdConnectSample/OpenIdConnectSample.csproj33
-rw-r--r--src/Security/samples/OpenIdConnectSample/Program.cs59
-rw-r--r--src/Security/samples/OpenIdConnectSample/Properties/launchSettings.json28
-rw-r--r--src/Security/samples/OpenIdConnectSample/Readme.md44
-rw-r--r--src/Security/samples/OpenIdConnectSample/Startup.cs297
-rw-r--r--src/Security/samples/OpenIdConnectSample/compiler/resources/cert.pfxbin0 -> 2483 bytes
-rw-r--r--src/Security/samples/SocialSample/Program.cs57
-rw-r--r--src/Security/samples/SocialSample/Properties/launchSettings.json28
-rw-r--r--src/Security/samples/SocialSample/SocialSample.csproj36
-rw-r--r--src/Security/samples/SocialSample/Startup.cs502
-rw-r--r--src/Security/samples/SocialSample/compiler/resources/cert.pfxbin0 -> 2483 bytes
-rw-r--r--src/Security/samples/SocialSample/web.config9
-rw-r--r--src/Security/samples/WsFedSample/Program.cs64
-rw-r--r--src/Security/samples/WsFedSample/Properties/launchSettings.json28
-rw-r--r--src/Security/samples/WsFedSample/Startup.cs168
-rw-r--r--src/Security/samples/WsFedSample/WsFedSample.csproj27
-rw-r--r--src/Security/samples/WsFedSample/compiler/resources/cert.pfxbin0 -> 2483 bytes
-rw-r--r--src/Security/shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs309
-rw-r--r--src/Security/src/Directory.Build.props7
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Constants.cs13
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAppBuilderExtensions.cs37
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationDefaults.cs46
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs451
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs214
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieExtensions.cs32
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieAuthenticationEvents.cs158
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSignedInContext.cs33
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningInContext.cs42
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningOutContext.cs36
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs51
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ICookieManager.cs39
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ITicketStore.cs43
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/LoggingExtensions.cs35
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj20
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/PostConfigureCookieAuthenticationOptions.cs59
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json1621
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookAppBuilderExtensions.cs37
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs18
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookExtensions.cs24
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs80
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs102
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj15
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/Resources.Designer.cs44
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Resources.resx123
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json390
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleAppBuilderExtensions.cs37
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleChallengeProperties.cs89
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.cs21
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleExtensions.cs24
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs108
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs50
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs42
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj15
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Google/Properties/Resources.Designer.cs58
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Google/Resources.resx126
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json550
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs19
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs53
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerEvents.cs42
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/MessageReceivedContext.cs21
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/TokenValidatedContext.cs19
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerAppBuilderExtensions.cs37
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerDefaults.cs16
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerExtensions.cs29
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs323
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs114
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerPostConfigureOptions.cs63
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/LoggingExtensions.cs39
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj19
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/Resources.Designer.cs58
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Resources.resx126
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json1064
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj15
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs37
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs18
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountExtensions.cs24
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs42
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs35
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/Resources.Designer.cs72
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Resources.resx129
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/baseline.netcore.json284
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimAction.cs42
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollection.cs52
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs139
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/CustomJsonClaimAction.cs46
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/DeleteClaimAction.cs33
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs57
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonSubKeyClaimAction.cs58
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/MapAllClaimsAction.cs42
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs152
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthEvents.cs41
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.csproj19
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthAppBuilderExtensions.cs37
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthChallengeProperties.cs41
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthDefaults.cs10
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthExtensions.cs33
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs243
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs108
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthPostConfigureOptions.cs45
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthTokenResponse.cs42
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/Resources.Designer.cs58
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Resources.resx126
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json1810
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/ClaimActionCollectionUniqueExtensions.cs39
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/UniqueJsonKeyClaimAction.cs61
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs20
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs93
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs25
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs86
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RedirectContext.cs34
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs17
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs29
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenValidatedContext.cs28
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs21
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs508
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj19
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectAppBuilderExtensions.cs37
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectChallengeProperties.cs49
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectDefaults.cs41
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectExtensions.cs29
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs1240
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs326
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectPostConfigureOptions.cs114
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectRedirectBehavior.cs21
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/Resources.Designer.cs114
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Resources.resx138
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json2452
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs77
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterEvents.cs41
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/LoggingExtensions.cs46
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/AccessToken.cs21
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestToken.cs30
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestTokenSerializer.cs104
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj15
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/Resources.Designer.cs58
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Resources.resx126
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterAppBuilderExtensions.cs37
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs12
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterExtensions.cs29
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs371
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs128
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterPostConfigureOptions.cs51
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json924
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs35
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs33
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs44
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RemoteSignoutContext.cs30
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenReceivedContext.cs28
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.cs34
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs74
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/LoggingExtensions.cs85
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Microsoft.AspNetCore.Authentication.WsFederation.csproj17
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Properties/Resources.Designer.cs114
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Resources.resx138
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationDefaults.cs26
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationExtensions.cs58
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationHandler.cs425
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationOptions.cs180
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs89
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/baseline.netcore.json1314
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/AuthAppBuilderExtensions.cs29
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationBuilder.cs120
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs244
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs64
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs93
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationServiceCollectionExtensions.cs108
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Data/IDataSerializer.cs11
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Data/ISecureDataFormat.cs13
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesDataFormat.cs16
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesSerializer.cs87
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Data/SecureDataFormat.cs79
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Data/TextEncoder.cs30
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketDataFormat.cs15
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketSerializer.cs275
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Events/BaseContext.cs65
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Events/HandleRequestContext.cs32
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Events/PrincipalContext.cs30
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Events/PropertiesContext.cs31
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Events/RedirectContext.cs38
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationContext.cs49
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.cs25
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteFailureContext.cs34
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Events/ResultContext.cs65
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Events/TicketReceivedContext.cs24
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs96
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/ISystemClock.cs19
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Internal/RequestPathBaseCookieBuilder.cs38
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs125
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj22
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeHandler.cs36
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeOptions.cs11
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Properties/Resources.Designer.cs100
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs245
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs153
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/Resources.resx135
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/SignInAuthenticationHandler.cs39
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/SignOutAuthenticationHandler.cs36
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/SystemClock.cs27
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json3330
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization.Policy/IPolicyEvaluator.cs40
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj20
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyAuthorizationResult.cs35
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyEvaluator.cs96
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyServiceCollectionExtensions.cs31
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json211
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/AllowAnonymousAttribute.cs15
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationFailure.cs46
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandler.cs68
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs98
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs87
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicy.cs165
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.cs250
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationResult.cs37
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs59
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceExtensions.cs118
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs53
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationEvaluator.cs23
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerContextFactory.cs29
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerProvider.cs35
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs54
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs135
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/IAllowAnonymous.cs12
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationEvaluator.cs18
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandler.cs19
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerContextFactory.cs26
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerProvider.cs21
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs26
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationRequirement.cs12
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationService.cs54
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizeData.cs26
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/AssertionRequirement.cs60
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/ClaimsAuthorizationRequirement.cs73
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/DenyAnonymousAuthorizationRequirement.cs33
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/NameAuthorizationRequirement.cs52
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/OperationAuthorizationRequirement.cs17
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/PassThroughAuthorizationHandler.cs27
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/RolesAuthorizationRequirement.cs68
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/LoggingExtensions.cs31
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj20
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/Properties/Resources.Designer.cs72
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/Resources.resx129
-rw-r--r--src/Security/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json1947
-rw-r--r--src/Security/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs26
-rw-r--r--src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyAppBuilderExtensions.cs50
-rw-r--r--src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs56
-rw-r--r--src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs52
-rw-r--r--src/Security/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs24
-rw-r--r--src/Security/src/Microsoft.AspNetCore.CookiePolicy/HttpOnlyPolicy.cs11
-rw-r--r--src/Security/src/Microsoft.AspNetCore.CookiePolicy/LoggingExtensions.cs105
-rw-r--r--src/Security/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.csproj17
-rw-r--r--src/Security/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs281
-rw-r--r--src/Security/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json548
-rw-r--r--src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketDataFormat.cs17
-rw-r--r--src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketSerializer.cs220
-rw-r--r--src/Security/src/Microsoft.Owin.Security.Interop/ChunkingCookieManager.cs280
-rw-r--r--src/Security/src/Microsoft.Owin.Security.Interop/Constants.cs13
-rw-r--r--src/Security/src/Microsoft.Owin.Security.Interop/DataProtectorShim.cs31
-rw-r--r--src/Security/src/Microsoft.Owin.Security.Interop/Microsoft.Owin.Security.Interop.csproj16
-rw-r--r--src/Security/src/Microsoft.Owin.Security.Interop/Properties/AssemblyInfo.cs8
-rw-r--r--src/Security/src/Microsoft.Owin.Security.Interop/baseline.netframework.json372
-rw-r--r--src/Security/test/Directory.Build.props19
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/AuthenticationMiddlewareTests.cs196
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/Base64UrlTextEncoderTests.cs30
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs112
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/CookieTests.cs1989
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/DynamicSchemeTests.cs170
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs836
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs1622
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs1237
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj47
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs713
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthChallengePropertiesTest.cs149
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthTests.cs752
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/MockOpenIdConnectMessage.cs21
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs616
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs574
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs1347
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectTests.cs351
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerBuilder.cs120
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerExtensions.cs49
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs351
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestTransaction.cs40
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownconfig.json23
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownkeys.json31
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/PolicyTests.cs487
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/SecureDataFormatTests.cs80
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestClock.cs23
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestExtensions.cs86
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHandlers.cs115
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHttpMessageHandler.cs24
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/TicketSerializerTests.cs130
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs181
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/Transaction.cs62
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/TwitterTests.cs685
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/CustomStateDataFormat.cs58
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/InvalidToken.xml83
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityToken.cs27
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityTokenValidator.cs31
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/ValidToken.xml83
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/WsFederationTest.cs443
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/federationmetadata.xml132
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/katanatest.redmond.corp.microsoft.com.cerbin0 -> 1462 bytes
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authentication.Test/selfSigned.cerbin0 -> 762 bytes
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationPolicyFacts.cs159
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs1168
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj18
-rw-r--r--src/Security/test/Microsoft.AspNetCore.Authorization.Test/PolicyEvaluatorTests.cs209
-rw-r--r--src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/CookieChunkingTests.cs131
-rw-r--r--src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj15
-rw-r--r--src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs660
-rw-r--r--src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs471
-rw-r--r--src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj17
-rw-r--r--src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/TestExtensions.cs68
-rw-r--r--src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Transaction.cs51
-rw-r--r--src/Security/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs332
-rw-r--r--src/Security/test/Microsoft.Owin.Security.Interop.Test/Microsoft.Owin.Security.Interop.Test.csproj18
-rw-r--r--src/Security/test/Microsoft.Owin.Security.Interop.Test/TicketInteropTests.cs91
-rw-r--r--src/Security/version.props12
-rw-r--r--src/ServerTests/Directory.Build.props2
-rw-r--r--src/Session/Directory.Build.props2
-rw-r--r--src/StaticFiles/Directory.Build.props2
-rw-r--r--src/Templating/Directory.Build.props2
864 files changed, 131878 insertions, 143 deletions
diff --git a/.gitmodules b/.gitmodules
index 0af0534995..eff5315721 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,7 +1,3 @@
-[submodule "modules/Antiforgery"]
- path = modules/Antiforgery
- url = https://github.com/aspnet/Antiforgery.git
- branch = release/2.2
[submodule "modules/BasicMiddleware"]
path = modules/BasicMiddleware
url = https://github.com/aspnet/BasicMiddleware.git
@@ -10,18 +6,10 @@
path = modules/BrowserLink
url = https://github.com/aspnet/BrowserLink.git
branch = release/2.2
-[submodule "modules/CORS"]
- path = modules/CORS
- url = https://github.com/aspnet/CORS.git
- branch = release/2.2
[submodule "modules/EntityFrameworkCore"]
path = modules/EntityFrameworkCore
url = https://github.com/aspnet/EntityFrameworkCore.git
branch = release/2.2
-[submodule "modules/HttpSysServer"]
- path = modules/HttpSysServer
- url = https://github.com/aspnet/HttpSysServer.git
- branch = release/2.2
[submodule "modules/Identity"]
path = modules/Identity
url = https://github.com/aspnet/Identity.git
@@ -42,22 +30,10 @@
path = modules/Razor
url = https://github.com/aspnet/Razor.git
branch = release/2.2
-[submodule "modules/ResponseCaching"]
- path = modules/ResponseCaching
- url = https://github.com/aspnet/ResponseCaching.git
- branch = release/2.2
-[submodule "modules/Routing"]
- path = modules/Routing
- url = https://github.com/aspnet/Routing.git
- branch = release/2.2
[submodule "modules/Scaffolding"]
path = modules/Scaffolding
url = https://github.com/aspnet/Scaffolding.git
branch = release/2.2
-[submodule "modules/Security"]
- path = modules/Security
- url = https://github.com/aspnet/Security.git
- branch = release/2.2
[submodule "modules/SignalR"]
path = modules/SignalR
url = https://github.com/aspnet/SignalR.git
diff --git a/build/buildorder.props b/build/buildorder.props
index 57d6541aa8..a72614523e 100644
--- a/build/buildorder.props
+++ b/build/buildorder.props
@@ -9,18 +9,18 @@
<ItemGroup>
<RepositoryBuildOrder Include="Razor" Order="6" />
<RepositoryBuildOrder Include="EntityFrameworkCore" Order="8" />
- <RepositoryBuildOrder Include="HttpSysServer" Order="8" />
+ <RepositoryBuildOrder Include="HttpSysServer" Order="8" RootPath="$(RepositoryRoot)src\HttpSysServer\" />
<RepositoryBuildOrder Include="BasicMiddleware" Order="9" />
- <RepositoryBuildOrder Include="Antiforgery" Order="10" />
+ <RepositoryBuildOrder Include="Antiforgery" Order="9" RootPath="$(RepositoryRoot)src\Antiforgery\" />
<RepositoryBuildOrder Include="IISIntegration" Order="10" RootPath="$(RepositoryRoot)src\IISIntegration\" />
- <RepositoryBuildOrder Include="CORS" Order="12" />
+ <RepositoryBuildOrder Include="CORS" Order="11" RootPath="$(RepositoryRoot)src\CORS\" />
<RepositoryBuildOrder Include="StaticFiles" Order="11" RootPath="$(RepositoryRoot)src\StaticFiles\" />
- <RepositoryBuildOrder Include="Routing" Order="12" />
- <RepositoryBuildOrder Include="ResponseCaching" Order="11" />
+ <RepositoryBuildOrder Include="Routing" Order="11" RootPath="$(RepositoryRoot)src\Routing\" />
+ <RepositoryBuildOrder Include="ResponseCaching" Order="11" RootPath="$(RepositoryRoot)src\ResponseCaching\" />
<RepositoryBuildOrder Include="Session" Order="11" RootPath="$(RepositoryRoot)src\Session\" />
<RepositoryBuildOrder Include="ServerTests" Order="11" RootPath="$(RepositoryRoot)src\ServerTests\" />
- <RepositoryBuildOrder Include="Localization" Order="13" />
- <RepositoryBuildOrder Include="Security" Order="13" />
+ <RepositoryBuildOrder Include="Localization" Order="12" />
+ <RepositoryBuildOrder Include="Security" Order="13" RootPath="$(RepositoryRoot)src\Security\" />
<RepositoryBuildOrder Include="MetaPackages" Order="13" RootPath="$(RepositoryRoot)src\MetaPackages\" />
<RepositoryBuildOrder Include="Mvc" Order="14" />
<RepositoryBuildOrder Include="AADIntegration" Order="15" RootPath="$(RepositoryRoot)src\AADIntegration\" />
diff --git a/build/submodules.props b/build/submodules.props
index ecec882447..34fbbba45c 100644
--- a/build/submodules.props
+++ b/build/submodules.props
@@ -48,12 +48,12 @@
<ItemGroup>
<ShippedRepository Include="AADIntegration" RootPath="$(RepositoryRoot)src\AADIntegration\" />
- <ShippedRepository Include="Antiforgery" />
+ <ShippedRepository Include="Antiforgery" RootPath="$(RepositoryRoot)src\Antiforgery\" />
<ShippedRepository Include="AzureIntegration" RootPath="$(RepositoryRoot)src\AzureIntegration\" />
<ShippedRepository Include="BasicMiddleware" />
- <ShippedRepository Include="CORS" />
+ <ShippedRepository Include="CORS" RootPath="$(RepositoryRoot)src\CORS\" />
<ShippedRepository Include="EntityFrameworkCore" />
- <ShippedRepository Include="HttpSysServer" />
+ <ShippedRepository Include="HttpSysServer" RootPath="$(RepositoryRoot)src\HttpSysServer\" />
<ShippedRepository Include="Identity" />
<ShippedRepository Include="JavaScriptServices" RootPath="$(RepositoryRoot)src\JavaScriptServices\" />
<ShippedRepository Include="Localization" />
@@ -61,9 +61,9 @@
<ShippedRepository Include="Mvc" />
<ShippedRepository Include="MvcPrecompilation" />
<ShippedRepository Include="Razor" />
- <ShippedRepository Include="ResponseCaching" />
- <ShippedRepository Include="Routing" />
- <ShippedRepository Include="Security" />
+ <ShippedRepository Include="ResponseCaching" RootPath="$(RepositoryRoot)src\ResponseCaching\" />
+ <ShippedRepository Include="Routing" RootPath="$(RepositoryRoot)src\Routing\" />
+ <ShippedRepository Include="Security" RootPath="$(RepositoryRoot)src\Security\" />
<ShippedRepository Include="Session" RootPath="$(RepositoryRoot)src\Session\" />
<ShippedRepository Include="SignalR" />
<ShippedRepository Include="StaticFiles" RootPath="$(RepositoryRoot)src\StaticFiles\" />
diff --git a/modules/Antiforgery b/modules/Antiforgery
deleted file mode 160000
-Subproject 9e5146cff912ebbd003d597eb055144e740759f
diff --git a/modules/CORS b/modules/CORS
deleted file mode 160000
-Subproject f05b0e792d2361be214947798857ef3eac77825
diff --git a/modules/HttpSysServer b/modules/HttpSysServer
deleted file mode 160000
-Subproject 3e08bf8833827737a5bd1b64210c7a5e8e2941e
diff --git a/modules/ResponseCaching b/modules/ResponseCaching
deleted file mode 160000
-Subproject 9f49398f28d7c1e0dc0305f8080351c413b0f29
diff --git a/modules/Routing b/modules/Routing
deleted file mode 160000
-Subproject 3d828221a19d91907c52c2f40928b019bee1ef9
diff --git a/modules/Security b/modules/Security
deleted file mode 160000
-Subproject 93926543f8469614c2feb23de8a8c0561b8b246
diff --git a/src/AADIntegration/Directory.Build.props b/src/AADIntegration/Directory.Build.props
index fa923a5f7d..1cc732270e 100644
--- a/src/AADIntegration/Directory.Build.props
+++ b/src/AADIntegration/Directory.Build.props
@@ -9,7 +9,7 @@
<PropertyGroup>
<Product>Microsoft ASP.NET Core AAD Integration</Product>
- <RepositoryUrl>https://github.com/aspnet/AADIntegration</RepositoryUrl>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)..\..\eng\AspNetCore.snk</AssemblyOriginatorKeyFile>
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..811534b9fb
--- /dev/null
+++ b/src/Antiforgery/Directory.Build.props
@@ -0,0 +1,24 @@
+<Project>
+ <Import
+ Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))\AspNetCoreSettings.props"
+ Condition=" '$(CI)' != 'true' AND '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))' != '' " />
+
+ <Import Project="version.props" />
+ <Import Project="build\dependencies.props" />
+ <Import Project="build\sources.props" />
+
+ <PropertyGroup>
+ <Product>Microsoft ASP.NET Core</Product>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
+ <RepositoryType>git</RepositoryType>
+ <RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
+ <AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)build\Key.snk</AssemblyOriginatorKeyFile>
+ <SignAssembly>true</SignAssembly>
+ <PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
+ </ItemGroup>
+</Project>
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 @@
+<Project>
+ <PropertyGroup>
+ <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">$(MicrosoftNETCoreApp20PackageVersion)</RuntimeFrameworkVersion>
+ <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">$(MicrosoftNETCoreApp21PackageVersion)</RuntimeFrameworkVersion>
+ <NETStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard2.0' ">$(NETStandardLibrary20PackageVersion)</NETStandardImplicitPackageVersion>
+ </PropertyGroup>
+</Project>
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
--- /dev/null
+++ b/src/Antiforgery/build/Key.snk
Binary files 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 @@
+<Project>
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ </PropertyGroup>
+
+ <!-- These package versions may be overridden or updated by automation. -->
+ <PropertyGroup Label="Package Versions: Auto">
+ <InternalAspNetCoreSdkPackageVersion>2.1.3-rtm-15802</InternalAspNetCoreSdkPackageVersion>
+ <MicrosoftNETCoreApp20PackageVersion>2.0.0</MicrosoftNETCoreApp20PackageVersion>
+ <MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion>
+ <MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
+ <MoqPackageVersion>4.7.49</MoqPackageVersion>
+ <NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
+ <XunitPackageVersion>2.3.1</XunitPackageVersion>
+ <XunitRunnerVisualStudioPackageVersion>2.4.0-beta.1.build3945</XunitRunnerVisualStudioPackageVersion>
+ </PropertyGroup>
+
+ <!-- This may import a generated file which may override the variables above. -->
+ <Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
+
+ <!-- These are package versions that should not be overridden or updated by automation. -->
+ <PropertyGroup Label="Package Versions: Pinned">
+ <MicrosoftAspNetCoreDataProtectionPackageVersion>2.1.1</MicrosoftAspNetCoreDataProtectionPackageVersion>
+ <MicrosoftAspNetCoreHttpAbstractionsPackageVersion>2.1.1</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
+ <MicrosoftAspNetCoreHttpExtensionsPackageVersion>2.1.1</MicrosoftAspNetCoreHttpExtensionsPackageVersion>
+ <MicrosoftAspNetCoreHttpPackageVersion>2.1.1</MicrosoftAspNetCoreHttpPackageVersion>
+ <MicrosoftAspNetCoreTestingPackageVersion>2.1.0</MicrosoftAspNetCoreTestingPackageVersion>
+ <MicrosoftAspNetCoreWebUtilitiesPackageVersion>2.1.1</MicrosoftAspNetCoreWebUtilitiesPackageVersion>
+ <MicrosoftExtensionsDependencyInjectionPackageVersion>2.1.1</MicrosoftExtensionsDependencyInjectionPackageVersion>
+ <MicrosoftExtensionsLoggingPackageVersion>2.1.1</MicrosoftExtensionsLoggingPackageVersion>
+ <MicrosoftExtensionsLoggingTestingPackageVersion>2.1.1</MicrosoftExtensionsLoggingTestingPackageVersion>
+ <MicrosoftExtensionsObjectPoolPackageVersion>2.1.1</MicrosoftExtensionsObjectPoolPackageVersion>
+ <MicrosoftExtensionsWebEncodersPackageVersion>2.1.1</MicrosoftExtensionsWebEncodersPackageVersion>
+ </PropertyGroup>
+</Project> \ 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 @@
+<Project>
+ <Import Project="dependencies.props" />
+
+ <PropertyGroup>
+ <LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
+ <LineupPackageVersion>2.1.0-rc1-*</LineupPackageVersion>
+ <LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp20PackageVersion)" />
+ <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
+ </ItemGroup>
+</Project>
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 @@
+<Project>
+ <Import Project="$(DotNetRestoreSourcePropsPath)" Condition="'$(DotNetRestoreSourcePropsPath)' != ''"/>
+
+ <PropertyGroup Label="RestoreSources">
+ <RestoreSources>$(DotNetRestoreSources)</RestoreSources>
+ <RestoreSources Condition="'$(DotNetBuildOffline)' != 'true' AND '$(AspNetUniverseBuildOffline)' != 'true' ">
+ $(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>
+ <RestoreSources Condition="'$(DotNetBuildOffline)' != 'true'">
+ $(RestoreSources);
+ https://api.nuget.org/v3/index.json;
+ </RestoreSources>
+ </PropertyGroup>
+</Project>
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
+{
+ /// <summary>
+ /// Provides programmatic configuration for the antiforgery token system.
+ /// </summary>
+ 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,
+ };
+
+ /// <summary>
+ /// The default cookie prefix, which is ".AspNetCore.Antiforgery.".
+ /// </summary>
+ public static readonly string DefaultCookiePrefix = ".AspNetCore.Antiforgery.";
+
+ /// <summary>
+ /// Determines the settings used to create the antiforgery cookies.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// If an explicit <see cref="CookieBuilder.Name"/> is not provided, the system will automatically generate a
+ /// unique name that begins with <see cref="DefaultCookiePrefix"/>.
+ /// </para>
+ /// <para>
+ /// <see cref="CookieBuilder.SameSite"/> defaults to <see cref="SameSiteMode.Strict"/>.
+ /// <see cref="CookieBuilder.HttpOnly"/> defaults to <c>true</c>.
+ /// <see cref="CookieBuilder.IsEssential"/> defaults to <c>true</c>. 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.
+ /// <see cref="CookieBuilder.SecurePolicy"/> defaults to <see cref="CookieSecurePolicy.None"/>.
+ /// </para>
+ /// </remarks>
+ public CookieBuilder Cookie
+ {
+ get => _cookieBuilder;
+ set => _cookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ /// <summary>
+ /// Specifies the name of the antiforgery token field that is used by the antiforgery system.
+ /// </summary>
+ public string FormFieldName
+ {
+ get => _formFieldName;
+ set => _formFieldName = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ /// <summary>
+ /// Specifies the name of the header value that is used by the antiforgery system. If <c>null</c> then
+ /// antiforgery validation will only consider form data.
+ /// </summary>
+ public string HeaderName { get; set; } = AntiforgeryTokenHeaderName;
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ public bool SuppressXFrameOptionsHeader { get; set; }
+
+ #region Obsolete API
+ /// <summary>
+ /// <para>
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is <seealso cref="CookieBuilder.Name"/> on <see cref="Cookie"/>.
+ /// </para>
+ /// <para>
+ /// Specifies the name of the cookie that is used by the antiforgery system.
+ /// </para>
+ /// </summary>
+ /// <remarks>
+ /// If an explicit name is not provided, the system will automatically generate a
+ /// unique name that begins with <see cref="DefaultCookiePrefix"/>.
+ /// </remarks>
+ [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; }
+
+ /// <summary>
+ /// <para>
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is <seealso cref="CookieBuilder.Path"/> on <see cref="Cookie"/>.
+ /// </para>
+ /// <para>
+ /// The path set on the cookie. If set to <c>null</c>, the "path" attribute on the cookie is set to the current
+ /// request's <see cref="HttpRequest.PathBase"/> value. If the value of <see cref="HttpRequest.PathBase"/> is
+ /// <c>null</c> or empty, then the "path" attribute is set to the value of <see cref="CookieOptions.Path"/>.
+ /// </para>
+ /// </summary>
+ [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; }
+
+ /// <summary>
+ /// <para>
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is <seealso cref="CookieBuilder.Domain"/> on <see cref="Cookie"/>.
+ /// </para>
+ /// <para>
+ /// The domain set on the cookie. By default its <c>null</c> which results in the "domain" attribute not being set.
+ /// </para>
+ /// </summary>
+ [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; }
+
+
+ /// <summary>
+ /// <para>
+ /// This property is obsolete and will be removed in a future version.
+ /// The recommended alternative is to set <seealso cref="CookieBuilder.SecurePolicy"/> on <see cref="Cookie"/>.
+ /// </para>
+ /// <para>
+ /// <c>true</c> is equivalent to <see cref="CookieSecurePolicy.Always"/>.
+ /// <c>false</c> is equivalent to <see cref="CookieSecurePolicy.None"/>.
+ /// </para>
+ /// <para>
+ /// 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.
+ /// </para>
+ /// </summary>
+ [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
+{
+ /// <summary>
+ /// Extension methods for setting up antiforgery services in an <see cref="IServiceCollection" />.
+ /// </summary>
+ public static class AntiforgeryServiceCollectionExtensions
+ {
+ /// <summary>
+ /// Adds antiforgery services to the specified <see cref="IServiceCollection" />.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
+ /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
+ 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<IConfigureOptions<AntiforgeryOptions>, AntiforgeryOptionsSetup>());
+
+ services.TryAddSingleton<IAntiforgery, DefaultAntiforgery>();
+ services.TryAddSingleton<IAntiforgeryTokenGenerator, DefaultAntiforgeryTokenGenerator>();
+ services.TryAddSingleton<IAntiforgeryTokenSerializer, DefaultAntiforgeryTokenSerializer>();
+ services.TryAddSingleton<IAntiforgeryTokenStore, DefaultAntiforgeryTokenStore>();
+ services.TryAddSingleton<IClaimUidExtractor, DefaultClaimUidExtractor>();
+ services.TryAddSingleton<IAntiforgeryAdditionalDataProvider, DefaultAntiforgeryAdditionalDataProvider>();
+
+ services.TryAddSingleton<ObjectPool<AntiforgerySerializationContext>>(serviceProvider =>
+ {
+ var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
+ var policy = new AntiforgerySerializationContextPooledObjectPolicy();
+ return provider.Create(policy);
+ });
+
+ return services;
+ }
+
+ /// <summary>
+ /// Adds antiforgery services to the specified <see cref="IServiceCollection" />.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
+ /// <param name="setupAction">An <see cref="Action{AntiforgeryOptions}"/> to configure the provided <see cref="AntiforgeryOptions"/>.</param>
+ /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
+ public static IServiceCollection AddAntiforgery(this IServiceCollection services, Action<AntiforgeryOptions> 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
+{
+ /// <summary>
+ /// The antiforgery token pair (cookie and request token) for a request.
+ /// </summary>
+ public class AntiforgeryTokenSet
+ {
+ /// <summary>
+ /// Creates the antiforgery token pair (cookie and request token) for a request.
+ /// </summary>
+ /// <param name="requestToken">The token that is supplied in the request.</param>
+ /// <param name="cookieToken">The token that is supplied in the request cookie.</param>
+ /// <param name="formFieldName">The name of the form field used for the request token.</param>
+ /// <param name="headerName">The name of the header used for the request token.</param>
+ 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;
+ }
+
+ /// <summary>
+ /// Gets the request token.
+ /// </summary>
+ public string RequestToken { get; }
+
+ /// <summary>
+ /// Gets the name of the form field used for the request token.
+ /// </summary>
+ public string FormFieldName { get; }
+
+ /// <summary>
+ /// Gets the name of the header used for the request token.
+ /// </summary>
+ public string HeaderName { get; }
+
+ /// <summary>
+ /// Gets the cookie token.
+ /// </summary>
+ 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
+{
+ /// <summary>
+ /// The <see cref="Exception"/> that is thrown when the antiforgery token validation fails.
+ /// </summary>
+ public class AntiforgeryValidationException : Exception
+ {
+ /// <summary>
+ /// Creates a new instance of <see cref="AntiforgeryValidationException"/> with the specified
+ /// exception message.
+ /// </summary>
+ /// <param name="message">The message that describes the error.</param>
+ public AntiforgeryValidationException(string message)
+ : base(message)
+ {
+ }
+
+ /// <summary>
+ /// Creates a new instance of <see cref="AntiforgeryValidationException"/> with the specified
+ /// exception message and inner exception.
+ /// </summary>
+ /// <param name="message">The message that describes the error.</param>
+ /// <param name="innerException">The inner <see cref="Exception"/>.</param>
+ 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
+{
+ /// <summary>
+ /// Provides access to the antiforgery system, which provides protection against
+ /// Cross-site Request Forgery (XSRF, also called CSRF) attacks.
+ /// </summary>
+ public interface IAntiforgery
+ {
+ /// <summary>
+ /// Generates an <see cref="AntiforgeryTokenSet"/> 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".
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
+ /// <returns>An <see cref="AntiforgeryTokenSet" /> with tokens for the response.</returns>
+ /// <remarks>
+ /// This method has a side effect:
+ /// A response cookie is set if there is no valid cookie associated with the request.
+ /// </remarks>
+ AntiforgeryTokenSet GetAndStoreTokens(HttpContext httpContext);
+
+ /// <summary>
+ /// Generates an <see cref="AntiforgeryTokenSet"/> for this request.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
+ /// <remarks>
+ /// Unlike <see cref="GetAndStoreTokens(HttpContext)"/>, this method has no side effect. The caller
+ /// is responsible for setting the response cookie and injecting the returned
+ /// form token as appropriate.
+ /// </remarks>
+ AntiforgeryTokenSet GetTokens(HttpContext httpContext);
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
+ /// <returns>
+ /// A <see cref="Task{Boolean}"/> that, when completed, returns <c>true</c> if the request uses a safe HTTP
+ /// method or contains a valid antiforgery token, otherwise returns <c>false</c>.
+ /// </returns>
+ Task<bool> IsRequestValidAsync(HttpContext httpContext);
+
+ /// <summary>
+ /// Validates an antiforgery token that was supplied as part of the request.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
+ /// <exception cref="AntiforgeryValidationException">
+ /// Thrown when the request does not include a valid antiforgery token.
+ /// </exception>
+ Task ValidateRequestAsync(HttpContext httpContext);
+
+ /// <summary>
+ /// Generates and stores an antiforgery cookie token if one is not available or not valid.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
+ 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
+{
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <remarks>
+ /// The antiforgery system already embeds the client's username within the
+ /// generated tokens. This interface provides and consumes <em>supplemental</em>
+ /// data. If an incoming antiforgery token contains supplemental data but no
+ /// additional data provider is configured, the supplemental data will not be
+ /// validated.
+ /// </remarks>
+ public interface IAntiforgeryAdditionalDataProvider
+ {
+ /// <summary>
+ /// Provides additional data to be stored for the antiforgery tokens generated
+ /// during this request.
+ /// </summary>
+ /// <param name="context">Information about the current request.</param>
+ /// <returns>Supplemental data to embed within the antiforgery token.</returns>
+ string GetAdditionalData(HttpContext context);
+
+ /// <summary>
+ /// Validates additional data that was embedded inside an incoming antiforgery
+ /// token.
+ /// </summary>
+ /// <param name="context">Information about the current request.</param>
+ /// <param name="additionalData">Supplemental data that was embedded within the token.</param>
+ /// <returns>True if the data is valid; false if the data is invalid.</returns>
+ 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
+{
+ /// <summary>
+ /// Used to hold per-request state.
+ /// </summary>
+ 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<ILogger, Exception> _failedToDeserialzeTokens;
+ private static readonly Action<ILogger, string, Exception> _validationFailed;
+ private static readonly Action<ILogger, Exception> _validated;
+ private static readonly Action<ILogger, string, Exception> _missingCookieToken;
+ private static readonly Action<ILogger, string, string, Exception> _missingRequestToken;
+ private static readonly Action<ILogger, Exception> _newCookieToken;
+ private static readonly Action<ILogger, Exception> _reusedCookieToken;
+ private static readonly Action<ILogger, Exception> _tokenDeserializeException;
+ private static readonly Action<ILogger, Exception> _responseCacheHeadersOverridenToNoCache;
+
+ static AntiforgeryLoggerExtensions()
+ {
+ _validationFailed = LoggerMessage.Define<string>(
+ LogLevel.Warning,
+ 1,
+ "Antiforgery validation failed with message '{Message}'.");
+ _validated = LoggerMessage.Define(
+ LogLevel.Debug,
+ 2,
+ "Antiforgery successfully validated a request.");
+ _missingCookieToken = LoggerMessage.Define<string>(
+ LogLevel.Warning,
+ 3,
+ "The required antiforgery cookie '{CookieName}' is not present.");
+ _missingRequestToken = LoggerMessage.Define<string, string>(
+ 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<AntiforgeryOptions>
+ {
+ public AntiforgeryOptionsSetup(IOptions<DataProtectionOptions> 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<AntiforgerySerializationContext>
+ {
+ 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<BinaryBlob>
+ {
+ 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
+{
+ /// <summary>
+ /// Provides access to the antiforgery system, which provides protection against
+ /// Cross-site Request Forgery (XSRF, also called CSRF) attacks.
+ /// </summary>
+ public class DefaultAntiforgery : IAntiforgery
+ {
+ private readonly AntiforgeryOptions _options;
+ private readonly IAntiforgeryTokenGenerator _tokenGenerator;
+ private readonly IAntiforgeryTokenSerializer _tokenSerializer;
+ private readonly IAntiforgeryTokenStore _tokenStore;
+ private readonly ILogger<DefaultAntiforgery> _logger;
+
+ public DefaultAntiforgery(
+ IOptions<AntiforgeryOptions> antiforgeryOptionsAccessor,
+ IAntiforgeryTokenGenerator tokenGenerator,
+ IAntiforgeryTokenSerializer tokenSerializer,
+ IAntiforgeryTokenStore tokenStore,
+ ILoggerFactory loggerFactory)
+ {
+ _options = antiforgeryOptionsAccessor.Value;
+ _tokenGenerator = tokenGenerator;
+ _tokenSerializer = tokenSerializer;
+ _tokenStore = tokenStore;
+ _logger = loggerFactory.CreateLogger<DefaultAntiforgery>();
+ }
+
+ /// <inheritdoc />
+ 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;
+ }
+
+ /// <inheritdoc />
+ public AntiforgeryTokenSet GetTokens(HttpContext httpContext)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ CheckSSLConfig(httpContext);
+
+ var antiforgeryFeature = GetTokensInternal(httpContext);
+ return Serialize(antiforgeryFeature);
+ }
+
+ /// <inheritdoc />
+ public async Task<bool> 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;
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+ }
+
+ /// <inheritdoc />
+ 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<IAntiforgeryFeature>();
+ 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;
+ }
+
+ /// <summary>
+ /// Sets the 'Cache-Control' header to 'no-cache, no-store' and 'Pragma' header to 'no-cache' overriding any user set value.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContext"/>.</param>
+ 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
+{
+ /// <summary>
+ /// A default <see cref="IAntiforgeryAdditionalDataProvider"/> implementation.
+ /// </summary>
+ public class DefaultAntiforgeryAdditionalDataProvider : IAntiforgeryAdditionalDataProvider
+ {
+ /// <inheritdoc />
+ public virtual string GetAdditionalData(HttpContext context)
+ {
+ return string.Empty;
+ }
+
+ /// <inheritdoc />
+ 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;
+ }
+
+ /// <inheritdoc />
+ public AntiforgeryToken GenerateCookieToken()
+ {
+ return new AntiforgeryToken()
+ {
+ // SecurityToken will be populated automatically.
+ IsCookieToken = true
+ };
+ }
+
+ /// <inheritdoc />
+ 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;
+ }
+
+ /// <inheritdoc />
+ public bool IsCookieTokenValid(AntiforgeryToken cookieToken)
+ {
+ return cookieToken != null && cookieToken.IsCookieToken;
+ }
+
+ /// <inheritdoc />
+ 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<ClaimsIdentity>;
+ 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<AntiforgerySerializationContext> _pool;
+
+ public DefaultAntiforgeryTokenSerializer(
+ IDataProtectionProvider provider,
+ ObjectPool<AntiforgerySerializationContext> 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<AntiforgeryOptions> 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<AntiforgeryTokenSet> 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
+{
+ /// <summary>
+ /// Default implementation of <see cref="IClaimUidExtractor"/>.
+ /// </summary>
+ public class DefaultClaimUidExtractor : IClaimUidExtractor
+ {
+ private readonly ObjectPool<AntiforgerySerializationContext> _pool;
+
+ public DefaultClaimUidExtractor(ObjectPool<AntiforgerySerializationContext> pool)
+ {
+ _pool = pool;
+ }
+
+ /// <inheritdoc />
+ 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<string> GetUniqueIdentifierParameters(IEnumerable<ClaimsIdentity> claimsIdentities)
+ {
+ var identitiesList = claimsIdentities as List<ClaimsIdentity>;
+ if (identitiesList == null)
+ {
+ identitiesList = new List<ClaimsIdentity>(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<Claim>();
+ 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<string>(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<string> 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
+{
+ /// <summary>
+ /// Generates and validates antiforgery tokens.
+ /// </summary>
+ public interface IAntiforgeryTokenGenerator
+ {
+ /// <summary>
+ /// Generates a new random cookie token.
+ /// </summary>
+ /// <returns>An <see cref="AntiforgeryToken"/>.</returns>
+ AntiforgeryToken GenerateCookieToken();
+
+ /// <summary>
+ /// Generates a request token corresponding to <paramref name="cookieToken"/>.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
+ /// <param name="cookieToken">A valid cookie token.</param>
+ /// <returns>An <see cref="AntiforgeryToken"/>.</returns>
+ AntiforgeryToken GenerateRequestToken(HttpContext httpContext, AntiforgeryToken cookieToken);
+
+ /// <summary>
+ /// Attempts to validate a cookie token.
+ /// </summary>
+ /// <param name="cookieToken">A valid cookie token.</param>
+ /// <returns><c>true</c> if the cookie token is valid, otherwise <c>false</c>.</returns>
+ bool IsCookieTokenValid(AntiforgeryToken cookieToken);
+
+ /// <summary>
+ /// Attempts to validate a cookie and request token set for the given <paramref name="httpContext"/>.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
+ /// <param name="cookieToken">A cookie token.</param>
+ /// <param name="requestToken">A request token.</param>
+ /// <param name="message">
+ /// Will be set to the validation message if the tokens are invalid, otherwise <c>null</c>.
+ /// </param>
+ /// <returns><c>true</c> if the tokens are valid, otherwise <c>false</c>.</returns>
+ 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);
+
+ /// <summary>
+ /// Gets the cookie and request tokens from the request.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContext"/> for the current request.</param>
+ /// <returns>The <see cref="AntiforgeryTokenSet"/>.</returns>
+ Task<AntiforgeryTokenSet> 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
+{
+ /// <summary>
+ /// This interface can extract unique identifers for a <see cref="ClaimsPrincipal"/>.
+ /// </summary>
+ public interface IClaimUidExtractor
+ {
+ /// <summary>
+ /// Extracts claims identifier.
+ /// </summary>
+ /// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/>.</param>
+ /// <returns>The claims identifier.</returns>
+ 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 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>An antiforgery system for ASP.NET Core designed to generate and validate tokens to prevent Cross-Site Request Forgery attacks.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;antiforgery</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="$(MicrosoftAspNetCoreDataProtectionPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="$(MicrosoftAspNetCoreHttpAbstractionsPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(MicrosoftAspNetCoreHttpExtensionsPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="$(MicrosoftAspNetCoreWebUtilitiesPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="$(MicrosoftExtensionsObjectPoolPackageVersion)" />
+ </ItemGroup>
+
+</Project>
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 @@
+// <auto-generated />
+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);
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ internal static string AntiforgeryTokenValidator_AuthenticatedUserWithoutUsername
+ {
+ get => GetString("AntiforgeryTokenValidator_AuthenticatedUserWithoutUsername");
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ 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);
+
+ /// <summary>
+ /// The provided antiforgery token failed a custom data check.
+ /// </summary>
+ internal static string AntiforgeryToken_AdditionalDataCheckFailed
+ {
+ get => GetString("AntiforgeryToken_AdditionalDataCheckFailed");
+ }
+
+ /// <summary>
+ /// The provided antiforgery token failed a custom data check.
+ /// </summary>
+ internal static string FormatAntiforgeryToken_AdditionalDataCheckFailed()
+ => GetString("AntiforgeryToken_AdditionalDataCheckFailed");
+
+ /// <summary>
+ /// The provided antiforgery token was meant for a different claims-based user than the current user.
+ /// </summary>
+ internal static string AntiforgeryToken_ClaimUidMismatch
+ {
+ get => GetString("AntiforgeryToken_ClaimUidMismatch");
+ }
+
+ /// <summary>
+ /// The provided antiforgery token was meant for a different claims-based user than the current user.
+ /// </summary>
+ internal static string FormatAntiforgeryToken_ClaimUidMismatch()
+ => GetString("AntiforgeryToken_ClaimUidMismatch");
+
+ /// <summary>
+ /// The antiforgery token could not be decrypted.
+ /// </summary>
+ internal static string AntiforgeryToken_DeserializationFailed
+ {
+ get => GetString("AntiforgeryToken_DeserializationFailed");
+ }
+
+ /// <summary>
+ /// The antiforgery token could not be decrypted.
+ /// </summary>
+ internal static string FormatAntiforgeryToken_DeserializationFailed()
+ => GetString("AntiforgeryToken_DeserializationFailed");
+
+ /// <summary>
+ /// The antiforgery cookie token and request token do not match.
+ /// </summary>
+ internal static string AntiforgeryToken_SecurityTokenMismatch
+ {
+ get => GetString("AntiforgeryToken_SecurityTokenMismatch");
+ }
+
+ /// <summary>
+ /// The antiforgery cookie token and request token do not match.
+ /// </summary>
+ internal static string FormatAntiforgeryToken_SecurityTokenMismatch()
+ => GetString("AntiforgeryToken_SecurityTokenMismatch");
+
+ /// <summary>
+ /// Validation of the provided antiforgery token failed. The cookie token and the request token were swapped.
+ /// </summary>
+ internal static string AntiforgeryToken_TokensSwapped
+ {
+ get => GetString("AntiforgeryToken_TokensSwapped");
+ }
+
+ /// <summary>
+ /// Validation of the provided antiforgery token failed. The cookie token and the request token were swapped.
+ /// </summary>
+ internal static string FormatAntiforgeryToken_TokensSwapped()
+ => GetString("AntiforgeryToken_TokensSwapped");
+
+ /// <summary>
+ /// The provided antiforgery token was meant for user "{0}", but the current user is "{1}".
+ /// </summary>
+ internal static string AntiforgeryToken_UsernameMismatch
+ {
+ get => GetString("AntiforgeryToken_UsernameMismatch");
+ }
+
+ /// <summary>
+ /// The provided antiforgery token was meant for user "{0}", but the current user is "{1}".
+ /// </summary>
+ internal static string FormatAntiforgeryToken_UsernameMismatch(object p0, object p1)
+ => string.Format(CultureInfo.CurrentCulture, GetString("AntiforgeryToken_UsernameMismatch"), p0, p1);
+
+ /// <summary>
+ /// The antiforgery cookie token is invalid.
+ /// </summary>
+ internal static string Antiforgery_CookieToken_IsInvalid
+ {
+ get => GetString("Antiforgery_CookieToken_IsInvalid");
+ }
+
+ /// <summary>
+ /// The antiforgery cookie token is invalid.
+ /// </summary>
+ internal static string FormatAntiforgery_CookieToken_IsInvalid()
+ => GetString("Antiforgery_CookieToken_IsInvalid");
+
+ /// <summary>
+ /// The required antiforgery cookie "{0}" is not present.
+ /// </summary>
+ internal static string Antiforgery_CookieToken_MustBeProvided
+ {
+ get => GetString("Antiforgery_CookieToken_MustBeProvided");
+ }
+
+ /// <summary>
+ /// The required antiforgery cookie "{0}" is not present.
+ /// </summary>
+ internal static string FormatAntiforgery_CookieToken_MustBeProvided(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Antiforgery_CookieToken_MustBeProvided"), p0);
+
+ /// <summary>
+ /// The required antiforgery cookie token must be provided.
+ /// </summary>
+ internal static string Antiforgery_CookieToken_MustBeProvided_Generic
+ {
+ get => GetString("Antiforgery_CookieToken_MustBeProvided_Generic");
+ }
+
+ /// <summary>
+ /// The required antiforgery cookie token must be provided.
+ /// </summary>
+ internal static string FormatAntiforgery_CookieToken_MustBeProvided_Generic()
+ => GetString("Antiforgery_CookieToken_MustBeProvided_Generic");
+
+ /// <summary>
+ /// The required antiforgery form field "{0}" is not present.
+ /// </summary>
+ internal static string Antiforgery_FormToken_MustBeProvided
+ {
+ get => GetString("Antiforgery_FormToken_MustBeProvided");
+ }
+
+ /// <summary>
+ /// The required antiforgery form field "{0}" is not present.
+ /// </summary>
+ internal static string FormatAntiforgery_FormToken_MustBeProvided(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Antiforgery_FormToken_MustBeProvided"), p0);
+
+ /// <summary>
+ /// The required antiforgery header value "{0}" is not present.
+ /// </summary>
+ internal static string Antiforgery_HeaderToken_MustBeProvided
+ {
+ get => GetString("Antiforgery_HeaderToken_MustBeProvided");
+ }
+
+ /// <summary>
+ /// The required antiforgery header value "{0}" is not present.
+ /// </summary>
+ internal static string FormatAntiforgery_HeaderToken_MustBeProvided(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Antiforgery_HeaderToken_MustBeProvided"), p0);
+
+ /// <summary>
+ /// The required antiforgery request token was not provided in either form field "{0}" or header value "{1}".
+ /// </summary>
+ internal static string Antiforgery_RequestToken_MustBeProvided
+ {
+ get => GetString("Antiforgery_RequestToken_MustBeProvided");
+ }
+
+ /// <summary>
+ /// The required antiforgery request token was not provided in either form field "{0}" or header value "{1}".
+ /// </summary>
+ internal static string FormatAntiforgery_RequestToken_MustBeProvided(object p0, object p1)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Antiforgery_RequestToken_MustBeProvided"), p0, p1);
+
+ /// <summary>
+ /// The required antiforgery request token must be provided.
+ /// </summary>
+ internal static string Antiforgery_RequestToken_MustBeProvided_Generic
+ {
+ get => GetString("Antiforgery_RequestToken_MustBeProvided_Generic");
+ }
+
+ /// <summary>
+ /// The required antiforgery request token must be provided.
+ /// </summary>
+ internal static string FormatAntiforgery_RequestToken_MustBeProvided_Generic()
+ => GetString("Antiforgery_RequestToken_MustBeProvided_Generic");
+
+ /// <summary>
+ /// The antiforgery system has the configuration value {optionName} = {value}, but the current request is not an SSL request.
+ /// </summary>
+ internal static string Antiforgery_RequiresSSL
+ {
+ get => GetString("Antiforgery_RequiresSSL");
+ }
+
+ /// <summary>
+ /// The antiforgery system has the configuration value {optionName} = {value}, but the current request is not an SSL request.
+ /// </summary>
+ internal static string FormatAntiforgery_RequiresSSL(object optionName, object value)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Antiforgery_RequiresSSL", "optionName", "value"), optionName, value);
+
+ /// <summary>
+ /// Value cannot be null or empty.
+ /// </summary>
+ internal static string ArgumentCannotBeNullOrEmpty
+ {
+ get => GetString("ArgumentCannotBeNullOrEmpty");
+ }
+
+ /// <summary>
+ /// Value cannot be null or empty.
+ /// </summary>
+ 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="AntiforgeryTokenValidator_AuthenticatedUserWithoutUsername" xml:space="preserve">
+ <value>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.</value>
+ <comment>0 = typeof(identity), 1 = nameof(IsAuthenticated), 2 = bool.TrueString, 3 = nameof(Name), 4 = nameof(IAdditionalDataProvider), 5 = nameof(DefaultAdditionalDataProvider)</comment>
+ </data>
+ <data name="AntiforgeryToken_AdditionalDataCheckFailed" xml:space="preserve">
+ <value>The provided antiforgery token failed a custom data check.</value>
+ </data>
+ <data name="AntiforgeryToken_ClaimUidMismatch" xml:space="preserve">
+ <value>The provided antiforgery token was meant for a different claims-based user than the current user.</value>
+ </data>
+ <data name="AntiforgeryToken_DeserializationFailed" xml:space="preserve">
+ <value>The antiforgery token could not be decrypted.</value>
+ </data>
+ <data name="AntiforgeryToken_SecurityTokenMismatch" xml:space="preserve">
+ <value>The antiforgery cookie token and request token do not match.</value>
+ </data>
+ <data name="AntiforgeryToken_TokensSwapped" xml:space="preserve">
+ <value>Validation of the provided antiforgery token failed. The cookie token and the request token were swapped.</value>
+ </data>
+ <data name="AntiforgeryToken_UsernameMismatch" xml:space="preserve">
+ <value>The provided antiforgery token was meant for user "{0}", but the current user is "{1}".</value>
+ </data>
+ <data name="Antiforgery_CookieToken_IsInvalid" xml:space="preserve">
+ <value>The antiforgery cookie token is invalid.</value>
+ </data>
+ <data name="Antiforgery_CookieToken_MustBeProvided" xml:space="preserve">
+ <value>The required antiforgery cookie "{0}" is not present.</value>
+ </data>
+ <data name="Antiforgery_CookieToken_MustBeProvided_Generic" xml:space="preserve">
+ <value>The required antiforgery cookie token must be provided.</value>
+ </data>
+ <data name="Antiforgery_FormToken_MustBeProvided" xml:space="preserve">
+ <value>The required antiforgery form field "{0}" is not present.</value>
+ </data>
+ <data name="Antiforgery_HeaderToken_MustBeProvided" xml:space="preserve">
+ <value>The required antiforgery header value "{0}" is not present.</value>
+ </data>
+ <data name="Antiforgery_RequestToken_MustBeProvided" xml:space="preserve">
+ <value>The required antiforgery request token was not provided in either form field "{0}" or header value "{1}".</value>
+ </data>
+ <data name="Antiforgery_RequestToken_MustBeProvided_Generic" xml:space="preserve">
+ <value>The required antiforgery request token must be provided.</value>
+ </data>
+ <data name="Antiforgery_RequiresSSL" xml:space="preserve">
+ <value>The antiforgery system has the configuration value {optionName} = {value}, but the current request is not an SSL request.</value>
+ </data>
+ <data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
+ <value>Value cannot be null or empty.</value>
+ </data>
+</root> \ 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<Microsoft.AspNetCore.Antiforgery.AntiforgeryOptions>"
+ }
+ ],
+ "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<Microsoft.AspNetCore.Http.PathString>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookiePath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Nullable<Microsoft.AspNetCore.Http.PathString>"
+ }
+ ],
+ "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<System.Boolean>",
+ "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 @@
+<Project>
+ <Import Project="..\Directory.Build.props" />
+
+ <PropertyGroup>
+ <DeveloperBuildTestTfms>netcoreapp2.1</DeveloperBuildTestTfms>
+ <StandardTestTfms>$(DeveloperBuildTestTfms)</StandardTestTfms>
+ <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' ">netcoreapp2.1;netcoreapp2.0</StandardTestTfms>
+ <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' AND '$(OS)' == 'Windows_NT' ">$(StandardTestTfms);net461</StandardTestTfms>
+ </PropertyGroup>
+</Project> \ 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<IOptions<AntiforgeryOptions>>();
+
+ // 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<AntiforgeryOptions>(o =>
+ {
+ Assert.Null(o.Cookie.Name);
+ o.Cookie.Name = "antiforgery";
+ });
+ serviceCollection.AddAntiforgery();
+ serviceCollection
+ .AddDataProtection()
+ .SetApplicationName("HelloWorldApp");
+
+ var services = serviceCollection.BuildServiceProvider();
+ var options = services.GetRequiredService<IOptions<AntiforgeryOptions>>();
+
+ // 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<ArgumentOutOfRangeException>(() => 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<ArgumentOutOfRangeException>(() => 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<InvalidOperationException>(
+ () => 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<InvalidOperationException>(
+ () => 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<InvalidOperationException>(
+ () => 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<InvalidOperationException>(
+ () => 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<InvalidOperationException>(
+ () => 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<string>()),
+ Times.Never);
+ context.TokenSerializer.Verify(
+ o => o.Serialize(It.IsAny<AntiforgeryToken>()),
+ 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<HttpContext>(), It.IsAny<string>()),
+ 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<HttpContext>(), It.IsAny<string>()),
+ 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<HttpContext>(), It.IsAny<string>()),
+ 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<HttpContext>(), 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<HttpContext>(), 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<string>()),
+ Times.Never);
+ context.TokenSerializer.Verify(
+ o => o.Serialize(It.IsAny<AntiforgeryToken>()),
+ 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<HttpContext>(), It.IsAny<string>()),
+ Times.Never);
+
+ // Token serializer not used.
+ context.TokenSerializer.Verify(
+ o => o.Deserialize(It.IsAny<string>()),
+ Times.Never);
+ context.TokenSerializer.Verify(
+ o => o.Serialize(It.IsAny<AntiforgeryToken>()),
+ 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<string>()),
+ Times.Never);
+ context.TokenSerializer.Verify(
+ o => o.Serialize(It.IsAny<AntiforgeryToken>()),
+ 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<AntiforgeryToken>(),
+ It.IsAny<AntiforgeryToken>(),
+ 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<AntiforgeryToken>(),
+ It.IsAny<AntiforgeryToken>(),
+ 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<AntiforgeryToken>(),
+ It.IsAny<AntiforgeryToken>(),
+ 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<AntiforgeryValidationException>(
+ () => 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<AntiforgeryValidationException>(
+ () => 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<AntiforgeryValidationException>(
+ () => 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<AntiforgeryValidationException>(
+ () => 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<AntiforgeryValidationException>(
+ () => 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<string>()),
+ Times.Never);
+ context.TokenSerializer.Verify(
+ o => o.Serialize(It.IsAny<AntiforgeryToken>()),
+ 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<string>()),
+ Times.Never);
+ context.TokenSerializer.Verify(
+ o => o.Serialize(It.IsAny<AntiforgeryToken>()),
+ 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<string>()),
+ Times.Never);
+ context.TokenSerializer.Verify(
+ o => o.Serialize(It.IsAny<AntiforgeryToken>()),
+ Times.Never);
+
+ // Token store not used.
+ context.TokenStore.Verify(
+ t => t.SaveCookieToken(It.IsAny<HttpContext>(), It.IsAny<string>()),
+ 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<IHttpResponseFeature>(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<ILoggerFactory>();
+ 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<ILoggerFactory>();
+ 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<ILoggerFactory>();
+ 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<ILoggerFactory>();
+ return new DefaultAntiforgery(
+ antiforgeryOptionsAccessor: optionsManager,
+ tokenGenerator: tokenGenerator,
+ tokenSerializer: tokenSerializer,
+ tokenStore: tokenStore,
+ loggerFactory: loggerFactory);
+ }
+
+ private IServiceProvider GetServices()
+ {
+ var builder = new ServiceCollection();
+ builder.AddSingleton<ILoggerFactory>(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<IAntiforgeryTokenStore> GetTokenStore(
+ HttpContext context,
+ TestTokenSet testTokenSet,
+ bool saveNewCookie = true)
+ {
+ var oldCookieToken = testTokenSet.OldCookieTokenString;
+ var formToken = testTokenSet.FormTokenString;
+ var mockTokenStore = new Mock<IAntiforgeryTokenStore>(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<IAntiforgeryTokenSerializer> GetTokenSerializer(TestTokenSet testTokenSet)
+ {
+ var oldCookieToken = testTokenSet.OldCookieToken;
+ var newCookieToken = testTokenSet.NewCookieToken;
+ var formToken = testTokenSet.RequestToken;
+ var mockSerializer = new Mock<IAntiforgeryTokenSerializer>(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<IAntiforgeryTokenGenerator>(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<IAntiforgeryTokenGenerator> TokenGenerator { get; set; }
+
+ public Mock<IAntiforgeryTokenStore> TokenStore { get; set; }
+
+ public Mock<IAntiforgeryTokenSerializer> TokenSerializer { get; set; }
+ }
+
+ private class TestOptionsManager : IOptions<AntiforgeryOptions>
+ {
+ 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<IClaimUidExtractor>().Object;
+
+ var tokenProvider = new DefaultAntiforgeryTokenGenerator(
+ claimUidExtractor: claimUidExtractor,
+ additionalDataProvider: null);
+
+ // Act & assert
+ var exception = Assert.Throws<InvalidOperationException>(
+ () => 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<IAntiforgeryAdditionalDataProvider>();
+ mockAdditionalDataProvider.Setup(o => o.GetAdditionalData(httpContext))
+ .Returns("additional-data");
+
+ var claimUidExtractor = new Mock<IClaimUidExtractor>().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<IClaimUidExtractor>();
+ mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is<ClaimsPrincipal>(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<ClaimsIdentity>();
+ 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<IClaimUidExtractor>().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<ArgumentNullException>(
+ () => 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<ArgumentNullException>(
+ () => 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<IClaimUidExtractor>();
+ mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is<ClaimsPrincipal>(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<IClaimUidExtractor>();
+ mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is<ClaimsPrincipal>(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<IAntiforgeryAdditionalDataProvider>();
+ 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<IAntiforgeryAdditionalDataProvider>();
+ 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<IAntiforgeryAdditionalDataProvider>();
+ mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data"))
+ .Returns(true);
+
+ var tokenProvider = new DefaultAntiforgeryTokenGenerator(
+ claimUidExtractor: new Mock<IClaimUidExtractor>().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<IClaimUidExtractor>();
+ mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(It.Is<ClaimsPrincipal>(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<IDataProtectionProvider> _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<AntiforgerySerializationContext> _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<AntiforgeryValidationException>(() => 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<IDataProtectionProvider> GetDataProtector()
+ {
+ var mockCryptoSystem = new Mock<IDataProtector>();
+ mockCryptoSystem.Setup(o => o.Protect(It.IsAny<byte[]>()))
+ .Returns<byte[]>(Protect)
+ .Verifiable();
+ mockCryptoSystem.Setup(o => o.Unprotect(It.IsAny<byte[]>()))
+ .Returns<byte[]>(UnProtect)
+ .Verifiable();
+
+ var provider = new Mock<IDataProtectionProvider>();
+ provider
+ .Setup(p => p.CreateProtector(It.IsAny<string>()))
+ .Returns(mockCryptoSystem.Object);
+ return provider;
+ }
+
+ private static byte[] Protect(byte[] data)
+ {
+ var input = new List<byte>(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<string, StringValues>
+ {
+ { "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<string, StringValues>
+ {
+ { "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>();
+ 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>();
+ 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>();
+ 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>();
+ 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<string, string>
+ {
+ { 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<AntiforgerySerializationContext> _pool =
+ new DefaultObjectPoolProvider().Create(new AntiforgerySerializationContextPooledObjectPolicy());
+
+ [Fact]
+ public void ExtractClaimUid_Unauthenticated()
+ {
+ // Arrange
+ var extractor = new DefaultClaimUidExtractor(_pool);
+
+ var mockIdentity = new Mock<ClaimsIdentity>();
+ 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<ClaimsIdentity>();
+ 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<string>
+ {
+ 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 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Antiforgery\Microsoft.AspNetCore.Antiforgery.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.WebEncoders" Version="$(MicrosoftExtensionsWebEncodersPackageVersion)" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
+ <PackageReference Include="Moq" Version="$(MoqPackageVersion)" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
+ <PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
+ </ItemGroup>
+
+</Project>
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<AntiforgeryOptions>
+ {
+ 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 @@
+<Project>
+ <PropertyGroup>
+ <VersionPrefix>2.1.1</VersionPrefix>
+ <VersionSuffix>rtm</VersionSuffix>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' == 'rtm' ">$(VersionPrefix)</PackageVersion>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' != 'rtm' ">$(VersionPrefix)-$(VersionSuffix)-final</PackageVersion>
+ <BuildNumber Condition="'$(BuildNumber)' == ''">t000</BuildNumber>
+ <FeatureBranchVersionPrefix Condition="'$(FeatureBranchVersionPrefix)' == ''">a-</FeatureBranchVersionPrefix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
+ </PropertyGroup>
+</Project>
diff --git a/src/AuthSamples/Directory.Build.props b/src/AuthSamples/Directory.Build.props
index 407b45e839..26d1c16e45 100644
--- a/src/AuthSamples/Directory.Build.props
+++ b/src/AuthSamples/Directory.Build.props
@@ -5,7 +5,7 @@
<PropertyGroup>
<Product>Microsoft ASP.NET Core</Product>
- <RepositoryUrl>https://github.com/aspnet/AuthSamples</RepositoryUrl>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
</PropertyGroup>
diff --git a/src/AzureIntegration/Directory.Build.props b/src/AzureIntegration/Directory.Build.props
index 9752fc6253..cf33763ee4 100644
--- a/src/AzureIntegration/Directory.Build.props
+++ b/src/AzureIntegration/Directory.Build.props
@@ -9,7 +9,7 @@
<PropertyGroup>
<Product>Microsoft ASP.NET Core</Product>
- <RepositoryUrl>https://github.com/aspnet/AzureIntegration</RepositoryUrl>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)..\..\eng\AspNetCore.snk</AssemblyOriginatorKeyFile>
diff --git a/src/CORS/.gitignore b/src/CORS/.gitignore
new file mode 100644
index 0000000000..a435689c0a
--- /dev/null
+++ b/src/CORS/.gitignore
@@ -0,0 +1,41 @@
+[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/
+.vscode
+global.json
diff --git a/src/CORS/CORS.sln b/src/CORS/CORS.sln
new file mode 100644
index 0000000000..afdbf1fdc0
--- /dev/null
+++ b/src/CORS/CORS.sln
@@ -0,0 +1,89 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26817.0
+MinimumVisualStudioVersion = 15.0.26730.03
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{84FE6872-A610-4CEC-855F-A84CBF1F40FC}"
+ ProjectSection(SolutionItems) = preProject
+ src\Directory.Build.props = src\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F32074C7-087C-46CC-A913-422BFD2D6E0A}"
+ ProjectSection(SolutionItems) = preProject
+ test\Directory.Build.props = test\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Cors", "src\Microsoft.AspNetCore.Cors\Microsoft.AspNetCore.Cors.csproj", "{41349FCD-D1C4-47A6-82D0-D16D00A8D59D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Cors.Test", "test\Microsoft.AspNetCore.Cors.Test\Microsoft.AspNetCore.Cors.Test.csproj", "{F05BE96F-F869-4408-A480-96935B4835EE}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebSites", "WebSites", "{538380BF-0D4C-4E30-8F41-E75C4B1C01FA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CorsMiddlewareWebSite", "test\WebSites\CorsMiddlewareWebSite\CorsMiddlewareWebSite.csproj", "{B42D4844-FFF8-4EC2-88D1-3AE95234D9EB}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{960E0703-A8A5-44DF-AA87-B7C614683B3C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleDestination", "samples\SampleDestination\SampleDestination.csproj", "{F6675DC1-AA21-453B-89B6-DA425FB9C3A5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleOrigin", "samples\SampleOrigin\SampleOrigin.csproj", "{99460370-AE5D-4DC9-8DBF-04DF66D6B21D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0CCC5C1B-F548-4A17-96F8-14C700093FA0}"
+ ProjectSection(SolutionItems) = preProject
+ .appveyor.yml = .appveyor.yml
+ .gitattributes = .gitattributes
+ .gitignore = .gitignore
+ .travis.yml = .travis.yml
+ build.cmd = build.cmd
+ build.ps1 = build.ps1
+ build.sh = build.sh
+ CONTRIBUTING.md = CONTRIBUTING.md
+ Directory.Build.props = Directory.Build.props
+ Directory.Build.targets = Directory.Build.targets
+ LICENSE.txt = LICENSE.txt
+ NuGet.config = NuGet.config
+ NuGetPackageVerifier.json = NuGetPackageVerifier.json
+ README.md = README.md
+ version.xml = version.xml
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {41349FCD-D1C4-47A6-82D0-D16D00A8D59D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {41349FCD-D1C4-47A6-82D0-D16D00A8D59D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {41349FCD-D1C4-47A6-82D0-D16D00A8D59D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {41349FCD-D1C4-47A6-82D0-D16D00A8D59D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F05BE96F-F869-4408-A480-96935B4835EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F05BE96F-F869-4408-A480-96935B4835EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F05BE96F-F869-4408-A480-96935B4835EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F05BE96F-F869-4408-A480-96935B4835EE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B42D4844-FFF8-4EC2-88D1-3AE95234D9EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B42D4844-FFF8-4EC2-88D1-3AE95234D9EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B42D4844-FFF8-4EC2-88D1-3AE95234D9EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B42D4844-FFF8-4EC2-88D1-3AE95234D9EB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F6675DC1-AA21-453B-89B6-DA425FB9C3A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F6675DC1-AA21-453B-89B6-DA425FB9C3A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F6675DC1-AA21-453B-89B6-DA425FB9C3A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F6675DC1-AA21-453B-89B6-DA425FB9C3A5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {99460370-AE5D-4DC9-8DBF-04DF66D6B21D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {99460370-AE5D-4DC9-8DBF-04DF66D6B21D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {99460370-AE5D-4DC9-8DBF-04DF66D6B21D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {99460370-AE5D-4DC9-8DBF-04DF66D6B21D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {41349FCD-D1C4-47A6-82D0-D16D00A8D59D} = {84FE6872-A610-4CEC-855F-A84CBF1F40FC}
+ {F05BE96F-F869-4408-A480-96935B4835EE} = {F32074C7-087C-46CC-A913-422BFD2D6E0A}
+ {538380BF-0D4C-4E30-8F41-E75C4B1C01FA} = {F32074C7-087C-46CC-A913-422BFD2D6E0A}
+ {B42D4844-FFF8-4EC2-88D1-3AE95234D9EB} = {538380BF-0D4C-4E30-8F41-E75C4B1C01FA}
+ {F6675DC1-AA21-453B-89B6-DA425FB9C3A5} = {960E0703-A8A5-44DF-AA87-B7C614683B3C}
+ {99460370-AE5D-4DC9-8DBF-04DF66D6B21D} = {960E0703-A8A5-44DF-AA87-B7C614683B3C}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {F9ED9C53-44CD-4853-9621-D028B7B6A431}
+ EndGlobalSection
+EndGlobal
diff --git a/src/CORS/Directory.Build.props b/src/CORS/Directory.Build.props
new file mode 100644
index 0000000000..2883c9cc0a
--- /dev/null
+++ b/src/CORS/Directory.Build.props
@@ -0,0 +1,21 @@
+<Project>
+ <Import
+ Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))\AspNetCoreSettings.props"
+ Condition=" '$(CI)' != 'true' AND '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))' != '' " />
+
+ <Import Project="version.props" />
+ <Import Project="build\dependencies.props" />
+ <Import Project="build\sources.props" />
+
+ <PropertyGroup>
+ <Product>Microsoft ASP.NET Core</Product>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
+ <RepositoryType>git</RepositoryType>
+ <RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
+ <AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)build\Key.snk</AssemblyOriginatorKeyFile>
+ <SignAssembly>true</SignAssembly>
+ <PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+
+</Project>
diff --git a/src/CORS/Directory.Build.targets b/src/CORS/Directory.Build.targets
new file mode 100644
index 0000000000..53b3f6e1da
--- /dev/null
+++ b/src/CORS/Directory.Build.targets
@@ -0,0 +1,7 @@
+<Project>
+ <PropertyGroup>
+ <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">$(MicrosoftNETCoreApp20PackageVersion)</RuntimeFrameworkVersion>
+ <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">$(MicrosoftNETCoreApp21PackageVersion)</RuntimeFrameworkVersion>
+ <NETStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard2.0' ">$(NETStandardLibrary20PackageVersion)</NETStandardImplicitPackageVersion>
+ </PropertyGroup>
+</Project>
diff --git a/src/CORS/NuGetPackageVerifier.json b/src/CORS/NuGetPackageVerifier.json
new file mode 100644
index 0000000000..b153ab1515
--- /dev/null
+++ b/src/CORS/NuGetPackageVerifier.json
@@ -0,0 +1,7 @@
+{
+ "Default": {
+ "rules": [
+ "DefaultCompositeRule"
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/CORS/README.md b/src/CORS/README.md
new file mode 100644
index 0000000000..795d311da9
--- /dev/null
+++ b/src/CORS/README.md
@@ -0,0 +1,9 @@
+CORS
+===
+AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/yi0m8evjtg107o12/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/CORS/branch/dev)
+
+Travis: [![Travis](https://travis-ci.org/aspnet/CORS.svg?branch=dev)](https://travis-ci.org/aspnet/CORS)
+
+CORS repository includes the core implementation for CORS policy, utilized by the CORS middleware and MVC.
+
+This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
diff --git a/src/CORS/build/Key.snk b/src/CORS/build/Key.snk
new file mode 100644
index 0000000000..e10e4889c1
--- /dev/null
+++ b/src/CORS/build/Key.snk
Binary files differ
diff --git a/src/CORS/build/buildpipeline/linux.groovy b/src/CORS/build/buildpipeline/linux.groovy
new file mode 100644
index 0000000000..903f218bb8
--- /dev/null
+++ b/src/CORS/build/buildpipeline/linux.groovy
@@ -0,0 +1,10 @@
+@Library('dotnet-ci') _
+
+simpleNode('Ubuntu16.04', 'latest-or-auto-docker') {
+ stage ('Checking out source') {
+ checkout scm
+ }
+ stage ('Build') {
+ sh './build.sh --ci'
+ }
+}
diff --git a/src/CORS/build/buildpipeline/osx.groovy b/src/CORS/build/buildpipeline/osx.groovy
new file mode 100644
index 0000000000..aaac63686b
--- /dev/null
+++ b/src/CORS/build/buildpipeline/osx.groovy
@@ -0,0 +1,10 @@
+@Library('dotnet-ci') _
+
+simpleNode('OSX10.12','latest') {
+ stage ('Checking out source') {
+ checkout scm
+ }
+ stage ('Build') {
+ sh './build.sh --ci'
+ }
+}
diff --git a/src/CORS/build/buildpipeline/pipeline.groovy b/src/CORS/build/buildpipeline/pipeline.groovy
new file mode 100644
index 0000000000..e915cadae1
--- /dev/null
+++ b/src/CORS/build/buildpipeline/pipeline.groovy
@@ -0,0 +1,18 @@
+import org.dotnet.ci.pipelines.Pipeline
+
+def windowsPipeline = Pipeline.createPipeline(this, 'build/buildpipeline/windows.groovy')
+def linuxPipeline = Pipeline.createPipeline(this, 'build/buildpipeline/linux.groovy')
+def osxPipeline = Pipeline.createPipeline(this, 'build/buildpipeline/osx.groovy')
+String configuration = 'Release'
+def parameters = [
+ 'Configuration': configuration
+]
+
+windowsPipeline.triggerPipelineOnEveryGithubPR("Windows ${configuration} x64 Build", parameters)
+windowsPipeline.triggerPipelineOnGithubPush(parameters)
+
+linuxPipeline.triggerPipelineOnEveryGithubPR("Ubuntu 16.04 ${configuration} Build", parameters)
+linuxPipeline.triggerPipelineOnGithubPush(parameters)
+
+osxPipeline.triggerPipelineOnEveryGithubPR("OSX 10.12 ${configuration} Build", parameters)
+osxPipeline.triggerPipelineOnGithubPush(parameters)
diff --git a/src/CORS/build/buildpipeline/windows.groovy b/src/CORS/build/buildpipeline/windows.groovy
new file mode 100644
index 0000000000..8d26f313d4
--- /dev/null
+++ b/src/CORS/build/buildpipeline/windows.groovy
@@ -0,0 +1,12 @@
+@Library('dotnet-ci') _
+
+// 'node' indicates to Jenkins that the enclosed block runs on a node that matches
+// the label 'windows-with-vs'
+simpleNode('Windows_NT','latest') {
+ stage ('Checking out source') {
+ checkout scm
+ }
+ stage ('Build') {
+ bat '.\\run.cmd -CI default-build'
+ }
+}
diff --git a/src/CORS/build/dependencies.props b/src/CORS/build/dependencies.props
new file mode 100644
index 0000000000..36fe73426a
--- /dev/null
+++ b/src/CORS/build/dependencies.props
@@ -0,0 +1,35 @@
+<Project>
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ </PropertyGroup>
+
+ <!-- These package versions may be overridden or updated by automation. -->
+ <PropertyGroup Label="Package Versions: Auto">
+ <InternalAspNetCoreSdkPackageVersion>2.1.3-rtm-15802</InternalAspNetCoreSdkPackageVersion>
+ <MicrosoftNETCoreApp20PackageVersion>2.0.0</MicrosoftNETCoreApp20PackageVersion>
+ <MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion>
+ <MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
+ <MoqPackageVersion>4.7.49</MoqPackageVersion>
+ <NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
+ <XunitAnalyzersPackageVersion>0.8.0</XunitAnalyzersPackageVersion>
+ <XunitPackageVersion>2.3.1</XunitPackageVersion>
+ <XunitRunnerVisualStudioPackageVersion>2.4.0-beta.1.build3945</XunitRunnerVisualStudioPackageVersion>
+ </PropertyGroup>
+
+ <!-- This may import a generated file which may override the variables above. -->
+ <Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
+
+ <!-- These are package versions that should not be overridden or updated by automation. -->
+ <PropertyGroup Label="Package Versions: Pinned">
+ <MicrosoftAspNetCoreHttpExtensionsPackageVersion>2.1.1</MicrosoftAspNetCoreHttpExtensionsPackageVersion>
+ <MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.1.1</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
+ <MicrosoftAspNetCoreServerKestrelPackageVersion>2.1.2</MicrosoftAspNetCoreServerKestrelPackageVersion>
+ <MicrosoftAspNetCoreTestHostPackageVersion>2.1.1</MicrosoftAspNetCoreTestHostPackageVersion>
+ <MicrosoftExtensionsConfigurationAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsConfigurationAbstractionsPackageVersion>
+ <MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>
+ <MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
+ <MicrosoftExtensionsLoggingConsolePackageVersion>2.1.1</MicrosoftExtensionsLoggingConsolePackageVersion>
+ <MicrosoftExtensionsLoggingTestingPackageVersion>2.1.1</MicrosoftExtensionsLoggingTestingPackageVersion>
+ <MicrosoftExtensionsOptionsPackageVersion>2.1.1</MicrosoftExtensionsOptionsPackageVersion>
+ </PropertyGroup>
+</Project> \ No newline at end of file
diff --git a/src/CORS/build/repo.props b/src/CORS/build/repo.props
new file mode 100644
index 0000000000..dab1601c88
--- /dev/null
+++ b/src/CORS/build/repo.props
@@ -0,0 +1,15 @@
+<Project>
+ <Import Project="dependencies.props" />
+
+ <PropertyGroup>
+ <!-- These properties are use by the automation that updates dependencies.props -->
+ <LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
+ <LineupPackageVersion>2.1.0-rc1-*</LineupPackageVersion>
+ <LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp20PackageVersion)" />
+ <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/CORS/build/sources.props b/src/CORS/build/sources.props
new file mode 100644
index 0000000000..9215df9751
--- /dev/null
+++ b/src/CORS/build/sources.props
@@ -0,0 +1,17 @@
+<Project>
+ <Import Project="$(DotNetRestoreSourcePropsPath)" Condition="'$(DotNetRestoreSourcePropsPath)' != ''"/>
+
+ <PropertyGroup Label="RestoreSources">
+ <RestoreSources>$(DotNetRestoreSources)</RestoreSources>
+ <RestoreSources Condition="'$(DotNetBuildOffline)' != 'true' AND '$(AspNetUniverseBuildOffline)' != 'true' ">
+ $(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>
+ <RestoreSources Condition="'$(DotNetBuildOffline)' != 'true'">
+ $(RestoreSources);
+ https://api.nuget.org/v3/index.json;
+ </RestoreSources>
+ </PropertyGroup>
+</Project>
diff --git a/src/CORS/samples/README.md b/src/CORS/samples/README.md
new file mode 100644
index 0000000000..0224188d71
--- /dev/null
+++ b/src/CORS/samples/README.md
@@ -0,0 +1,38 @@
+# CORS Sample
+
+This sample consists of a request origin (SampleOrigin) and a request destination (SampleDestination). Both have different domain names, to simulate a CORS request.
+
+## Modify Hosts File
+To run this CORS sample, modify the hosts file to register the hostnames `destination.example.com` and `origin.example.com`.
+### Windows:
+Run a text editor (e.g. Notepad) as an Administrator. Open the hosts file on the path: "C:\Windows\System32\drivers\etc\hosts".
+
+### Linux:
+On a Terminal window, type "sudo nano /etc/hosts" and enter your admin password when prompted.
+
+In the hosts file, add the following to the bottom of the file:
+
+```
+127.0.0.1 destination.example.com
+127.0.0.1 origin.example.com
+```
+
+Save the file and close it. Then clear your browser history.
+
+## Run the sample
+The SampleOrigin application will use port 5001, and SampleDestination will use 5000. Please ensure there are no other processes using those ports before running the CORS sample.
+
+* In a command prompt window, open the directory where you cloned the repository, and open the SampleDestination directory. Run the command: dotnet run
+* Repeat the above step in the SampleOrigin directory
+* Open a browser window and go to `http://origin.example.com:5001`
+* Input a method and header to create a CORS request or use one of the example buttons to see CORS in action
+
+As an example, apart from `GET`, `HEAD` and `POST` requests, `PUT` requests are allowed in the CORS policy on SampleDestination. Any others, like `DELETE`, `OPTIONS` etc. are not allowed and throw an error.
+`Cache-Control` has been added as an allowed header to the sample. Any other headers are not allowed and throw an error. You may leave the header name and value blank.
+
+To edit the policy, please see `app.UseCors()` method in the `Startup.cs` file of SampleDestination.
+
+**If using Visual Studio to launch the request origin:**
+Open Visual Studio and in the `launchSettings.json` file for the SampleOrigin project, change the `launchUrl` under SampleOrigin to `http://origin.example.com:5001`.
+Using the dropdown near the Start button, choose SampleOrigin before pressing Start to ensure that it uses Kestrel and not IIS Express.
+
diff --git a/src/CORS/samples/SampleDestination/Program.cs b/src/CORS/samples/SampleDestination/Program.cs
new file mode 100644
index 0000000000..0c2bf0968f
--- /dev/null
+++ b/src/CORS/samples/SampleDestination/Program.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.IO;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace SampleDestination
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .UseKestrel()
+ .UseUrls("http://*:5000")
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .ConfigureLogging(factory => factory.AddConsole())
+ .UseStartup<Startup>()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/CORS/samples/SampleDestination/SampleDestination.csproj b/src/CORS/samples/SampleDestination/SampleDestination.csproj
new file mode 100644
index 0000000000..b8e59ede3a
--- /dev/null
+++ b/src/CORS/samples/SampleDestination/SampleDestination.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Cors\Microsoft.AspNetCore.Cors.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/CORS/samples/SampleDestination/Startup.cs b/src/CORS/samples/SampleDestination/Startup.cs
new file mode 100644
index 0000000000..0664d84572
--- /dev/null
+++ b/src/CORS/samples/SampleDestination/Startup.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.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace SampleDestination
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddCors();
+ }
+
+ public void Configure(IApplicationBuilder app, IHostingEnvironment env)
+ {
+ app.UseCors(policy => policy
+ .WithOrigins("http://origin.example.com:5001")
+ .WithMethods("PUT")
+ .WithHeaders("Cache-Control"));
+
+ app.Run(async context =>
+ {
+ var responseHeaders = context.Response.Headers;
+ context.Response.ContentType = "text/plain";
+ foreach (var responseHeader in responseHeaders)
+ {
+ await context.Response.WriteAsync("\n" + responseHeader.Key + ": " + responseHeader.Value);
+ }
+
+ await context.Response.WriteAsync("\nStatus code of your request: " + context.Response.StatusCode.ToString());
+ });
+ }
+ }
+}
diff --git a/src/CORS/samples/SampleOrigin/Program.cs b/src/CORS/samples/SampleOrigin/Program.cs
new file mode 100644
index 0000000000..278c09f48a
--- /dev/null
+++ b/src/CORS/samples/SampleOrigin/Program.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.IO;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace SampleOrigin
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .UseKestrel()
+ .UseUrls("http://*:5001")
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .ConfigureLogging(factory => factory.AddConsole())
+ .UseStartup<Startup>()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/CORS/samples/SampleOrigin/SampleOrigin.csproj b/src/CORS/samples/SampleOrigin/SampleOrigin.csproj
new file mode 100644
index 0000000000..b43f880f9c
--- /dev/null
+++ b/src/CORS/samples/SampleOrigin/SampleOrigin.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/CORS/samples/SampleOrigin/Startup.cs b/src/CORS/samples/SampleOrigin/Startup.cs
new file mode 100644
index 0000000000..a442b98d68
--- /dev/null
+++ b/src/CORS/samples/SampleOrigin/Startup.cs
@@ -0,0 +1,29 @@
+// 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.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace SampleOrigin
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ }
+
+ public void Configure(IApplicationBuilder app, IHostingEnvironment env)
+ {
+ app.Run(context =>
+ {
+ var fileInfoProvider = env.WebRootFileProvider;
+ var fileInfo = fileInfoProvider.GetFileInfo("/Index.html");
+ context.Response.ContentType = "text/html";
+ return context.Response.SendFileAsync(fileInfo);
+ });
+ }
+ }
+}
diff --git a/src/CORS/samples/SampleOrigin/wwwroot/Index.html b/src/CORS/samples/SampleOrigin/wwwroot/Index.html
new file mode 100644
index 0000000000..fad335b83f
--- /dev/null
+++ b/src/CORS/samples/SampleOrigin/wwwroot/Index.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <style>
+ p {
+ font-size: 20px;
+ }
+
+ .button {
+ border: none;
+ color: white;
+ padding: 10px 15px;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 14px;
+ margin: 5px 5px;
+ cursor: pointer;
+ }
+
+ .green {
+ background-color: #4CAF50;
+ }
+
+ .red {
+ background-color: indianred;
+ }
+
+ .gray {
+ background-color: gray;
+ }
+ </style>
+ <title>CORS Sample</title>
+</head>
+<body>
+ <script type="text/javascript">
+ // Make the CORS request.
+ function makeCORSRequest(method, headerName, headerValue) {
+ // Destination server with CORS enabled.
+ var url = 'http://destination.example.com:5000/';
+ var request = new XMLHttpRequest();
+ request.open(method, url, true);
+ if (headerName && headerValue) {
+ request.setRequestHeader(headerName, headerValue);
+ }
+
+ if (!request) {
+ alert('CORS not supported');
+ return;
+ }
+
+ // Response handlers.
+ request.onload = function () {
+ var text = request.responseText;
+ alert('Response from CORS ' + method + ' request to ' + url + ': ' + text);
+ };
+
+ request.onerror = function () {
+ alert('There was an error making the request for method ' + method);
+ };
+
+ request.send();
+ }
+ </script>
+
+ <p>CORS Sample</p>
+ Method: <input type="text" id="methodName" /><br /><br />
+ Header Name: <input type="text" id="headerName" /> Header Value: <input type="text" id="headerValue" /><br /><br />
+
+ <script>
+ document.getElementById('headerValue')
+ .addEventListener("keyup", function (event) {
+ event.preventDefault();
+ if (event.keyCode == 13) {
+ document.getElementById("CORS").click();
+ }
+ });
+ </script>
+
+ <button class="button gray" id="CORS" type="submit" onclick="makeCORSRequest(document.getElementById('methodName').value, document.getElementById('headerName').value, document.getElementById('headerValue').value);">Make a CORS Request</button><br /><br /><br /><br />
+
+ Method DELETE is not allowed:<button class="button red" id="InvalidMethodCORS" type="submit" onclick="makeCORSRequest('DELETE', 'Cache-Control', 'no-cache');">Invalid Method CORS Request</button>
+ Method PUT is allowed:<button class="button green" id="ValidMethodCORS" type="submit" onclick="makeCORSRequest('PUT', 'Cache-Control', 'no-cache');">Valid Method CORS Request</button><br /><br />
+
+ Header 'Max-Forwards' not supported:<button class="button red" id="InvalidHeaderCORS" type="submit" onclick="makeCORSRequest('PUT', 'Max-Forwards', '2');">Invalid Header CORS Request</button>
+ Header 'Cache-Control' is supported:<button class="button green" id="ValidHeaderCORS" type="submit" onclick="makeCORSRequest('PUT', 'Cache-Control', 'no-cache');">Valid Header CORS Request</button><br /><br />
+</body>
+</html> \ No newline at end of file
diff --git a/src/CORS/src/Directory.Build.props b/src/CORS/src/Directory.Build.props
new file mode 100644
index 0000000000..4f07cbc45d
--- /dev/null
+++ b/src/CORS/src/Directory.Build.props
@@ -0,0 +1,8 @@
+<Project>
+ <Import Project="..\Directory.Build.props" />
+
+ <ItemGroup>
+ <PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/CorsServiceCollectionExtensions.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/CorsServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..430621d2fd
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/CorsServiceCollectionExtensions.cs
@@ -0,0 +1,59 @@
+// 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.Cors.Infrastructure;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ /// <summary>
+ /// Extension methods for setting up cross-origin resource sharing services in an <see cref="IServiceCollection" />.
+ /// </summary>
+ public static class CorsServiceCollectionExtensions
+ {
+ /// <summary>
+ /// Adds cross-origin resource sharing services to the specified <see cref="IServiceCollection" />.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
+ /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
+ public static IServiceCollection AddCors(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.AddOptions();
+
+ services.TryAdd(ServiceDescriptor.Transient<ICorsService, CorsService>());
+ services.TryAdd(ServiceDescriptor.Transient<ICorsPolicyProvider, DefaultCorsPolicyProvider>());
+
+ return services;
+ }
+
+ /// <summary>
+ /// Adds cross-origin resource sharing services to the specified <see cref="IServiceCollection" />.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
+ /// <param name="setupAction">An <see cref="Action{CorsOptions}"/> to configure the provided <see cref="CorsOptions"/>.</param>
+ /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
+ public static IServiceCollection AddCors(this IServiceCollection services, Action<CorsOptions> setupAction)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (setupAction == null)
+ {
+ throw new ArgumentNullException(nameof(setupAction));
+ }
+
+ services.AddCors();
+ services.Configure(setupAction);
+
+ return services;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/DisableCorsAttribute.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/DisableCorsAttribute.cs
new file mode 100644
index 0000000000..ab4c755066
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/DisableCorsAttribute.cs
@@ -0,0 +1,14 @@
+// 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.Cors.Infrastructure;
+
+namespace Microsoft.AspNetCore.Cors
+{
+ /// <inheritdoc />
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
+ public class DisableCorsAttribute : Attribute, IDisableCorsAttribute
+ {
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/EnableCorsAttribute.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/EnableCorsAttribute.cs
new file mode 100644
index 0000000000..b19851e39a
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/EnableCorsAttribute.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;
+using Microsoft.AspNetCore.Cors.Infrastructure;
+
+namespace Microsoft.AspNetCore.Cors
+{
+ /// <inheritdoc />
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public class EnableCorsAttribute : Attribute, IEnableCorsAttribute
+ {
+ /// <summary>
+ /// Creates a new instance of the <see cref="EnableCorsAttribute"/> with the default policy
+ /// name defined by <see cref="CorsOptions.DefaultPolicyName"/>.
+ /// </summary>
+ public EnableCorsAttribute()
+ : this(policyName: null)
+ {
+ }
+
+ /// <summary>
+ /// Creates a new instance of the <see cref="EnableCorsAttribute"/> with the supplied policy name.
+ /// </summary>
+ /// <param name="policyName">The name of the policy to be applied.</param>
+ public EnableCorsAttribute(string policyName)
+ {
+ PolicyName = policyName;
+ }
+
+ /// <inheritdoc />
+ public string PolicyName { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsConstants.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsConstants.cs
new file mode 100644
index 0000000000..22110b2e41
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsConstants.cs
@@ -0,0 +1,95 @@
+// 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;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ /// <summary>
+ /// CORS-related constants.
+ /// </summary>
+ public static class CorsConstants
+ {
+ /// <summary>
+ /// The HTTP method for the CORS preflight request.
+ /// </summary>
+ public static readonly string PreflightHttpMethod = HttpMethods.Options;
+
+ /// <summary>
+ /// The Origin request header.
+ /// </summary>
+ public static readonly string Origin = HeaderNames.Origin;
+
+ /// <summary>
+ /// The value for the Access-Control-Allow-Origin response header to allow all origins.
+ /// </summary>
+ public static readonly string AnyOrigin = "*";
+
+ /// <summary>
+ /// The Access-Control-Request-Method request header.
+ /// </summary>
+ public static readonly string AccessControlRequestMethod = HeaderNames.AccessControlRequestMethod;
+
+ /// <summary>
+ /// The Access-Control-Request-Headers request header.
+ /// </summary>
+ public static readonly string AccessControlRequestHeaders = HeaderNames.AccessControlRequestHeaders;
+
+ /// <summary>
+ /// The Access-Control-Allow-Origin response header.
+ /// </summary>
+ public static readonly string AccessControlAllowOrigin = HeaderNames.AccessControlAllowOrigin;
+
+ /// <summary>
+ /// The Access-Control-Allow-Headers response header.
+ /// </summary>
+ public static readonly string AccessControlAllowHeaders = HeaderNames.AccessControlAllowHeaders;
+
+ /// <summary>
+ /// The Access-Control-Expose-Headers response header.
+ /// </summary>
+ public static readonly string AccessControlExposeHeaders = HeaderNames.AccessControlExposeHeaders;
+
+ /// <summary>
+ /// The Access-Control-Allow-Methods response header.
+ /// </summary>
+ public static readonly string AccessControlAllowMethods = HeaderNames.AccessControlAllowMethods;
+
+ /// <summary>
+ /// The Access-Control-Allow-Credentials response header.
+ /// </summary>
+ public static readonly string AccessControlAllowCredentials = HeaderNames.AccessControlAllowCredentials;
+
+ /// <summary>
+ /// The Access-Control-Max-Age response header.
+ /// </summary>
+ public static readonly string AccessControlMaxAge = HeaderNames.AccessControlMaxAge;
+
+
+ internal static readonly string[] SimpleRequestHeaders =
+ {
+ HeaderNames.Origin,
+ HeaderNames.Accept,
+ HeaderNames.AcceptLanguage,
+ HeaderNames.ContentLanguage,
+ };
+
+ internal static readonly string[] SimpleResponseHeaders =
+ {
+ HeaderNames.CacheControl,
+ HeaderNames.ContentLanguage,
+ HeaderNames.ContentType,
+ HeaderNames.Expires,
+ HeaderNames.LastModified,
+ HeaderNames.Pragma
+ };
+
+ internal static readonly string[] SimpleMethods =
+ {
+ HttpMethods.Get,
+ HttpMethods.Head,
+ HttpMethods.Post
+ };
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddleware.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddleware.cs
new file mode 100644
index 0000000000..eb773f5abf
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddleware.cs
@@ -0,0 +1,131 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ /// <summary>
+ /// An ASP.NET middleware for handling CORS.
+ /// </summary>
+ public class CorsMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly ICorsService _corsService;
+ private readonly ICorsPolicyProvider _corsPolicyProvider;
+ private readonly CorsPolicy _policy;
+ private readonly string _corsPolicyName;
+
+ /// <summary>
+ /// Instantiates a new <see cref="CorsMiddleware"/>.
+ /// </summary>
+ /// <param name="next">The next middleware in the pipeline.</param>
+ /// <param name="corsService">An instance of <see cref="ICorsService"/>.</param>
+ /// <param name="policyProvider">A policy provider which can get an <see cref="CorsPolicy"/>.</param>
+ public CorsMiddleware(
+ RequestDelegate next,
+ ICorsService corsService,
+ ICorsPolicyProvider policyProvider)
+ : this(next, corsService, policyProvider, policyName: null)
+ {
+ }
+
+ /// <summary>
+ /// Instantiates a new <see cref="CorsMiddleware"/>.
+ /// </summary>
+ /// <param name="next">The next middleware in the pipeline.</param>
+ /// <param name="corsService">An instance of <see cref="ICorsService"/>.</param>
+ /// <param name="policyProvider">A policy provider which can get an <see cref="CorsPolicy"/>.</param>
+ /// <param name="policyName">An optional name of the policy to be fetched.</param>
+ public CorsMiddleware(
+ RequestDelegate next,
+ ICorsService corsService,
+ ICorsPolicyProvider policyProvider,
+ string policyName)
+ {
+ if (next == null)
+ {
+ throw new ArgumentNullException(nameof(next));
+ }
+
+ if (corsService == null)
+ {
+ throw new ArgumentNullException(nameof(corsService));
+ }
+
+ if (policyProvider == null)
+ {
+ throw new ArgumentNullException(nameof(policyProvider));
+ }
+
+ _next = next;
+ _corsService = corsService;
+ _corsPolicyProvider = policyProvider;
+ _corsPolicyName = policyName;
+ }
+
+ /// <summary>
+ /// Instantiates a new <see cref="CorsMiddleware"/>.
+ /// </summary>
+ /// <param name="next">The next middleware in the pipeline.</param>
+ /// <param name="corsService">An instance of <see cref="ICorsService"/>.</param>
+ /// <param name="policy">An instance of the <see cref="CorsPolicy"/> which can be applied.</param>
+ public CorsMiddleware(
+ RequestDelegate next,
+ ICorsService corsService,
+ CorsPolicy policy)
+ {
+ if (next == null)
+ {
+ throw new ArgumentNullException(nameof(next));
+ }
+
+ if (corsService == null)
+ {
+ throw new ArgumentNullException(nameof(corsService));
+ }
+
+ if (policy == null)
+ {
+ throw new ArgumentNullException(nameof(policy));
+ }
+
+ _next = next;
+ _corsService = corsService;
+ _policy = policy;
+ }
+
+ /// <inheritdoc />
+ public async Task Invoke(HttpContext context)
+ {
+ if (context.Request.Headers.ContainsKey(CorsConstants.Origin))
+ {
+ var corsPolicy = _policy ?? await _corsPolicyProvider?.GetPolicyAsync(context, _corsPolicyName);
+ if (corsPolicy != null)
+ {
+ var corsResult = _corsService.EvaluatePolicy(context, corsPolicy);
+ _corsService.ApplyResult(corsResult, context.Response);
+
+ var accessControlRequestMethod =
+ context.Request.Headers[CorsConstants.AccessControlRequestMethod];
+ if (string.Equals(
+ context.Request.Method,
+ CorsConstants.PreflightHttpMethod,
+ StringComparison.OrdinalIgnoreCase) &&
+ !StringValues.IsNullOrEmpty(accessControlRequestMethod))
+ {
+ // Since there is a policy which was identified,
+ // always respond to preflight requests.
+ context.Response.StatusCode = StatusCodes.Status204NoContent;
+ return;
+ }
+ }
+ }
+
+ await _next(context);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddlewareExtensions.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddlewareExtensions.cs
new file mode 100644
index 0000000000..dea1ad01d9
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsMiddlewareExtensions.cs
@@ -0,0 +1,70 @@
+// 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.Cors.Infrastructure;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// The <see cref="IApplicationBuilder"/> extensions for adding CORS middleware support.
+ /// </summary>
+ public static class CorsMiddlewareExtensions
+ {
+ /// <summary>
+ /// Adds a CORS middleware to your web application pipeline to allow cross domain requests.
+ /// </summary>
+ /// <param name="app">The IApplicationBuilder passed to your Configure method</param>
+ /// <returns>The original app parameter</returns>
+ public static IApplicationBuilder UseCors(this IApplicationBuilder app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseMiddleware<CorsMiddleware>();
+ }
+
+ /// <summary>
+ /// Adds a CORS middleware to your web application pipeline to allow cross domain requests.
+ /// </summary>
+ /// <param name="app">The IApplicationBuilder passed to your Configure method</param>
+ /// <param name="policyName">The policy name of a configured policy.</param>
+ /// <returns>The original app parameter</returns>
+ public static IApplicationBuilder UseCors(this IApplicationBuilder app, string policyName)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseMiddleware<CorsMiddleware>(policyName);
+ }
+
+ /// <summary>
+ /// Adds a CORS middleware to your web application pipeline to allow cross domain requests.
+ /// </summary>
+ /// <param name="app">The IApplicationBuilder passed to your Configure method.</param>
+ /// <param name="configurePolicy">A delegate which can use a policy builder to build a policy.</param>
+ /// <returns>The original app parameter</returns>
+ public static IApplicationBuilder UseCors(
+ this IApplicationBuilder app,
+ Action<CorsPolicyBuilder> configurePolicy)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ if (configurePolicy == null)
+ {
+ throw new ArgumentNullException(nameof(configurePolicy));
+ }
+
+ var policyBuilder = new CorsPolicyBuilder();
+ configurePolicy(policyBuilder);
+ return app.UseMiddleware<CorsMiddleware>(policyBuilder.Build());
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsOptions.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsOptions.cs
new file mode 100644
index 0000000000..92e1f775ba
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsOptions.cs
@@ -0,0 +1,119 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ /// <summary>
+ /// Provides programmatic configuration for Cors.
+ /// </summary>
+ public class CorsOptions
+ {
+ private string _defaultPolicyName = "__DefaultCorsPolicy";
+ private IDictionary<string, CorsPolicy> PolicyMap { get; } = new Dictionary<string, CorsPolicy>();
+
+ public string DefaultPolicyName
+ {
+ get
+ {
+ return _defaultPolicyName;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ _defaultPolicyName = value;
+ }
+ }
+
+ /// <summary>
+ /// Adds a new policy and sets it as the default.
+ /// </summary>
+ /// <param name="policy">The <see cref="CorsPolicy"/> policy to be added.</param>
+ public void AddDefaultPolicy(CorsPolicy policy)
+ {
+ if (policy == null)
+ {
+ throw new ArgumentNullException(nameof(policy));
+ }
+
+ AddPolicy(DefaultPolicyName, policy);
+ }
+
+ /// <summary>
+ /// Adds a new policy and sets it as the default.
+ /// </summary>
+ /// <param name="configurePolicy">A delegate which can use a policy builder to build a policy.</param>
+ public void AddDefaultPolicy(Action<CorsPolicyBuilder> configurePolicy)
+ {
+ if (configurePolicy == null)
+ {
+ throw new ArgumentNullException(nameof(configurePolicy));
+ }
+
+ AddPolicy(DefaultPolicyName, configurePolicy);
+ }
+
+ /// <summary>
+ /// Adds a new policy.
+ /// </summary>
+ /// <param name="name">The name of the policy.</param>
+ /// <param name="policy">The <see cref="CorsPolicy"/> policy to be added.</param>
+ public void AddPolicy(string name, CorsPolicy policy)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (policy == null)
+ {
+ throw new ArgumentNullException(nameof(policy));
+ }
+
+ PolicyMap[name] = policy;
+ }
+
+ /// <summary>
+ /// Adds a new policy.
+ /// </summary>
+ /// <param name="name">The name of the policy.</param>
+ /// <param name="configurePolicy">A delegate which can use a policy builder to build a policy.</param>
+ public void AddPolicy(string name, Action<CorsPolicyBuilder> configurePolicy)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (configurePolicy == null)
+ {
+ throw new ArgumentNullException(nameof(configurePolicy));
+ }
+
+ var policyBuilder = new CorsPolicyBuilder();
+ configurePolicy(policyBuilder);
+ PolicyMap[name] = policyBuilder.Build();
+ }
+
+ /// <summary>
+ /// Gets the policy based on the <paramref name="name"/>
+ /// </summary>
+ /// <param name="name">The name of the policy to lookup.</param>
+ /// <returns>The <see cref="CorsPolicy"/> if the policy was added.<c>null</c> otherwise.</returns>
+ public CorsPolicy GetPolicy(string name)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ return PolicyMap.ContainsKey(name) ? PolicyMap[name] : null;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicy.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicy.cs
new file mode 100644
index 0000000000..f541863c69
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicy.cs
@@ -0,0 +1,164 @@
+// 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.Text;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ /// <summary>
+ /// Defines the policy for Cross-Origin requests based on the CORS specifications.
+ /// </summary>
+ public class CorsPolicy
+ {
+ private TimeSpan? _preflightMaxAge;
+
+ /// <summary>
+ /// Default constructor for a CorsPolicy.
+ /// </summary>
+ public CorsPolicy()
+ {
+ IsOriginAllowed = DefaultIsOriginAllowed;
+ }
+
+ /// <summary>
+ /// Gets a value indicating if all headers are allowed.
+ /// </summary>
+ public bool AllowAnyHeader
+ {
+ get
+ {
+ if (Headers == null || Headers.Count != 1 || Headers.Count == 1 && Headers[0] != "*")
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating if all methods are allowed.
+ /// </summary>
+ public bool AllowAnyMethod
+ {
+ get
+ {
+ if (Methods == null || Methods.Count != 1 || Methods.Count == 1 && Methods[0] != "*")
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating if all origins are allowed.
+ /// </summary>
+ public bool AllowAnyOrigin
+ {
+ get
+ {
+ if (Origins == null || Origins.Count != 1 || Origins.Count == 1 && Origins[0] != "*")
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a function which evaluates whether an origin is allowed.
+ /// </summary>
+ public Func<string, bool> IsOriginAllowed { get; set; }
+
+ /// <summary>
+ /// Gets the headers that the resource might use and can be exposed.
+ /// </summary>
+ public IList<string> ExposedHeaders { get; } = new List<string>();
+
+ /// <summary>
+ /// Gets the headers that are supported by the resource.
+ /// </summary>
+ public IList<string> Headers { get; } = new List<string>();
+
+ /// <summary>
+ /// Gets the methods that are supported by the resource.
+ /// </summary>
+ public IList<string> Methods { get; } = new List<string>();
+
+ /// <summary>
+ /// Gets the origins that are allowed to access the resource.
+ /// </summary>
+ public IList<string> Origins { get; } = new List<string>();
+
+ /// <summary>
+ /// Gets or sets the <see cref="TimeSpan"/> for which the results of a preflight request can be cached.
+ /// </summary>
+ public TimeSpan? PreflightMaxAge
+ {
+ get
+ {
+ return _preflightMaxAge;
+ }
+ set
+ {
+ if (value < TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), Resources.PreflightMaxAgeOutOfRange);
+ }
+
+ _preflightMaxAge = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the resource supports user credentials in the request.
+ /// </summary>
+ public bool SupportsCredentials { get; set; }
+
+ /// <summary>
+ /// Returns a <see cref="System.String" /> that represents this instance.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="System.String" /> that represents this instance.
+ /// </returns>
+ public override string ToString()
+ {
+ var builder = new StringBuilder();
+ builder.Append("AllowAnyHeader: ");
+ builder.Append(AllowAnyHeader);
+ builder.Append(", AllowAnyMethod: ");
+ builder.Append(AllowAnyMethod);
+ builder.Append(", AllowAnyOrigin: ");
+ builder.Append(AllowAnyOrigin);
+ builder.Append(", PreflightMaxAge: ");
+ builder.Append(PreflightMaxAge.HasValue ?
+ PreflightMaxAge.Value.TotalSeconds.ToString() : "null");
+ builder.Append(", SupportsCredentials: ");
+ builder.Append(SupportsCredentials);
+ builder.Append(", Origins: {");
+ builder.Append(string.Join(",", Origins));
+ builder.Append("}");
+ builder.Append(", Methods: {");
+ builder.Append(string.Join(",", Methods));
+ builder.Append("}");
+ builder.Append(", Headers: {");
+ builder.Append(string.Join(",", Headers));
+ builder.Append("}");
+ builder.Append(", ExposedHeaders: {");
+ builder.Append(string.Join(",", ExposedHeaders));
+ builder.Append("}");
+ return builder.ToString();
+ }
+
+ private bool DefaultIsOriginAllowed(string origin)
+ {
+ return Origins.Contains(origin, StringComparer.Ordinal);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyBuilder.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyBuilder.cs
new file mode 100644
index 0000000000..88e16fa0fa
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyBuilder.cs
@@ -0,0 +1,225 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ /// <summary>
+ /// Exposes methods to build a policy.
+ /// </summary>
+ public class CorsPolicyBuilder
+ {
+ private readonly CorsPolicy _policy = new CorsPolicy();
+
+ /// <summary>
+ /// Creates a new instance of the <see cref="CorsPolicyBuilder"/>.
+ /// </summary>
+ /// <param name="origins">list of origins which can be added.</param>
+ public CorsPolicyBuilder(params string[] origins)
+ {
+ WithOrigins(origins);
+ }
+
+ /// <summary>
+ /// Creates a new instance of the <see cref="CorsPolicyBuilder"/>.
+ /// </summary>
+ /// <param name="policy">The policy which will be used to intialize the builder.</param>
+ public CorsPolicyBuilder(CorsPolicy policy)
+ {
+ Combine(policy);
+ }
+
+ /// <summary>
+ /// Adds the specified <paramref name="origins"/> to the policy.
+ /// </summary>
+ /// <param name="origins">The origins that are allowed.</param>
+ /// <returns>The current policy builder.</returns>
+ public CorsPolicyBuilder WithOrigins(params string[] origins)
+ {
+ foreach (var req in origins)
+ {
+ _policy.Origins.Add(req);
+ }
+
+ return this;
+ }
+
+ /// <summary>
+ /// Adds the specified <paramref name="headers"/> to the policy.
+ /// </summary>
+ /// <param name="headers">The headers which need to be allowed in the request.</param>
+ /// <returns>The current policy builder.</returns>
+ public CorsPolicyBuilder WithHeaders(params string[] headers)
+ {
+ foreach (var req in headers)
+ {
+ _policy.Headers.Add(req);
+ }
+ return this;
+ }
+
+ /// <summary>
+ /// Adds the specified <paramref name="exposedHeaders"/> to the policy.
+ /// </summary>
+ /// <param name="exposedHeaders">The headers which need to be exposed to the client.</param>
+ /// <returns>The current policy builder.</returns>
+ public CorsPolicyBuilder WithExposedHeaders(params string[] exposedHeaders)
+ {
+ foreach (var req in exposedHeaders)
+ {
+ _policy.ExposedHeaders.Add(req);
+ }
+
+ return this;
+ }
+
+ /// <summary>
+ /// Adds the specified <paramref name="methods"/> to the policy.
+ /// </summary>
+ /// <param name="methods">The methods which need to be added to the policy.</param>
+ /// <returns>The current policy builder.</returns>
+ public CorsPolicyBuilder WithMethods(params string[] methods)
+ {
+ foreach (var req in methods)
+ {
+ _policy.Methods.Add(req);
+ }
+
+ return this;
+ }
+
+ /// <summary>
+ /// Sets the policy to allow credentials.
+ /// </summary>
+ /// <returns>The current policy builder.</returns>
+ public CorsPolicyBuilder AllowCredentials()
+ {
+ _policy.SupportsCredentials = true;
+ return this;
+ }
+
+ /// <summary>
+ /// Sets the policy to not allow credentials.
+ /// </summary>
+ /// <returns>The current policy builder.</returns>
+ public CorsPolicyBuilder DisallowCredentials()
+ {
+ _policy.SupportsCredentials = false;
+ return this;
+ }
+
+ /// <summary>
+ /// Ensures that the policy allows any origin.
+ /// </summary>
+ /// <returns>The current policy builder.</returns>
+ public CorsPolicyBuilder AllowAnyOrigin()
+ {
+ _policy.Origins.Clear();
+ _policy.Origins.Add(CorsConstants.AnyOrigin);
+ return this;
+ }
+
+ /// <summary>
+ /// Ensures that the policy allows any method.
+ /// </summary>
+ /// <returns>The current policy builder.</returns>
+ public CorsPolicyBuilder AllowAnyMethod()
+ {
+ _policy.Methods.Clear();
+ _policy.Methods.Add("*");
+ return this;
+ }
+
+ /// <summary>
+ /// Ensures that the policy allows any header.
+ /// </summary>
+ /// <returns>The current policy builder.</returns>
+ public CorsPolicyBuilder AllowAnyHeader()
+ {
+ _policy.Headers.Clear();
+ _policy.Headers.Add("*");
+ return this;
+ }
+
+ /// <summary>
+ /// Sets the preflightMaxAge for the underlying policy.
+ /// </summary>
+ /// <param name="preflightMaxAge">A positive <see cref="TimeSpan"/> indicating the time a preflight
+ /// request can be cached.</param>
+ /// <returns>The current policy builder.</returns>
+ public CorsPolicyBuilder SetPreflightMaxAge(TimeSpan preflightMaxAge)
+ {
+ _policy.PreflightMaxAge = preflightMaxAge;
+ return this;
+ }
+
+ /// <summary>
+ /// Sets the specified <paramref name="isOriginAllowed"/> for the underlying policy.
+ /// </summary>
+ /// <param name="isOriginAllowed">The function used by the policy to evaluate if an origin is allowed.</param>
+ /// <returns>The current policy builder.</returns>
+ public CorsPolicyBuilder SetIsOriginAllowed(Func<string, bool> isOriginAllowed)
+ {
+ _policy.IsOriginAllowed = isOriginAllowed;
+ return this;
+ }
+
+ /// <summary>
+ /// Sets the <see cref="CorsPolicy.IsOriginAllowed"/> property of the policy to be a function
+ /// that allows origins to match a configured wildcarded domain when evaluating if the
+ /// origin is allowed.
+ /// </summary>
+ /// <returns>The current policy builder.</returns>
+ public CorsPolicyBuilder SetIsOriginAllowedToAllowWildcardSubdomains()
+ {
+ _policy.IsOriginAllowed = _policy.IsOriginAnAllowedSubdomain;
+ return this;
+ }
+
+ /// <summary>
+ /// Builds a new <see cref="CorsPolicy"/> using the entries added.
+ /// </summary>
+ /// <returns>The constructed <see cref="CorsPolicy"/>.</returns>
+ public CorsPolicy Build()
+ {
+ return _policy;
+ }
+
+ /// <summary>
+ /// Combines the given <paramref name="policy"/> to the existing properties in the builder.
+ /// </summary>
+ /// <param name="policy">The policy which needs to be combined.</param>
+ /// <returns>The current policy builder.</returns>
+ private CorsPolicyBuilder Combine(CorsPolicy policy)
+ {
+ if (policy == null)
+ {
+ throw new ArgumentNullException(nameof(policy));
+ }
+
+ WithOrigins(policy.Origins.ToArray());
+ WithHeaders(policy.Headers.ToArray());
+ WithExposedHeaders(policy.ExposedHeaders.ToArray());
+ WithMethods(policy.Methods.ToArray());
+ SetIsOriginAllowed(policy.IsOriginAllowed);
+
+ if (policy.PreflightMaxAge.HasValue)
+ {
+ SetPreflightMaxAge(policy.PreflightMaxAge.Value);
+ }
+
+ if (policy.SupportsCredentials)
+ {
+ AllowCredentials();
+ }
+ else
+ {
+ DisallowCredentials();
+ }
+
+ return this;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyExtensions.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyExtensions.cs
new file mode 100644
index 0000000000..312f772994
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyExtensions.cs
@@ -0,0 +1,36 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ internal static class CorsPolicyExtensions
+ {
+ private const string _WildcardSubdomain = "*.";
+
+ public static bool IsOriginAnAllowedSubdomain(this CorsPolicy policy, string origin)
+ {
+ if (policy.Origins.Contains(origin))
+ {
+ return true;
+ }
+
+ if (Uri.TryCreate(origin, UriKind.Absolute, out var originUri))
+ {
+ return policy.Origins
+ .Where(o => o.Contains($"://{_WildcardSubdomain}"))
+ .Select(CreateDomainUri)
+ .Any(domain => UriHelpers.IsSubdomainOf(originUri, domain));
+ }
+
+ return false;
+ }
+
+ private static Uri CreateDomainUri(string origin)
+ {
+ return new Uri(origin.Replace(_WildcardSubdomain, string.Empty), UriKind.Absolute);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsResult.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsResult.cs
new file mode 100644
index 0000000000..99d38b9fd1
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsResult.cs
@@ -0,0 +1,94 @@
+// 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.Text;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ /// <summary>
+ /// Results returned by <see cref="ICorsService"/>.
+ /// </summary>
+ public class CorsResult
+ {
+ private TimeSpan? _preflightMaxAge;
+
+ /// <summary>
+ /// Gets or sets the allowed origin.
+ /// </summary>
+ public string AllowedOrigin { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the resource supports user credentials.
+ /// </summary>
+ public bool SupportsCredentials { get; set; }
+
+ /// <summary>
+ /// Gets the allowed methods.
+ /// </summary>
+ public IList<string> AllowedMethods { get; } = new List<string>();
+
+ /// <summary>
+ /// Gets the allowed headers.
+ /// </summary>
+ public IList<string> AllowedHeaders { get; } = new List<string>();
+
+ /// <summary>
+ /// Gets the allowed headers that can be exposed on the response.
+ /// </summary>
+ public IList<string> AllowedExposedHeaders { get; } = new List<string>();
+
+ /// <summary>
+ /// Gets or sets a value indicating if a 'Vary' header with the value 'Origin' is required.
+ /// </summary>
+ public bool VaryByOrigin { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="TimeSpan"/> for which the results of a preflight request can be cached.
+ /// </summary>
+ public TimeSpan? PreflightMaxAge
+ {
+ get
+ {
+ return _preflightMaxAge;
+ }
+ set
+ {
+ if (value < TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), Resources.PreflightMaxAgeOutOfRange);
+ }
+ _preflightMaxAge = value;
+ }
+ }
+
+ /// <summary>
+ /// Returns a <see cref="System.String" /> that represents this instance.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="System.String" /> that represents this instance.
+ /// </returns>
+ public override string ToString()
+ {
+ var builder = new StringBuilder();
+ builder.Append("AllowCredentials: ");
+ builder.Append(SupportsCredentials);
+ builder.Append(", PreflightMaxAge: ");
+ builder.Append(PreflightMaxAge.HasValue ?
+ PreflightMaxAge.Value.TotalSeconds.ToString() : "null");
+ builder.Append(", AllowOrigin: ");
+ builder.Append(AllowedOrigin);
+ builder.Append(", AllowExposedHeaders: {");
+ builder.Append(string.Join(",", AllowedExposedHeaders));
+ builder.Append("}");
+ builder.Append(", AllowHeaders: {");
+ builder.Append(string.Join(",", AllowedHeaders));
+ builder.Append("}");
+ builder.Append(", AllowMethods: {");
+ builder.Append(string.Join(",", AllowedMethods));
+ builder.Append("}");
+ return builder.ToString();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs
new file mode 100644
index 0000000000..5060ddf205
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsService.cs
@@ -0,0 +1,313 @@
+// 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.Globalization;
+using System.Linq;
+using Microsoft.AspNetCore.Cors.Internal;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ /// <summary>
+ /// Default implementation of <see cref="ICorsService"/>.
+ /// </summary>
+ public class CorsService : ICorsService
+ {
+ private readonly CorsOptions _options;
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Creates a new instance of the <see cref="CorsService"/>.
+ /// </summary>
+ /// <param name="options">The option model representing <see cref="CorsOptions"/>.</param>
+ public CorsService(IOptions<CorsOptions> options)
+ : this(options, loggerFactory: null)
+ {
+ }
+
+ /// <summary>
+ /// Creates a new instance of the <see cref="CorsService"/>.
+ /// </summary>
+ /// <param name="options">The option model representing <see cref="CorsOptions"/>.</param>
+ /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
+ public CorsService(IOptions<CorsOptions> options, ILoggerFactory loggerFactory)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ _options = options.Value;
+ _logger = loggerFactory?.CreateLogger<CorsService>();
+ }
+
+ /// <summary>
+ /// Looks up a policy using the <paramref name="policyName"/> and then evaluates the policy using the passed in
+ /// <paramref name="context"/>.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="policyName"></param>
+ /// <returns>A <see cref="CorsResult"/> which contains the result of policy evaluation and can be
+ /// used by the caller to set appropriate response headers.</returns>
+ public CorsResult EvaluatePolicy(HttpContext context, string policyName)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var policy = _options.GetPolicy(policyName);
+ return EvaluatePolicy(context, policy);
+ }
+
+ /// <inheritdoc />
+ public CorsResult EvaluatePolicy(HttpContext context, CorsPolicy policy)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (policy == null)
+ {
+ throw new ArgumentNullException(nameof(policy));
+ }
+
+ var corsResult = new CorsResult();
+ var accessControlRequestMethod = context.Request.Headers[CorsConstants.AccessControlRequestMethod];
+ if (string.Equals(context.Request.Method, CorsConstants.PreflightHttpMethod, StringComparison.OrdinalIgnoreCase) &&
+ !StringValues.IsNullOrEmpty(accessControlRequestMethod))
+ {
+ _logger?.IsPreflightRequest();
+ EvaluatePreflightRequest(context, policy, corsResult);
+ }
+ else
+ {
+ EvaluateRequest(context, policy, corsResult);
+ }
+
+ return corsResult;
+ }
+
+ public virtual void EvaluateRequest(HttpContext context, CorsPolicy policy, CorsResult result)
+ {
+ var origin = context.Request.Headers[CorsConstants.Origin];
+ if (!IsOriginAllowed(policy, origin))
+ {
+ return;
+ }
+
+ AddOriginToResult(origin, policy, result);
+ result.SupportsCredentials = policy.SupportsCredentials;
+ AddHeaderValues(result.AllowedExposedHeaders, policy.ExposedHeaders);
+ _logger?.PolicySuccess();
+ }
+
+ public virtual void EvaluatePreflightRequest(HttpContext context, CorsPolicy policy, CorsResult result)
+ {
+ var origin = context.Request.Headers[CorsConstants.Origin];
+ if (!IsOriginAllowed(policy, origin))
+ {
+ return;
+ }
+
+ var accessControlRequestMethod = context.Request.Headers[CorsConstants.AccessControlRequestMethod];
+ if (StringValues.IsNullOrEmpty(accessControlRequestMethod))
+ {
+ return;
+ }
+
+ var requestHeaders =
+ context.Request.Headers.GetCommaSeparatedValues(CorsConstants.AccessControlRequestHeaders);
+
+ if (!policy.AllowAnyMethod)
+ {
+ var found = false;
+ for (var i = 0; i < policy.Methods.Count; i++)
+ {
+ var method = policy.Methods[i];
+ if (string.Equals(method, accessControlRequestMethod, StringComparison.OrdinalIgnoreCase))
+ {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ _logger?.PolicyFailure();
+ _logger?.AccessControlMethodNotAllowed(accessControlRequestMethod);
+ return;
+ }
+ }
+
+ if (!policy.AllowAnyHeader &&
+ requestHeaders != null)
+ {
+ foreach (var requestHeader in requestHeaders)
+ {
+ if (!CorsConstants.SimpleRequestHeaders.Contains(requestHeader, StringComparer.OrdinalIgnoreCase) &&
+ !policy.Headers.Contains(requestHeader, StringComparer.OrdinalIgnoreCase))
+ {
+ _logger?.PolicyFailure();
+ _logger?.RequestHeaderNotAllowed(requestHeader);
+ return;
+ }
+ }
+ }
+
+ AddOriginToResult(origin, policy, result);
+ result.SupportsCredentials = policy.SupportsCredentials;
+ result.PreflightMaxAge = policy.PreflightMaxAge;
+ result.AllowedMethods.Add(accessControlRequestMethod);
+ AddHeaderValues(result.AllowedHeaders, requestHeaders);
+ _logger?.PolicySuccess();
+ }
+
+ /// <inheritdoc />
+ public virtual void ApplyResult(CorsResult result, HttpResponse response)
+ {
+ if (result == null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+ if (response == null)
+ {
+ throw new ArgumentNullException(nameof(response));
+ }
+
+ var headers = response.Headers;
+
+ if (result.AllowedOrigin != null)
+ {
+ headers[CorsConstants.AccessControlAllowOrigin] = result.AllowedOrigin;
+ }
+
+ if (result.VaryByOrigin)
+ {
+ headers["Vary"] = "Origin";
+ }
+
+ if (result.SupportsCredentials)
+ {
+ headers[CorsConstants.AccessControlAllowCredentials] = "true";
+ }
+
+ if (result.AllowedMethods.Count > 0)
+ {
+ // Filter out simple methods
+ var nonSimpleAllowMethods = result.AllowedMethods
+ .Where(m =>
+ !CorsConstants.SimpleMethods.Contains(m, StringComparer.OrdinalIgnoreCase))
+ .ToArray();
+
+ if (nonSimpleAllowMethods.Length > 0)
+ {
+ headers.SetCommaSeparatedValues(
+ CorsConstants.AccessControlAllowMethods,
+ nonSimpleAllowMethods);
+ }
+ }
+
+ if (result.AllowedHeaders.Count > 0)
+ {
+ // Filter out simple request headers
+ var nonSimpleAllowRequestHeaders = result.AllowedHeaders
+ .Where(header =>
+ !CorsConstants.SimpleRequestHeaders.Contains(header, StringComparer.OrdinalIgnoreCase))
+ .ToArray();
+
+ if (nonSimpleAllowRequestHeaders.Length > 0)
+ {
+ headers.SetCommaSeparatedValues(
+ CorsConstants.AccessControlAllowHeaders,
+ nonSimpleAllowRequestHeaders);
+ }
+ }
+
+ if (result.AllowedExposedHeaders.Count > 0)
+ {
+ // Filter out simple response headers
+ var nonSimpleAllowResponseHeaders = result.AllowedExposedHeaders
+ .Where(header =>
+ !CorsConstants.SimpleResponseHeaders.Contains(header, StringComparer.OrdinalIgnoreCase))
+ .ToArray();
+
+ if (nonSimpleAllowResponseHeaders.Length > 0)
+ {
+ headers.SetCommaSeparatedValues(
+ CorsConstants.AccessControlExposeHeaders,
+ nonSimpleAllowResponseHeaders);
+ }
+ }
+
+ if (result.PreflightMaxAge.HasValue)
+ {
+ headers[CorsConstants.AccessControlMaxAge]
+ = result.PreflightMaxAge.Value.TotalSeconds.ToString(CultureInfo.InvariantCulture);
+ }
+ }
+
+ private void AddOriginToResult(string origin, CorsPolicy policy, CorsResult result)
+ {
+ if (policy.AllowAnyOrigin)
+ {
+ if (policy.SupportsCredentials)
+ {
+ result.AllowedOrigin = origin;
+ result.VaryByOrigin = true;
+ }
+ else
+ {
+ result.AllowedOrigin = CorsConstants.AnyOrigin;
+ }
+ }
+ else if (policy.IsOriginAllowed(origin))
+ {
+ result.AllowedOrigin = origin;
+
+ if(policy.Origins.Count > 1)
+ {
+ result.VaryByOrigin = true;
+ }
+ }
+ }
+
+ private static void AddHeaderValues(IList<string> target, IEnumerable<string> headerValues)
+ {
+ if (headerValues == null)
+ {
+ return;
+ }
+
+ foreach (var current in headerValues)
+ {
+ target.Add(current);
+ }
+ }
+
+ private bool IsOriginAllowed(CorsPolicy policy, StringValues origin)
+ {
+ if (StringValues.IsNullOrEmpty(origin))
+ {
+ _logger?.RequestDoesNotHaveOriginHeader();
+ return false;
+ }
+
+ _logger?.RequestHasOriginHeader(origin);
+ if (policy.AllowAnyOrigin || policy.IsOriginAllowed(origin))
+ {
+ return true;
+ }
+ _logger?.PolicyFailure();
+ _logger?.OriginNotAllowed(origin);
+ return false;
+ }
+ }
+}
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/DefaultCorsPolicyProvider.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/DefaultCorsPolicyProvider.cs
new file mode 100644
index 0000000000..4841d60f75
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/DefaultCorsPolicyProvider.cs
@@ -0,0 +1,36 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ /// <inheritdoc />
+ public class DefaultCorsPolicyProvider : ICorsPolicyProvider
+ {
+ private readonly CorsOptions _options;
+
+ /// <summary>
+ /// Creates a new instance of <see cref="DefaultCorsPolicyProvider"/>.
+ /// </summary>
+ /// <param name="options">The options configured for the application.</param>
+ public DefaultCorsPolicyProvider(IOptions<CorsOptions> options)
+ {
+ _options = options.Value;
+ }
+
+ /// <inheritdoc />
+ public Task<CorsPolicy> GetPolicyAsync(HttpContext context, string policyName)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return Task.FromResult(_options.GetPolicy(policyName ?? _options.DefaultPolicyName));
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsPolicyProvider.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsPolicyProvider.cs
new file mode 100644
index 0000000000..7785e3de9c
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsPolicyProvider.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.Cors.Infrastructure
+{
+ /// <summary>
+ /// A type which can provide a <see cref="CorsPolicy"/> for a particular <see cref="HttpContext"/>.
+ /// </summary>
+ public interface ICorsPolicyProvider
+ {
+ /// <summary>
+ /// Gets a <see cref="CorsPolicy"/> from the given <paramref name="context"/>
+ /// </summary>
+ /// <param name="context">The <see cref="HttpContext"/> associated with this call.</param>
+ /// <param name="policyName">An optional policy name to look for.</param>
+ /// <returns>A <see cref="CorsPolicy"/></returns>
+ Task<CorsPolicy> GetPolicyAsync(HttpContext context, string policyName);
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsService.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsService.cs
new file mode 100644
index 0000000000..ab8176a961
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/ICorsService.cs
@@ -0,0 +1,31 @@
+// 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.Cors.Infrastructure
+{
+ /// <summary>
+ /// A type which can evaluate a policy for a particular <see cref="HttpContext"/>.
+ /// </summary>
+ public interface ICorsService
+ {
+ /// <summary>
+ /// Evaluates the given <paramref name="policy"/> using the passed in <paramref name="context"/>.
+ /// </summary>
+ /// <param name="context">The <see cref="HttpContext"/> associated with the call.</param>
+ /// <param name="policy">The <see cref="CorsPolicy"/> which needs to be evaluated.</param>
+ /// <returns>A <see cref="CorsResult"/> which contains the result of policy evaluation and can be
+ /// used by the caller to set appropriate response headers.</returns>
+ CorsResult EvaluatePolicy(HttpContext context, CorsPolicy policy);
+
+
+ /// <summary>
+ /// Adds CORS-specific response headers to the given <paramref name="response"/>.
+ /// </summary>
+ /// <param name="result">The <see cref="CorsResult"/> used to read the allowed values.</param>
+ /// <param name="response">The <see cref="HttpResponse"/> associated with the current call.</param>
+ void ApplyResult(CorsResult result, HttpResponse response);
+
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IDisableCorsAttribute.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IDisableCorsAttribute.cs
new file mode 100644
index 0000000000..1e69ba3da3
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IDisableCorsAttribute.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.Cors.Infrastructure
+{
+ /// <summary>
+ /// An interface which can be used to identify a type which provides metdata to disable cors for a resource.
+ /// </summary>
+ public interface IDisableCorsAttribute
+ {
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IEnableCorsAttribute.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IEnableCorsAttribute.cs
new file mode 100644
index 0000000000..c58e2a1d96
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/IEnableCorsAttribute.cs
@@ -0,0 +1,16 @@
+// 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.Cors.Infrastructure
+{
+ /// <summary>
+ /// An interface which can be used to identify a type which provides metadata needed for enabling CORS support.
+ /// </summary>
+ public interface IEnableCorsAttribute
+ {
+ /// <summary>
+ /// The name of the policy which needs to be applied.
+ /// </summary>
+ string PolicyName { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/UriHelpers.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/UriHelpers.cs
new file mode 100644
index 0000000000..6c420e9260
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Infrastructure/UriHelpers.cs
@@ -0,0 +1,19 @@
+// 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.Cors.Infrastructure
+{
+ internal static class UriHelpers
+ {
+ public static bool IsSubdomainOf(Uri subdomain, Uri domain)
+ {
+ return subdomain.IsAbsoluteUri
+ && domain.IsAbsoluteUri
+ && subdomain.Scheme == domain.Scheme
+ && subdomain.Port == domain.Port
+ && subdomain.Host.EndsWith($".{domain.Host}", StringComparison.Ordinal);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs
new file mode 100644
index 0000000000..727d19a4ea
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Internal/CORSLoggerExtensions.cs
@@ -0,0 +1,103 @@
+// 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.Cors.Internal
+{
+ internal static class CORSLoggerExtensions
+ {
+ private static readonly Action<ILogger, Exception> _isPreflightRequest;
+ private static readonly Action<ILogger, string, Exception> _requestHasOriginHeader;
+ private static readonly Action<ILogger, Exception> _requestDoesNotHaveOriginHeader;
+ private static readonly Action<ILogger, Exception> _policySuccess;
+ private static readonly Action<ILogger, Exception> _policyFailure;
+ private static readonly Action<ILogger, string, Exception> _originNotAllowed;
+ private static readonly Action<ILogger, string, Exception> _accessControlMethodNotAllowed;
+ private static readonly Action<ILogger, string, Exception> _requestHeaderNotAllowed;
+
+ static CORSLoggerExtensions()
+ {
+ _isPreflightRequest = LoggerMessage.Define(
+ LogLevel.Debug,
+ 1,
+ "The request is a preflight request.");
+
+ _requestHasOriginHeader = LoggerMessage.Define<string>(
+ LogLevel.Debug,
+ 2,
+ "The request has an origin header: '{origin}'.");
+
+ _requestDoesNotHaveOriginHeader = LoggerMessage.Define(
+ LogLevel.Debug,
+ 3,
+ "The request does not have an origin header.");
+
+ _policySuccess = LoggerMessage.Define(
+ LogLevel.Information,
+ 4,
+ "Policy execution successful.");
+
+ _policyFailure = LoggerMessage.Define(
+ LogLevel.Information,
+ 5,
+ "Policy execution failed.");
+
+ _originNotAllowed = LoggerMessage.Define<string>(
+ LogLevel.Information,
+ 6,
+ "Request origin {origin} does not have permission to access the resource.");
+
+ _accessControlMethodNotAllowed = LoggerMessage.Define<string>(
+ LogLevel.Information,
+ 7,
+ "Request method {accessControlRequestMethod} not allowed in CORS policy.");
+
+ _requestHeaderNotAllowed = LoggerMessage.Define<string>(
+ LogLevel.Information,
+ 8,
+ "Request header '{requestHeader}' not allowed in CORS policy.");
+ }
+
+ public static void IsPreflightRequest(this ILogger logger)
+ {
+ _isPreflightRequest(logger, null);
+ }
+
+ public static void RequestHasOriginHeader(this ILogger logger, string origin)
+ {
+ _requestHasOriginHeader(logger, origin, null);
+ }
+
+ public static void RequestDoesNotHaveOriginHeader(this ILogger logger)
+ {
+ _requestDoesNotHaveOriginHeader(logger, null);
+ }
+
+ public static void PolicySuccess(this ILogger logger)
+ {
+ _policySuccess(logger, null);
+ }
+
+ public static void PolicyFailure(this ILogger logger)
+ {
+ _policyFailure(logger, null);
+ }
+
+ public static void OriginNotAllowed(this ILogger logger, string origin)
+ {
+ _originNotAllowed(logger, origin, null);
+ }
+
+ public static void AccessControlMethodNotAllowed(this ILogger logger, string accessControlMethod)
+ {
+ _accessControlMethodNotAllowed(logger, accessControlMethod, null);
+ }
+
+ public static void RequestHeaderNotAllowed(this ILogger logger, string requestHeader)
+ {
+ _requestHeaderNotAllowed(logger, requestHeader, null);
+ }
+ }
+}
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Microsoft.AspNetCore.Cors.csproj b/src/CORS/src/Microsoft.AspNetCore.Cors/Microsoft.AspNetCore.Cors.csproj
new file mode 100644
index 0000000000..10d8530a2e
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Microsoft.AspNetCore.Cors.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>CORS middleware and policy for ASP.NET Core to enable cross-origin resource sharing.
+Commonly used types:
+Microsoft.AspNetCore.Cors.DisableCorsAttribute
+Microsoft.AspNetCore.Cors.EnableCorsAttribute</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;cors</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(MicrosoftAspNetCoreHttpExtensionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="$(MicrosoftExtensionsConfigurationAbstractionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Properties/AssemblyInfo.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..b3d086e2e5
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// 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("Microsoft.AspNetCore.Cors.Test,PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Resources.Designer.cs b/src/CORS/src/Microsoft.AspNetCore.Cors/Resources.Designer.cs
new file mode 100644
index 0000000000..0bc258074c
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Resources.Designer.cs
@@ -0,0 +1,71 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.AspNetCore.Cors {
+ using System;
+ using System.Reflection;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ internal Resources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNetCore.Cors.Resources", typeof(Resources).GetTypeInfo().Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to PreflightMaxAge must be greater than or equal to 0..
+ /// </summary>
+ internal static string PreflightMaxAgeOutOfRange {
+ get {
+ return ResourceManager.GetString("PreflightMaxAgeOutOfRange", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/Resources.resx b/src/CORS/src/Microsoft.AspNetCore.Cors/Resources.resx
new file mode 100644
index 0000000000..6b9ebaad31
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/Resources.resx
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="PreflightMaxAgeOutOfRange" xml:space="preserve">
+ <value>PreflightMaxAge must be greater than or equal to 0.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/CORS/src/Microsoft.AspNetCore.Cors/baseline.netcore.json b/src/CORS/src/Microsoft.AspNetCore.Cors/baseline.netcore.json
new file mode 100644
index 0000000000..ef96cf2eed
--- /dev/null
+++ b/src/CORS/src/Microsoft.AspNetCore.Cors/baseline.netcore.json
@@ -0,0 +1,1250 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Cors, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.Builder.CorsMiddlewareExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseCors",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseCors",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "policyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseCors",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "configurePolicy",
+ "Type": "System.Action<Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.DisableCorsAttribute",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "System.Attribute",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Cors.Infrastructure.IDisableCorsAttribute"
+ ],
+ "Members": [
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.EnableCorsAttribute",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "System.Attribute",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Cors.Infrastructure.IEnableCorsAttribute"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_PolicyName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Cors.Infrastructure.IEnableCorsAttribute",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_PolicyName",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Cors.Infrastructure.IEnableCorsAttribute",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "policyName",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsConstants",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "PreflightHttpMethod",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "Origin",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AnyOrigin",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AccessControlRequestMethod",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AccessControlRequestHeaders",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AccessControlAllowOrigin",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AccessControlAllowHeaders",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AccessControlExposeHeaders",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AccessControlAllowMethods",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AccessControlAllowCredentials",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AccessControlMaxAge",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Invoke",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "next",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ },
+ {
+ "Name": "corsService",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService"
+ },
+ {
+ "Name": "policyProvider",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsPolicyProvider"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "next",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ },
+ {
+ "Name": "corsService",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService"
+ },
+ {
+ "Name": "policyProvider",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsPolicyProvider"
+ },
+ {
+ "Name": "policyName",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "next",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ },
+ {
+ "Name": "corsService",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService"
+ },
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_DefaultPolicyName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_DefaultPolicyName",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddDefaultPolicy",
+ "Parameters": [
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddDefaultPolicy",
+ "Parameters": [
+ {
+ "Name": "configurePolicy",
+ "Type": "System.Action<Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddPolicy",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddPolicy",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configurePolicy",
+ "Type": "System.Action<Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetPolicy",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_AllowAnyHeader",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AllowAnyMethod",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AllowAnyOrigin",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IsOriginAllowed",
+ "Parameters": [],
+ "ReturnType": "System.Func<System.String, System.Boolean>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_IsOriginAllowed",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<System.String, System.Boolean>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ExposedHeaders",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Headers",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Methods",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Origins",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_PreflightMaxAge",
+ "Parameters": [],
+ "ReturnType": "System.Nullable<System.TimeSpan>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_PreflightMaxAge",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Nullable<System.TimeSpan>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SupportsCredentials",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SupportsCredentials",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ToString",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "WithOrigins",
+ "Parameters": [
+ {
+ "Name": "origins",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "WithHeaders",
+ "Parameters": [
+ {
+ "Name": "headers",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "WithExposedHeaders",
+ "Parameters": [
+ {
+ "Name": "exposedHeaders",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "WithMethods",
+ "Parameters": [
+ {
+ "Name": "methods",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AllowCredentials",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "DisallowCredentials",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AllowAnyOrigin",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AllowAnyMethod",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AllowAnyHeader",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SetPreflightMaxAge",
+ "Parameters": [
+ {
+ "Name": "preflightMaxAge",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SetIsOriginAllowed",
+ "Parameters": [
+ {
+ "Name": "isOriginAllowed",
+ "Type": "System.Func<System.String, System.Boolean>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SetIsOriginAllowedToAllowWildcardSubdomains",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Build",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "origins",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_AllowedOrigin",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AllowedOrigin",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SupportsCredentials",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SupportsCredentials",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AllowedMethods",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AllowedHeaders",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AllowedExposedHeaders",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_VaryByOrigin",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_VaryByOrigin",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_PreflightMaxAge",
+ "Parameters": [],
+ "ReturnType": "System.Nullable<System.TimeSpan>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_PreflightMaxAge",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Nullable<System.TimeSpan>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ToString",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.Infrastructure.CorsService",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "EvaluatePolicy",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "policyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "EvaluatePolicy",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "EvaluateRequest",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy"
+ },
+ {
+ "Name": "result",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "EvaluatePreflightRequest",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy"
+ },
+ {
+ "Name": "result",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ApplyResult",
+ "Parameters": [
+ {
+ "Name": "result",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult"
+ },
+ {
+ "Name": "response",
+ "Type": "Microsoft.AspNetCore.Http.HttpResponse"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Cors.Infrastructure.CorsOptions>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Cors.Infrastructure.CorsOptions>"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.Infrastructure.DefaultCorsPolicyProvider",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Cors.Infrastructure.ICorsPolicyProvider"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetPolicyAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "policyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsPolicyProvider",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Cors.Infrastructure.CorsOptions>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsPolicyProvider",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetPolicyAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "policyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy>",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.Infrastructure.ICorsService",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "EvaluatePolicy",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ApplyResult",
+ "Parameters": [
+ {
+ "Name": "result",
+ "Type": "Microsoft.AspNetCore.Cors.Infrastructure.CorsResult"
+ },
+ {
+ "Name": "response",
+ "Type": "Microsoft.AspNetCore.Http.HttpResponse"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.Infrastructure.IDisableCorsAttribute",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Cors.Infrastructure.IEnableCorsAttribute",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_PolicyName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_PolicyName",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.CorsServiceCollectionExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddCors",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddCors",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "setupAction",
+ "Type": "System.Action<Microsoft.AspNetCore.Cors.Infrastructure.CorsOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/CORS/test/Directory.Build.props b/src/CORS/test/Directory.Build.props
new file mode 100644
index 0000000000..4244624987
--- /dev/null
+++ b/src/CORS/test/Directory.Build.props
@@ -0,0 +1,15 @@
+<Project>
+ <Import Project="..\Directory.Build.props" />
+
+ <PropertyGroup>
+ <DeveloperBuildTestTfms>netcoreapp2.1</DeveloperBuildTestTfms>
+ <StandardTestTfms>$(DeveloperBuildTestTfms)</StandardTestTfms>
+ <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' ">$(StandardTestTfms);netcoreapp2.0</StandardTestTfms>
+ <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' AND '$(OS)' == 'Windows_NT' ">$(StandardTestTfms);net461</StandardTestTfms>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareFunctionalTest.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareFunctionalTest.cs
new file mode 100644
index 0000000000..008a4926d9
--- /dev/null
+++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareFunctionalTest.cs
@@ -0,0 +1,98 @@
+// 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.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ public class CorsMiddlewareFunctionalTests : IClassFixture<CorsTestFixture<CorsMiddlewareWebSite.Startup>>
+ {
+ public CorsMiddlewareFunctionalTests(CorsTestFixture<CorsMiddlewareWebSite.Startup> fixture)
+ {
+ Client = fixture.Client;
+ }
+
+ public HttpClient Client { get; }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("HEAD")]
+ [InlineData("POST")]
+ public async Task ResourceWithSimpleRequestPolicy_Allows_SimpleRequests(string method)
+ {
+ // Arrange
+ var path = "/CorsMiddleware/EC6AA70D-BA3E-4B71-A87F-18625ADDB2BD";
+ var origin = "http://example.com";
+ var request = new HttpRequestMessage(new HttpMethod(method), path);
+ request.Headers.Add(CorsConstants.Origin, origin);
+
+ // Act
+ var response = await Client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ Assert.Equal(path, content);
+ var responseHeaders = response.Headers;
+ var header = Assert.Single(response.Headers);
+ Assert.Equal(CorsConstants.AccessControlAllowOrigin, header.Key);
+ Assert.Equal(new[] { "http://example.com" }, header.Value.ToArray());
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("HEAD")]
+ [InlineData("POST")]
+ [InlineData("PUT")]
+ public async Task PolicyFailed_Disallows_PreFlightRequest(string method)
+ {
+ // Arrange
+ var path = "/CorsMiddleware/9B8BB9C6-5BF2-4255-A636-DCB450D51AAE";
+ var request = new HttpRequestMessage(new HttpMethod(CorsConstants.PreflightHttpMethod), path);
+
+ // Adding a custom header makes it a non-simple request.
+ request.Headers.Add(CorsConstants.Origin, "http://example.com");
+ request.Headers.Add(CorsConstants.AccessControlRequestMethod, method);
+ request.Headers.Add(CorsConstants.AccessControlRequestHeaders, "Custom");
+
+ // Act
+ var response = await Client.SendAsync(request);
+
+ // Assert
+ // Middleware applied the policy and since that did not pass, there were no access control headers.
+ Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
+ Assert.Empty(response.Headers);
+
+ // It should short circuit and hence no result.
+ var content = await response.Content.ReadAsStringAsync();
+ Assert.Equal(string.Empty, content);
+ }
+
+ [Fact]
+ public async Task PolicyFailed_Allows_ActualRequest_WithMissingResponseHeaders()
+ {
+ // Arrange
+ var path = "/CorsMiddleware/1E6C6F4D-1E1C-450E-8BD0-73DBF089A78F";
+ var request = new HttpRequestMessage(HttpMethod.Put, path);
+
+ // Adding a custom header makes it a non simple request.
+ request.Headers.Add(CorsConstants.Origin, "http://example2.com");
+
+ // Act
+ var response = await Client.SendAsync(request);
+
+ // Assert
+ // Middleware applied the policy and since that did not pass, there were no access control headers.
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Empty(response.Headers);
+
+ // It still has executed the action.
+ var content = await response.Content.ReadAsStringAsync();
+ Assert.Equal(path, content);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareTests.cs
new file mode 100644
index 0000000000..d74c020eae
--- /dev/null
+++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsMiddlewareTests.cs
@@ -0,0 +1,353 @@
+// 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.Net;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ public class CorsMiddlewareTests
+ {
+ [Theory]
+ [InlineData("PuT")]
+ [InlineData("PUT")]
+ public async Task CorsRequest_MatchesPolicy_OnCaseInsensitiveAccessControlRequestMethod(string accessControlRequestMethod)
+ {
+ // Arrange
+ var hostBuilder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseCors(builder =>
+ builder.WithOrigins("http://localhost:5001")
+ .WithMethods("PUT"));
+ app.Run(async context =>
+ {
+ await context.Response.WriteAsync("Cross origin response");
+ });
+ })
+ .ConfigureServices(services => services.AddCors());
+
+ using (var server = new TestServer(hostBuilder))
+ {
+ // Act
+ // Actual request.
+ var response = await server.CreateRequest("/")
+ .AddHeader(CorsConstants.Origin, "http://localhost:5001")
+ .SendAsync(accessControlRequestMethod);
+
+ // Assert
+ response.EnsureSuccessStatusCode();
+ Assert.Single(response.Headers);
+ Assert.Equal("Cross origin response", await response.Content.ReadAsStringAsync());
+ Assert.Equal("http://localhost:5001", response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault());
+ }
+ }
+
+ [Fact]
+ public async Task CorsRequest_MatchPolicy_SetsResponseHeaders()
+ {
+ // Arrange
+ var hostBuilder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseCors(builder =>
+ builder.WithOrigins("http://localhost:5001")
+ .WithMethods("PUT")
+ .WithHeaders("Header1")
+ .WithExposedHeaders("AllowedHeader"));
+ app.Run(async context =>
+ {
+ await context.Response.WriteAsync("Cross origin response");
+ });
+ })
+ .ConfigureServices(services => services.AddCors());
+
+ using (var server = new TestServer(hostBuilder))
+ {
+ // Act
+ // Actual request.
+ var response = await server.CreateRequest("/")
+ .AddHeader(CorsConstants.Origin, "http://localhost:5001")
+ .SendAsync("PUT");
+
+ // Assert
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(2, response.Headers.Count());
+ Assert.Equal("Cross origin response", await response.Content.ReadAsStringAsync());
+ Assert.Equal("http://localhost:5001", response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault());
+ Assert.Equal("AllowedHeader", response.Headers.GetValues(CorsConstants.AccessControlExposeHeaders).FirstOrDefault());
+ }
+ }
+
+ [Theory]
+ [InlineData("OpTions")]
+ [InlineData("OPTIONS")]
+ public async Task PreFlight_MatchesPolicy_OnCaseInsensitiveOptionsMethod(string preflightMethod)
+ {
+ // Arrange
+ var policy = new CorsPolicy();
+ policy.Origins.Add("http://localhost:5001");
+ policy.Methods.Add("PUT");
+
+ var hostBuilder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseCors("customPolicy");
+ app.Run(async context =>
+ {
+ await context.Response.WriteAsync("Cross origin response");
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddCors(options =>
+ {
+ options.AddPolicy("customPolicy", policy);
+ });
+ });
+
+ using (var server = new TestServer(hostBuilder))
+ {
+ // Act
+ // Preflight request.
+ var response = await server.CreateRequest("/")
+ .AddHeader(CorsConstants.Origin, "http://localhost:5001")
+ .SendAsync(preflightMethod);
+
+ // Assert
+ response.EnsureSuccessStatusCode();
+ Assert.Single(response.Headers);
+ Assert.Equal("http://localhost:5001", response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault());
+ }
+ }
+
+ [Fact]
+ public async Task PreFlight_MatchesPolicy_SetsResponseHeaders()
+ {
+ // Arrange
+ var policy = new CorsPolicy();
+ policy.Origins.Add("http://localhost:5001");
+ policy.Methods.Add("PUT");
+ policy.Headers.Add("Header1");
+ policy.ExposedHeaders.Add("AllowedHeader");
+
+ var hostBuilder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseCors("customPolicy");
+ app.Run(async context =>
+ {
+ await context.Response.WriteAsync("Cross origin response");
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddCors(options =>
+ {
+ options.AddPolicy("customPolicy", policy);
+ });
+ });
+
+ using (var server = new TestServer(hostBuilder))
+ {
+ // Act
+ // Preflight request.
+ var response = await server.CreateRequest("/")
+ .AddHeader(CorsConstants.Origin, "http://localhost:5001")
+ .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT")
+ .SendAsync(CorsConstants.PreflightHttpMethod);
+
+ // Assert
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(2, response.Headers.Count());
+ Assert.Equal("http://localhost:5001", response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault());
+ Assert.Equal("PUT", response.Headers.GetValues(CorsConstants.AccessControlAllowMethods).FirstOrDefault());
+ }
+ }
+
+ [Fact]
+ public async Task PreFlightRequest_DoesNotMatchPolicy_DoesNotSetHeaders()
+ {
+ // Arrange
+ var hostBuilder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseCors(builder =>
+ builder.WithOrigins("http://localhost:5001")
+ .WithMethods("PUT")
+ .WithHeaders("Header1")
+ .WithExposedHeaders("AllowedHeader"));
+ app.Run(async context =>
+ {
+ await context.Response.WriteAsync("Cross origin response");
+ });
+ })
+ .ConfigureServices(services => services.AddCors());
+
+ using (var server = new TestServer(hostBuilder))
+ {
+ // Act
+ // Preflight request.
+ var response = await server.CreateRequest("/")
+ .AddHeader(CorsConstants.Origin, "http://localhost:5002")
+ .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT")
+ .SendAsync(CorsConstants.PreflightHttpMethod);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
+ Assert.Empty(response.Headers);
+ }
+ }
+
+ [Fact]
+ public async Task CorsRequest_DoesNotMatchPolicy_DoesNotSetHeaders()
+ {
+ // Arrange
+ var hostBuilder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseCors(builder =>
+ builder.WithOrigins("http://localhost:5001")
+ .WithMethods("PUT")
+ .WithHeaders("Header1")
+ .WithExposedHeaders("AllowedHeader"));
+ app.Run(async context =>
+ {
+ await context.Response.WriteAsync("Cross origin response");
+ });
+ })
+ .ConfigureServices(services => services.AddCors());
+
+ using (var server = new TestServer(hostBuilder))
+ {
+ // Act
+ // Actual request.
+ var response = await server.CreateRequest("/")
+ .AddHeader(CorsConstants.Origin, "http://localhost:5002")
+ .SendAsync("PUT");
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Empty(response.Headers);
+ }
+ }
+
+ [Fact]
+ public async Task Uses_PolicyProvider_AsFallback()
+ {
+ // Arrange
+ var corsService = Mock.Of<ICorsService>();
+ var mockProvider = new Mock<ICorsPolicyProvider>();
+ mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
+ .Returns(Task.FromResult<CorsPolicy>(null))
+ .Verifiable();
+
+ var middleware = new CorsMiddleware(
+ Mock.Of<RequestDelegate>(),
+ corsService,
+ mockProvider.Object,
+ policyName: null);
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" });
+
+ // Act
+ await middleware.Invoke(httpContext);
+
+ // Assert
+ mockProvider.Verify(
+ o => o.GetPolicyAsync(It.IsAny<HttpContext>(), It.IsAny<string>()),
+ Times.Once);
+ }
+
+ [Fact]
+ public async Task DoesNotSetHeaders_ForNoPolicy()
+ {
+ // Arrange
+ var corsService = Mock.Of<ICorsService>();
+ var mockProvider = new Mock<ICorsPolicyProvider>();
+ mockProvider.Setup(o => o.GetPolicyAsync(It.IsAny<HttpContext>(), It.IsAny<string>()))
+ .Returns(Task.FromResult<CorsPolicy>(null))
+ .Verifiable();
+
+ var middleware = new CorsMiddleware(
+ Mock.Of<RequestDelegate>(),
+ corsService,
+ mockProvider.Object,
+ policyName: null);
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { "http://example.com" });
+
+ // Act
+ await middleware.Invoke(httpContext);
+
+ // Assert
+ Assert.Equal(200, httpContext.Response.StatusCode);
+ Assert.Empty(httpContext.Response.Headers);
+ mockProvider.Verify(
+ o => o.GetPolicyAsync(It.IsAny<HttpContext>(), It.IsAny<string>()),
+ Times.Once);
+ }
+
+ [Fact]
+ public async Task PreFlight_MatchesDefaultPolicy_SetsResponseHeaders()
+ {
+ // Arrange
+ var hostBuilder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseCors();
+ app.Run(async context =>
+ {
+ await context.Response.WriteAsync("Cross origin response");
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddCors(options =>
+ {
+ options.AddDefaultPolicy(policyBuilder =>
+ {
+ policyBuilder
+ .WithOrigins("http://localhost:5001")
+ .WithMethods("PUT")
+ .WithHeaders("Header1")
+ .WithExposedHeaders("AllowedHeader")
+ .Build();
+ });
+ options.AddPolicy("policy2", policyBuilder =>
+ {
+ policyBuilder
+ .WithOrigins("http://localhost:5002")
+ .Build();
+ });
+ });
+ });
+
+ using (var server = new TestServer(hostBuilder))
+ {
+ // Act
+ // Preflight request.
+ var response = await server.CreateRequest("/")
+ .AddHeader(CorsConstants.Origin, "http://localhost:5001")
+ .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT")
+ .SendAsync(CorsConstants.PreflightHttpMethod);
+
+ // Assert
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(2, response.Headers.Count());
+ Assert.Equal("http://localhost:5001", response.Headers.GetValues(CorsConstants.AccessControlAllowOrigin).FirstOrDefault());
+ Assert.Equal("PUT", response.Headers.GetValues(CorsConstants.AccessControlAllowMethods).FirstOrDefault());
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsOptionsTest.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsOptionsTest.cs
new file mode 100644
index 0000000000..360231d38b
--- /dev/null
+++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsOptionsTest.cs
@@ -0,0 +1,67 @@
+// 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.Cors.Infrastructure
+{
+ public class CorsOptionsTest
+ {
+ [Fact]
+ public void AddDefaultPolicy_SetsDefaultPolicyName()
+ {
+ // Arrange
+ var corsOptions = new CorsOptions();
+ var expectedPolicy = new CorsPolicy();
+
+ // Act
+ corsOptions.AddPolicy("policy1", new CorsPolicy());
+ corsOptions.AddDefaultPolicy(expectedPolicy);
+ corsOptions.AddPolicy("policy3", new CorsPolicy());
+
+ // Assert
+ var actualPolicy = corsOptions.GetPolicy(corsOptions.DefaultPolicyName);
+ Assert.Same(expectedPolicy, actualPolicy);
+ }
+
+ [Fact]
+ public void AddDefaultPolicy_OverridesDefaultPolicyName()
+ {
+ // Arrange
+ var corsOptions = new CorsOptions();
+ var expectedPolicy = new CorsPolicy();
+
+ // Act
+ corsOptions.AddDefaultPolicy(new CorsPolicy());
+ corsOptions.AddDefaultPolicy(expectedPolicy);
+
+ // Assert
+ var actualPolicy = corsOptions.GetPolicy(corsOptions.DefaultPolicyName);
+ Assert.Same(expectedPolicy, actualPolicy);
+ }
+
+ [Fact]
+ public void AddDefaultPolicy_UsingPolicyBuilder_SetsDefaultPolicyName()
+ {
+ // Arrange
+ var corsOptions = new CorsOptions();
+ CorsPolicy expectedPolicy = null;
+
+ // Act
+ corsOptions.AddPolicy("policy1", policyBuilder =>
+ {
+ policyBuilder.AllowAnyOrigin().Build();
+ });
+ corsOptions.AddDefaultPolicy(policyBuilder =>
+ {
+ expectedPolicy = policyBuilder.AllowAnyOrigin().Build();
+ });
+ corsOptions.AddPolicy("policy3", new CorsPolicy());
+
+ // Assert
+ var actualPolicy = corsOptions.GetPolicy(corsOptions.DefaultPolicyName);
+ Assert.Same(expectedPolicy, actualPolicy);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyBuilderTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyBuilderTests.cs
new file mode 100644
index 0000000000..8a223ad225
--- /dev/null
+++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyBuilderTests.cs
@@ -0,0 +1,292 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ public class CorsPolicyBuilderTests
+ {
+ [Fact]
+ public void Constructor_WithPolicy_AddsTheGivenPolicy()
+ {
+ // Arrange
+ Func<string, bool> isOriginAllowed = origin => true;
+ var originalPolicy = new CorsPolicy();
+ originalPolicy.Origins.Add("http://existing.com");
+ originalPolicy.Headers.Add("Existing");
+ originalPolicy.Methods.Add("GET");
+ originalPolicy.ExposedHeaders.Add("ExistingExposed");
+ originalPolicy.SupportsCredentials = true;
+ originalPolicy.PreflightMaxAge = TimeSpan.FromSeconds(12);
+ originalPolicy.IsOriginAllowed = isOriginAllowed;
+
+ // Act
+ var builder = new CorsPolicyBuilder(originalPolicy);
+
+ // Assert
+ var corsPolicy = builder.Build();
+
+ Assert.False(corsPolicy.AllowAnyHeader);
+ Assert.False(corsPolicy.AllowAnyMethod);
+ Assert.False(corsPolicy.AllowAnyOrigin);
+ Assert.True(corsPolicy.SupportsCredentials);
+ Assert.NotSame(originalPolicy.Headers, corsPolicy.Headers);
+ Assert.Equal(originalPolicy.Headers, corsPolicy.Headers);
+ Assert.NotSame(originalPolicy.Methods, corsPolicy.Methods);
+ Assert.Equal(originalPolicy.Methods, corsPolicy.Methods);
+ Assert.NotSame(originalPolicy.Origins, corsPolicy.Origins);
+ Assert.Equal(originalPolicy.Origins, corsPolicy.Origins);
+ Assert.NotSame(originalPolicy.ExposedHeaders, corsPolicy.ExposedHeaders);
+ Assert.Equal(originalPolicy.ExposedHeaders, corsPolicy.ExposedHeaders);
+ Assert.Equal(TimeSpan.FromSeconds(12), corsPolicy.PreflightMaxAge);
+ Assert.Same(originalPolicy.IsOriginAllowed, corsPolicy.IsOriginAllowed);
+ }
+
+ [Fact]
+ public void ConstructorWithPolicy_HavingNullPreflightMaxAge_AddsTheGivenPolicy()
+ {
+ // Arrange
+ var originalPolicy = new CorsPolicy();
+ originalPolicy.Origins.Add("http://existing.com");
+
+ // Act
+ var builder = new CorsPolicyBuilder(originalPolicy);
+
+ // Assert
+ var corsPolicy = builder.Build();
+
+ Assert.Null(corsPolicy.PreflightMaxAge);
+ Assert.False(corsPolicy.AllowAnyHeader);
+ Assert.False(corsPolicy.AllowAnyMethod);
+ Assert.False(corsPolicy.AllowAnyOrigin);
+ Assert.NotSame(originalPolicy.Origins, corsPolicy.Origins);
+ Assert.Equal(originalPolicy.Origins, corsPolicy.Origins);
+ Assert.Empty(corsPolicy.Headers);
+ Assert.Empty(corsPolicy.Methods);
+ Assert.Empty(corsPolicy.ExposedHeaders);
+ }
+
+ [Fact]
+ public void Constructor_WithNoOrigin()
+ {
+ // Arrange & Act
+ var builder = new CorsPolicyBuilder();
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.False(corsPolicy.AllowAnyHeader);
+ Assert.False(corsPolicy.AllowAnyMethod);
+ Assert.False(corsPolicy.AllowAnyOrigin);
+ Assert.False(corsPolicy.SupportsCredentials);
+ Assert.Empty(corsPolicy.ExposedHeaders);
+ Assert.Empty(corsPolicy.Headers);
+ Assert.Empty(corsPolicy.Methods);
+ Assert.Empty(corsPolicy.Origins);
+ Assert.Null(corsPolicy.PreflightMaxAge);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("http://example.com,http://example2.com")]
+ public void Constructor_WithParamsOrigin_InitializesOrigin(string origin)
+ {
+ // Arrange
+ var origins = origin.Split(',');
+
+ // Act
+ var builder = new CorsPolicyBuilder(origins);
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.False(corsPolicy.AllowAnyHeader);
+ Assert.False(corsPolicy.AllowAnyMethod);
+ Assert.False(corsPolicy.AllowAnyOrigin);
+ Assert.False(corsPolicy.SupportsCredentials);
+ Assert.Empty(corsPolicy.ExposedHeaders);
+ Assert.Empty(corsPolicy.Headers);
+ Assert.Empty(corsPolicy.Methods);
+ Assert.Equal(origins.ToList(), corsPolicy.Origins);
+ Assert.Null(corsPolicy.PreflightMaxAge);
+ }
+
+ [Fact]
+ public void WithOrigins_AddsOrigins()
+ {
+ // Arrange
+ var builder = new CorsPolicyBuilder();
+
+ // Act
+ builder.WithOrigins("http://example.com", "http://example2.com");
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.False(corsPolicy.AllowAnyOrigin);
+ Assert.Equal(new List<string>() { "http://example.com", "http://example2.com" }, corsPolicy.Origins);
+ }
+
+ [Fact]
+ public void AllowAnyOrigin_AllowsAny()
+ {
+ // Arrange
+ var builder = new CorsPolicyBuilder();
+
+ // Act
+ builder.AllowAnyOrigin();
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.True(corsPolicy.AllowAnyOrigin);
+ Assert.Equal(new List<string>() { "*" }, corsPolicy.Origins);
+ }
+
+ [Fact]
+ public void SetIsOriginAllowed_AddsIsOriginAllowed()
+ {
+ // Arrange
+ var builder = new CorsPolicyBuilder();
+ Func<string, bool> isOriginAllowed = origin => true;
+
+ // Act
+ builder.SetIsOriginAllowed(isOriginAllowed);
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.Same(corsPolicy.IsOriginAllowed, isOriginAllowed);
+ }
+
+ [Fact]
+ public void SetIsOriginAllowedToAllowWildcardSubdomains_AllowsWildcardSubdomains()
+ {
+ // Arrange
+ var builder = new CorsPolicyBuilder("http://*.example.com");
+
+ // Act
+ builder.SetIsOriginAllowedToAllowWildcardSubdomains();
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.True(corsPolicy.IsOriginAllowed("http://test.example.com"));
+ }
+
+ [Fact]
+ public void WithMethods_AddsMethods()
+ {
+ // Arrange
+ var builder = new CorsPolicyBuilder();
+
+ // Act
+ builder.WithMethods("PUT", "GET");
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.False(corsPolicy.AllowAnyOrigin);
+ Assert.Equal(new List<string>() { "PUT", "GET" }, corsPolicy.Methods);
+ }
+
+ [Fact]
+ public void AllowAnyMethod_AllowsAny()
+ {
+ // Arrange
+ var builder = new CorsPolicyBuilder();
+
+ // Act
+ builder.AllowAnyMethod();
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.True(corsPolicy.AllowAnyMethod);
+ Assert.Equal(new List<string>() { "*" }, corsPolicy.Methods);
+ }
+
+ [Fact]
+ public void WithHeaders_AddsHeaders()
+ {
+ // Arrange
+ var builder = new CorsPolicyBuilder();
+
+ // Act
+ builder.WithHeaders("example1", "example2");
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.False(corsPolicy.AllowAnyHeader);
+ Assert.Equal(new List<string>() { "example1", "example2" }, corsPolicy.Headers);
+ }
+
+ [Fact]
+ public void AllowAnyHeaders_AllowsAny()
+ {
+ // Arrange
+ var builder = new CorsPolicyBuilder();
+
+ // Act
+ builder.AllowAnyHeader();
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.True(corsPolicy.AllowAnyHeader);
+ Assert.Equal(new List<string>() { "*" }, corsPolicy.Headers);
+ }
+
+ [Fact]
+ public void WithExposedHeaders_AddsExposedHeaders()
+ {
+ // Arrange
+ var builder = new CorsPolicyBuilder();
+
+ // Act
+ builder.WithExposedHeaders("exposed1", "exposed2");
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.Equal(new List<string>() { "exposed1", "exposed2" }, corsPolicy.ExposedHeaders);
+ }
+
+ [Fact]
+ public void SetPreFlightMaxAge_SetsThePreFlightAge()
+ {
+ // Arrange
+ var builder = new CorsPolicyBuilder();
+
+ // Act
+ builder.SetPreflightMaxAge(TimeSpan.FromSeconds(12));
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.Equal(TimeSpan.FromSeconds(12), corsPolicy.PreflightMaxAge);
+ }
+
+ [Fact]
+ public void AllowCredential_SetsSupportsCredentials_ToTrue()
+ {
+ // Arrange
+ var builder = new CorsPolicyBuilder();
+
+ // Act
+ builder.AllowCredentials();
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.True(corsPolicy.SupportsCredentials);
+ }
+
+
+ [Fact]
+ public void DisallowCredential_SetsSupportsCredentials_ToFalse()
+ {
+ // Arrange
+ var builder = new CorsPolicyBuilder();
+
+ // Act
+ builder.DisallowCredentials();
+
+ // Assert
+ var corsPolicy = builder.Build();
+ Assert.False(corsPolicy.SupportsCredentials);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyExtensionsTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyExtensionsTests.cs
new file mode 100644
index 0000000000..74dd67db0b
--- /dev/null
+++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyExtensionsTests.cs
@@ -0,0 +1,85 @@
+// 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.Cors.Infrastructure
+{
+ public sealed class CorsPolicyExtensionsTest
+ {
+ [Fact]
+ public void IsOriginAnAllowedSubdomain_ReturnsTrueIfPolicyContainsOrigin()
+ {
+ // Arrange
+ const string origin = "http://sub.domain";
+ var policy = new CorsPolicy();
+ policy.Origins.Add(origin);
+
+ // Act
+ var actual = policy.IsOriginAnAllowedSubdomain(origin);
+
+ // Assert
+ Assert.True(actual);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("null")]
+ [InlineData("http://")]
+ [InlineData("http://*")]
+ [InlineData("http://.domain")]
+ [InlineData("http://.domain/hello")]
+ public void IsOriginAnAllowedSubdomain_ReturnsFalseIfOriginIsMalformedUri(string malformedOrigin)
+ {
+ // Arrange
+ var policy = new CorsPolicy();
+ policy.Origins.Add("http://*.domain");
+
+ // Act
+ var actual = policy.IsOriginAnAllowedSubdomain(malformedOrigin);
+
+ // Assert
+ Assert.False(actual);
+ }
+
+ [Theory]
+ [InlineData("http://sub.domain", "http://*.domain")]
+ [InlineData("http://sub.sub.domain", "http://*.domain")]
+ [InlineData("http://sub.sub.domain", "http://*.sub.domain")]
+ [InlineData("http://sub.domain:4567", "http://*.domain:4567")]
+ public void IsOriginAnAllowedSubdomain_ReturnsTrue_WhenASubdomain(string origin, string allowedOrigin)
+ {
+ // Arrange
+ var policy = new CorsPolicy();
+ policy.Origins.Add(allowedOrigin);
+
+ // Act
+ var isAllowed = policy.IsOriginAnAllowedSubdomain(origin);
+
+ // Assert
+ Assert.True(isAllowed);
+ }
+
+ [Theory]
+ [InlineData("http://domain", "http://*.domain")]
+ [InlineData("http://sub.domain", "http://domain")]
+ [InlineData("http://sub.domain:1234", "http://*.domain:5678")]
+ [InlineData("http://sub.domain", "http://domain.*")]
+ [InlineData("http://sub.sub.domain", "http://sub.*.domain")]
+ [InlineData("http://sub.domain.hacker", "http://*.domain")]
+ [InlineData("https://sub.domain", "http://*.domain")]
+ public void IsOriginAnAllowedSubdomain_ReturnsFalse_WhenNotASubdomain(string origin, string allowedOrigin)
+ {
+ // Arrange
+ var policy = new CorsPolicy();
+ policy.Origins.Add(allowedOrigin);
+
+ // Act
+ var isAllowed = policy.IsOriginAnAllowedSubdomain(origin);
+
+ // Assert
+ Assert.False(isAllowed);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyTests.cs
new file mode 100644
index 0000000000..f49e30fabc
--- /dev/null
+++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyTests.cs
@@ -0,0 +1,74 @@
+// 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.Cors.Infrastructure
+{
+ public class CorsPolicyTest
+ {
+ [Fact]
+ public void Default_Constructor()
+ {
+ // Arrange & Act
+ var corsPolicy = new CorsPolicy();
+
+ // Assert
+ Assert.False(corsPolicy.AllowAnyHeader);
+ Assert.False(corsPolicy.AllowAnyMethod);
+ Assert.False(corsPolicy.AllowAnyOrigin);
+ Assert.False(corsPolicy.SupportsCredentials);
+ Assert.Empty(corsPolicy.ExposedHeaders);
+ Assert.Empty(corsPolicy.Headers);
+ Assert.Empty(corsPolicy.Methods);
+ Assert.Empty(corsPolicy.Origins);
+ Assert.Null(corsPolicy.PreflightMaxAge);
+ Assert.NotNull(corsPolicy.IsOriginAllowed);
+ }
+
+ [Fact]
+ public void SettingNegativePreflightMaxAge_Throws()
+ {
+ // Arrange
+ var policy = new CorsPolicy();
+
+ // Act
+ var exception = Assert.Throws<ArgumentOutOfRangeException>(() =>
+ {
+ policy.PreflightMaxAge = TimeSpan.FromSeconds(-12);
+ });
+
+ // Assert
+ Assert.Equal(
+ $"PreflightMaxAge must be greater than or equal to 0.{Environment.NewLine}Parameter name: value",
+ exception.Message);
+ }
+
+ [Fact]
+ public void ToString_ReturnsThePropertyValues()
+ {
+ // Arrange
+ var corsPolicy = new CorsPolicy
+ {
+ PreflightMaxAge = TimeSpan.FromSeconds(12),
+ SupportsCredentials = true
+ };
+ corsPolicy.Headers.Add("foo");
+ corsPolicy.Headers.Add("bar");
+ corsPolicy.Origins.Add("http://example.com");
+ corsPolicy.Origins.Add("http://example.org");
+ corsPolicy.Methods.Add("GET");
+
+ // Act
+ var policyString = corsPolicy.ToString();
+
+ // Assert
+ Assert.Equal(
+ @"AllowAnyHeader: False, AllowAnyMethod: False, AllowAnyOrigin: False, PreflightMaxAge: 12,"+
+ " SupportsCredentials: True, Origins: {http://example.com,http://example.org}, Methods: {GET},"+
+ " Headers: {foo,bar}, ExposedHeaders: {}",
+ policyString);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsResultTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsResultTests.cs
new file mode 100644
index 0000000000..bc6e4a2d3a
--- /dev/null
+++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsResultTests.cs
@@ -0,0 +1,69 @@
+// 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.Cors.Infrastructure
+{
+ public class CorsResultTest
+ {
+ [Fact]
+ public void Default_Constructor()
+ {
+ // Arrange & Act
+ var result = new CorsResult();
+
+ // Assert
+ Assert.Empty(result.AllowedHeaders);
+ Assert.Empty(result.AllowedExposedHeaders);
+ Assert.Empty(result.AllowedMethods);
+ Assert.False(result.SupportsCredentials);
+ Assert.Null(result.AllowedOrigin);
+ Assert.Null(result.PreflightMaxAge);
+ }
+
+ [Fact]
+ public void SettingNegativePreflightMaxAge_Throws()
+ {
+ // Arrange
+ var result = new CorsResult();
+
+ // Act
+ var exception = Assert.Throws<ArgumentOutOfRangeException>(() =>
+ {
+ result.PreflightMaxAge = TimeSpan.FromSeconds(-1);
+ });
+
+ // Assert
+ Assert.Equal(
+ $"PreflightMaxAge must be greater than or equal to 0.{Environment.NewLine}Parameter name: value",
+ exception.Message);
+ }
+
+ [Fact]
+ public void ToString_ReturnsThePropertyValues()
+ {
+ // Arrange
+ var corsResult = new CorsResult
+ {
+ SupportsCredentials = true,
+ PreflightMaxAge = TimeSpan.FromSeconds(30),
+ AllowedOrigin = "*"
+ };
+ corsResult.AllowedExposedHeaders.Add("foo");
+ corsResult.AllowedHeaders.Add("bar");
+ corsResult.AllowedHeaders.Add("baz");
+ corsResult.AllowedMethods.Add("GET");
+
+ // Act
+ var result = corsResult.ToString();
+
+ // Assert
+ Assert.Equal(
+ @"AllowCredentials: True, PreflightMaxAge: 30, AllowOrigin: *," +
+ " AllowExposedHeaders: {foo}, AllowHeaders: {bar,baz}, AllowMethods: {GET}",
+ result);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs
new file mode 100644
index 0000000000..8a71ce7b42
--- /dev/null
+++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsServiceTests.cs
@@ -0,0 +1,1167 @@
+// 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 Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging.Testing;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ public class CorsServiceTests
+ {
+ [Fact]
+ public void EvaluatePolicy_NoOrigin_ReturnsInvalidResult()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext("GET", origin: null);
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, new CorsPolicy());
+
+ // Assert
+ Assert.Null(result.AllowedOrigin);
+ Assert.False(result.VaryByOrigin);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_NoMatchingOrigin_ReturnsInvalidResult()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(origin: "http://example.com");
+ var policy = new CorsPolicy();
+ policy.Origins.Add("bar");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Null(result.AllowedOrigin);
+ Assert.False(result.VaryByOrigin);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_EmptyOriginsPolicy_ReturnsInvalidResult()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(origin: "http://example.com");
+ var policy = new CorsPolicy();
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Null(result.AllowedOrigin);
+ Assert.False(result.VaryByOrigin);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_IsOriginAllowedReturnsFalse_ReturnsInvalidResult()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(origin: "http://example.com");
+ var policy = new CorsPolicy()
+ {
+ IsOriginAllowed = origin => false
+ };
+ policy.Origins.Add("example.com");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Null(result.AllowedOrigin);
+ Assert.False(result.VaryByOrigin);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_AllowAnyOrigin_DoesNotSupportCredentials_EmitsWildcardForOrigin()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(origin: "http://example.com");
+
+ var policy = new CorsPolicy
+ {
+ SupportsCredentials = false
+ };
+
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Equal("*", result.AllowedOrigin);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_AllowAnyOrigin_SupportsCredentials_AddsSpecificOrigin()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(origin: "http://example.com");
+ var policy = new CorsPolicy
+ {
+ SupportsCredentials = true
+ };
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Equal("http://example.com", result.AllowedOrigin);
+ Assert.True(result.VaryByOrigin);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_DoesNotSupportCredentials_AllowCredentialsReturnsFalse()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(origin: "http://example.com");
+ var policy = new CorsPolicy
+ {
+ SupportsCredentials = false
+ };
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.False(result.SupportsCredentials);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_SupportsCredentials_AllowCredentialsReturnsTrue()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(origin: "http://example.com");
+ var policy = new CorsPolicy
+ {
+ SupportsCredentials = true
+ };
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.True(result.SupportsCredentials);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_NoExposedHeaders_NoAllowExposedHeaders()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(origin: "http://example.com");
+ var policy = new CorsPolicy();
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Empty(result.AllowedExposedHeaders);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_OneExposedHeaders_HeadersAllowed()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(origin: "http://example.com");
+ var policy = new CorsPolicy();
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.ExposedHeaders.Add("foo");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Equal(1, result.AllowedExposedHeaders.Count);
+ Assert.Contains("foo", result.AllowedExposedHeaders);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_ManyExposedHeaders_HeadersAllowed()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(origin: "http://example.com");
+ var policy = new CorsPolicy();
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.ExposedHeaders.Add("foo");
+ policy.ExposedHeaders.Add("bar");
+ policy.ExposedHeaders.Add("baz");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Equal(3, result.AllowedExposedHeaders.Count);
+ Assert.Contains("foo", result.AllowedExposedHeaders);
+ Assert.Contains("bar", result.AllowedExposedHeaders);
+ Assert.Contains("baz", result.AllowedExposedHeaders);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_PreflightRequest_MethodNotAllowed_ReturnsInvalidResult()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT");
+ var policy = new CorsPolicy();
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.Methods.Add("GET");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Empty(result.AllowedMethods);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_PreflightRequest_MethodAllowed_ReturnsAllowMethods()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT");
+ var policy = new CorsPolicy();
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.Methods.Add("PUT");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Contains("PUT", result.AllowedMethods);
+ }
+
+ public static TheoryData<LogData> PreflightRequests_LoggingData
+ {
+ get
+ {
+ return new TheoryData<LogData>
+ {
+ {
+ new LogData {
+ Origin = "http://example.com",
+ Method = "PUT",
+ Headers = null,
+ OriginLogMessage = "The request has an origin header: 'http://example.com'.",
+ PolicyLogMessage = "Policy execution failed.",
+ FailureReason = "Request origin http://example.com does not have permission to access the resource."
+ }
+ },
+ {
+ new LogData {
+ Origin = "http://allowed.example.com",
+ Method = "DELETE",
+ Headers = null,
+ OriginLogMessage = "The request has an origin header: 'http://allowed.example.com'.",
+ PolicyLogMessage = "Policy execution failed.",
+ FailureReason = "Request method DELETE not allowed in CORS policy."
+ }
+ },
+ {
+ new LogData {
+ Origin = "http://allowed.example.com",
+ Method = "PUT",
+ Headers = new[] { "test" },
+ OriginLogMessage = "The request has an origin header: 'http://allowed.example.com'.",
+ PolicyLogMessage = "Policy execution failed.",
+ FailureReason = "Request header 'test' not allowed in CORS policy."
+ }
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(PreflightRequests_LoggingData))]
+ public void EvaluatePolicy_LoggingForPreflightRequests_HasOriginHeader_PolicyFailed(LogData logData)
+ {
+ var sink = new TestSink();
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+ var corsService = new CorsService(new TestCorsOptions(), loggerFactory);
+ var requestContext = GetHttpContext(method: "OPTIONS", origin: logData.Origin, accessControlRequestMethod: logData.Method, accessControlRequestHeaders: logData.Headers);
+ var policy = new CorsPolicy();
+ policy.Origins.Add("http://allowed.example.com");
+ policy.Methods.Add("PUT");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ var writeList = sink.Writes.ToList();
+ Assert.Equal("The request is a preflight request.", writeList[0].State.ToString());
+ Assert.Equal(logData.OriginLogMessage, writeList[1].State.ToString());
+ Assert.Equal(logData.PolicyLogMessage, writeList[2].State.ToString());
+ Assert.Equal(logData.FailureReason, writeList[3].State.ToString());
+ }
+
+ [Fact]
+ public void EvaluatePolicy_LoggingForPreflightRequests_HasOriginHeader_PolicySucceeded()
+ {
+ var sink = new TestSink();
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+ var corsService = new CorsService(new TestCorsOptions(), loggerFactory);
+ var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://allowed.example.com", accessControlRequestMethod: "PUT");
+ var policy = new CorsPolicy();
+ policy.Origins.Add("http://allowed.example.com");
+ policy.Methods.Add("PUT");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ var writeList = sink.Writes.ToList();
+ Assert.Equal("The request is a preflight request.", writeList[0].State.ToString());
+ Assert.Equal("The request has an origin header: 'http://allowed.example.com'.", writeList[1].State.ToString());
+ Assert.Equal("Policy execution successful.", writeList[2].State.ToString());
+ }
+
+ [Fact]
+ public void EvaluatePolicy_LoggingForPreflightRequests_DoesNotHaveOriginHeader()
+ {
+ var sink = new TestSink();
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+ var corsService = new CorsService(new TestCorsOptions(), loggerFactory);
+ var requestContext = GetHttpContext(method: "OPTIONS", origin: null, accessControlRequestMethod: "PUT");
+ var policy = new CorsPolicy();
+ policy.Origins.Add("http://allowed.example.com");
+ policy.Methods.Add("PUT");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ var writeList = sink.Writes.ToList();
+ Assert.Equal("The request is a preflight request.", writeList[0].State.ToString());
+ Assert.Equal("The request does not have an origin header.", writeList[1].State.ToString());
+ }
+
+ [Fact]
+ public void EvaluatePolicy_LoggingForNonPreflightRequests_HasOriginHeader_PolicyFailed()
+ {
+ var sink = new TestSink();
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+ var corsService = new CorsService(new TestCorsOptions(), loggerFactory);
+ var requestContext = GetHttpContext(origin: "http://example.com");
+ var policy = new CorsPolicy();
+ policy.Origins.Add("http://allowed.example.com");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ var writeList = sink.Writes.ToList();
+ Assert.Equal("The request has an origin header: 'http://example.com'.", writeList[0].State.ToString());
+ Assert.Equal("Policy execution failed.", writeList[1].State.ToString());
+ Assert.Equal("Request origin http://example.com does not have permission to access the resource.", writeList[2].State.ToString());
+ }
+
+ [Fact]
+ public void EvaluatePolicy_LoggingForNonPreflightRequests_HasOriginHeader_PolicySucceeded()
+ {
+ var sink = new TestSink();
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+ var corsService = new CorsService(new TestCorsOptions(), loggerFactory);
+ var requestContext = GetHttpContext(origin: "http://allowed.example.com");
+ var policy = new CorsPolicy();
+ policy.Origins.Add("http://allowed.example.com");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ var writeList = sink.Writes.ToList();
+ Assert.Equal("The request has an origin header: 'http://allowed.example.com'.", writeList[0].State.ToString());
+ Assert.Equal("Policy execution successful.", writeList[1].State.ToString());
+ }
+
+ [Fact]
+ public void EvaluatePolicy_LoggingForNonPreflightRequests_DoesNotHaveOriginHeader()
+ {
+ var sink = new TestSink();
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+ var corsService = new CorsService(new TestCorsOptions(), loggerFactory);
+ var requestContext = GetHttpContext(origin: null);
+ var policy = new CorsPolicy();
+ policy.Origins.Add("http://allowed.example.com");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ var logMessage = Assert.Single(sink.Writes);
+ Assert.Equal("The request does not have an origin header.", logMessage.State.ToString());
+ }
+
+ [Theory]
+ [InlineData("OpTions")]
+ [InlineData("OPTIONS")]
+ public void EvaluatePolicy_CaseInsensitivePreflightRequest_OriginAllowed_ReturnsOrigin(string preflightMethod)
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(
+ method: preflightMethod,
+ origin: "http://example.com",
+ accessControlRequestMethod: "PUT");
+ var policy = new CorsPolicy();
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.Origins.Add("http://example.com");
+ policy.Methods.Add("*");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Equal("http://example.com", result.AllowedOrigin);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_PreflightRequest_IsOriginAllowedReturnsTrue_ReturnsOrigin()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(
+ method: "OPTIONS",
+ origin: "http://example.com",
+ accessControlRequestMethod: "PUT");
+ var policy = new CorsPolicy
+ {
+ IsOriginAllowed = origin => true
+ };
+ policy.Methods.Add("*");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Equal("http://example.com", result.AllowedOrigin);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_PreflightRequest_SupportsCredentials_AllowCredentialsReturnsTrue()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT");
+ var policy = new CorsPolicy
+ {
+ SupportsCredentials = true
+ };
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.Methods.Add("*");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.True(result.SupportsCredentials);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_PreflightRequest_NoPreflightMaxAge_NoPreflightMaxAgeSet()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT");
+ var policy = new CorsPolicy
+ {
+ PreflightMaxAge = null
+ };
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.Methods.Add("*");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Null(result.PreflightMaxAge);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_PreflightRequest_PreflightMaxAge_PreflightMaxAgeSet()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT");
+ var policy = new CorsPolicy
+ {
+ PreflightMaxAge = TimeSpan.FromSeconds(10)
+ };
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.Methods.Add("*");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Equal(TimeSpan.FromSeconds(10), result.PreflightMaxAge);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_PreflightRequest_AnyMethod_ReturnsRequestMethod()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "GET");
+ var policy = new CorsPolicy();
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.Methods.Add("*");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Equal(1, result.AllowedMethods.Count);
+ Assert.Contains("GET", result.AllowedMethods);
+ }
+
+ [Theory]
+ [InlineData("Put")]
+ [InlineData("PUT")]
+ public void EvaluatePolicy_CaseInsensitivePreflightRequest_ListedMethod_ReturnsSubsetOfListedMethods(string method)
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(
+ method: "OPTIONS",
+ origin: "http://example.com",
+ accessControlRequestMethod: method);
+ var policy = new CorsPolicy();
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.Methods.Add("PUT");
+ policy.Methods.Add("DELETE");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Equal(1, result.AllowedMethods.Count);
+ Assert.Contains(method, result.AllowedMethods);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_PreflightRequest_NoHeadersRequested_AllowedAllHeaders_ReturnsEmptyHeaders()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT");
+ var policy = new CorsPolicy();
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.Methods.Add("*");
+ policy.Headers.Add("*");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Empty(result.AllowedHeaders);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_PreflightRequest_HeadersRequested_AllowAllHeaders_ReturnsRequestedHeaders()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(
+ method: "OPTIONS",
+ origin: "http://example.com",
+ accessControlRequestMethod: "PUT",
+ accessControlRequestHeaders: new[] { "foo", "bar" });
+ var policy = new CorsPolicy();
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.Methods.Add("*");
+ policy.Headers.Add("*");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Equal(2, result.AllowedHeaders.Count);
+ Assert.Contains("foo", result.AllowedHeaders);
+ Assert.Contains("bar", result.AllowedHeaders);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_PreflightRequest_HeadersRequested_AllowSomeHeaders_ReturnsSubsetOfListedHeaders()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(
+ method: "OPTIONS",
+ origin: "http://example.com",
+ accessControlRequestMethod: "PUT",
+ accessControlRequestHeaders: new[] { "content-type", "accept" });
+ var policy = new CorsPolicy();
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.Methods.Add("*");
+ policy.Headers.Add("foo");
+ policy.Headers.Add("bar");
+ policy.Headers.Add("Content-Type");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Equal(2, result.AllowedHeaders.Count);
+ Assert.Contains("Content-Type", result.AllowedHeaders, StringComparer.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_PreflightRequest_HeadersRequested_NotAllHeaderMatches_ReturnsInvalidResult()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(
+ method: "OPTIONS",
+ origin: "http://example.com",
+ accessControlRequestMethod: "PUT",
+ accessControlRequestHeaders: new[] { "match", "noMatch" });
+ var policy = new CorsPolicy();
+ policy.Origins.Add(CorsConstants.AnyOrigin);
+ policy.Methods.Add("*");
+ policy.Headers.Add("match");
+ policy.Headers.Add("foo");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Empty(result.AllowedHeaders);
+ Assert.Empty(result.AllowedMethods);
+ Assert.Empty(result.AllowedExposedHeaders);
+ Assert.Null(result.AllowedOrigin);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_DoesCaseSensitiveComparison()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+
+ var policy = new CorsPolicy();
+ policy.Methods.Add("POST");
+ var httpContext = GetHttpContext(origin: null, accessControlRequestMethod: "post");
+
+ // Act
+ var result = corsService.EvaluatePolicy(httpContext, policy);
+
+ // Assert
+ Assert.Empty(result.AllowedHeaders);
+ Assert.Empty(result.AllowedMethods);
+ Assert.Empty(result.AllowedExposedHeaders);
+ Assert.Null(result.AllowedOrigin);
+ }
+
+ [Fact]
+ public void TryValidateOrigin_DoesCaseSensitiveComparison()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+
+ var policy = new CorsPolicy();
+ policy.Origins.Add("http://Example.com");
+ var httpContext = GetHttpContext(origin: "http://example.com");
+
+ // Act
+ var result = corsService.EvaluatePolicy(httpContext, policy);
+
+ // Assert
+ Assert.Empty(result.AllowedHeaders);
+ Assert.Empty(result.AllowedMethods);
+ Assert.Empty(result.AllowedExposedHeaders);
+ Assert.Null(result.AllowedOrigin);
+ }
+
+
+ [Fact]
+ public void ApplyResult_ReturnsNoHeaders_ByDefault()
+ {
+ // Arrange
+ var result = new CorsResult();
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.Empty(httpContext.Response.Headers);
+ }
+
+ [Fact]
+ public void ApplyResult_AllowOrigin_AllowOriginHeaderAdded()
+ {
+ // Arrange
+ var result = new CorsResult
+ {
+ AllowedOrigin = "http://example.com"
+ };
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.Equal("http://example.com", httpContext.Response.Headers["Access-Control-Allow-Origin"]);
+ }
+
+ [Fact]
+ public void ApplyResult_NoAllowOrigin_AllowOriginHeaderNotAdded()
+ {
+ // Arrange
+ var result = new CorsResult
+ {
+ AllowedOrigin = null
+ };
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.DoesNotContain("Access-Control-Allow-Origin", httpContext.Response.Headers.Keys);
+ }
+
+ [Fact]
+ public void ApplyResult_AllowCredentials_AllowCredentialsHeaderAdded()
+ {
+ // Arrange
+ var result = new CorsResult
+ {
+ SupportsCredentials = true
+ };
+
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ var httpContext = new DefaultHttpContext();
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.Equal("true", httpContext.Response.Headers["Access-Control-Allow-Credentials"]);
+ }
+
+ [Fact]
+ public void ApplyResult_AddVaryHeader_VaryHeaderAdded()
+ {
+ // Arrange
+ var result = new CorsResult
+ {
+ VaryByOrigin = true
+ };
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.Equal("Origin", httpContext.Response.Headers["Vary"]);
+ }
+
+ [Fact]
+ public void ApplyResult_NoAllowCredentials_AllowCredentialsHeaderNotAdded()
+ {
+ // Arrange
+ var result = new CorsResult
+ {
+ SupportsCredentials = false
+ };
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.DoesNotContain("Access-Control-Allow-Credentials", httpContext.Response.Headers.Keys);
+ }
+
+ [Fact]
+ public void ApplyResult_NoAllowMethods_AllowMethodsHeaderNotAdded()
+ {
+ // Arrange
+ var result = new CorsResult
+ {
+ // AllowMethods is empty by default
+ };
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.DoesNotContain("Access-Control-Allow-Methods", httpContext.Response.Headers.Keys);
+ }
+
+ [Fact]
+ public void ApplyResult_OneAllowMethods_AllowMethodsHeaderAdded()
+ {
+ // Arrange
+ var result = new CorsResult();
+ result.AllowedMethods.Add("PUT");
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.Equal("PUT", httpContext.Response.Headers["Access-Control-Allow-Methods"]);
+ }
+
+ [Fact]
+ public void ApplyResult_SomeSimpleAllowMethods_AllowMethodsHeaderAddedForNonSimpleMethods()
+ {
+ // Arrange
+ var result = new CorsResult();
+ result.AllowedMethods.Add("PUT");
+ result.AllowedMethods.Add("get");
+ result.AllowedMethods.Add("DELETE");
+ result.AllowedMethods.Add("POST");
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.Contains("Access-Control-Allow-Methods", httpContext.Response.Headers.Keys);
+ var value = Assert.Single(httpContext.Response.Headers.Values);
+ Assert.Equal(new[] { "PUT,DELETE" }, value);
+ string[] methods = httpContext.Response.Headers.GetCommaSeparatedValues("Access-Control-Allow-Methods");
+ Assert.Equal(2, methods.Length);
+ Assert.Contains("PUT", methods);
+ Assert.Contains("DELETE", methods);
+ }
+
+ [Fact]
+ public void ApplyResult_SimpleAllowMethods_AllowMethodsHeaderNotAdded()
+ {
+ // Arrange
+ var result = new CorsResult();
+ result.AllowedMethods.Add("GET");
+ result.AllowedMethods.Add("HEAD");
+ result.AllowedMethods.Add("POST");
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.DoesNotContain("Access-Control-Allow-Methods", httpContext.Response.Headers.Keys);
+ }
+
+ [Fact]
+ public void ApplyResult_NoAllowHeaders_AllowHeadersHeaderNotAdded()
+ {
+ // Arrange
+ var result = new CorsResult
+ {
+ // AllowHeaders is empty by default
+ };
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.DoesNotContain("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys);
+ }
+
+ [Fact]
+ public void ApplyResult_OneAllowHeaders_AllowHeadersHeaderAdded()
+ {
+ // Arrange
+ var result = new CorsResult();
+ result.AllowedHeaders.Add("foo");
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.Equal("foo", httpContext.Response.Headers["Access-Control-Allow-Headers"]);
+ }
+
+ [Fact]
+ public void ApplyResult_ManyAllowHeaders_AllowHeadersHeaderAdded()
+ {
+ // Arrange
+ var result = new CorsResult();
+ result.AllowedHeaders.Add("foo");
+ result.AllowedHeaders.Add("bar");
+ result.AllowedHeaders.Add("baz");
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.Contains("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys);
+ var value = Assert.Single(httpContext.Response.Headers.Values);
+ Assert.Equal(new[] { "foo,bar,baz" }, value);
+ string[] headerValues = httpContext.Response.Headers.GetCommaSeparatedValues("Access-Control-Allow-Headers");
+ Assert.Equal(3, headerValues.Length);
+ Assert.Contains("foo", headerValues);
+ Assert.Contains("bar", headerValues);
+ Assert.Contains("baz", headerValues);
+ }
+
+ [Fact]
+ public void ApplyResult_SomeSimpleAllowHeaders_AllowHeadersHeaderAddedForNonSimpleHeaders()
+ {
+ // Arrange
+ var result = new CorsResult();
+ result.AllowedHeaders.Add("Content-Language");
+ result.AllowedHeaders.Add("foo");
+ result.AllowedHeaders.Add("bar");
+ result.AllowedHeaders.Add("Accept");
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.Contains("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys);
+ string[] headerValues = httpContext.Response.Headers.GetCommaSeparatedValues("Access-Control-Allow-Headers");
+ Assert.Equal(2, headerValues.Length);
+ Assert.Contains("foo", headerValues);
+ Assert.Contains("bar", headerValues);
+ }
+
+ [Fact]
+ public void ApplyResult_SimpleAllowHeaders_AllowHeadersHeaderNotAdded()
+ {
+ // Arrange
+ var result = new CorsResult();
+ result.AllowedHeaders.Add("Accept");
+ result.AllowedHeaders.Add("Accept-Language");
+ result.AllowedHeaders.Add("Content-Language");
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.DoesNotContain("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys);
+ }
+
+ [Fact]
+ public void ApplyResult_NoAllowExposedHeaders_ExposedHeadersHeaderNotAdded()
+ {
+ // Arrange
+ var result = new CorsResult
+ {
+ // AllowExposedHeaders is empty by default
+ };
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.DoesNotContain("Access-Control-Expose-Headers", httpContext.Response.Headers.Keys);
+ }
+
+ [Fact]
+ public void ApplyResult_OneAllowExposedHeaders_ExposedHeadersHeaderAdded()
+ {
+ // Arrange
+ var result = new CorsResult();
+ result.AllowedExposedHeaders.Add("foo");
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.Equal("foo", httpContext.Response.Headers["Access-Control-Expose-Headers"]);
+ }
+
+ [Fact]
+ public void ApplyResult_ManyAllowExposedHeaders_ExposedHeadersHeaderAdded()
+ {
+ // Arrange
+ var result = new CorsResult();
+ result.AllowedExposedHeaders.Add("foo");
+ result.AllowedExposedHeaders.Add("bar");
+ result.AllowedExposedHeaders.Add("baz");
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.Contains("Access-Control-Expose-Headers", httpContext.Response.Headers.Keys);
+ var value = Assert.Single(httpContext.Response.Headers.Values);
+ Assert.Equal(new[] { "foo,bar,baz" }, value);
+ string[] exposedHeaderValues = httpContext.Response.Headers.GetCommaSeparatedValues("Access-Control-Expose-Headers");
+ Assert.Equal(3, exposedHeaderValues.Length);
+ Assert.Contains("foo", exposedHeaderValues);
+ Assert.Contains("bar", exposedHeaderValues);
+ Assert.Contains("baz", exposedHeaderValues);
+ }
+
+ [Fact]
+ public void ApplyResult_NoPreflightMaxAge_MaxAgeHeaderNotAdded()
+ {
+ // Arrange
+ var result = new CorsResult
+ {
+ PreflightMaxAge = null
+ };
+
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.DoesNotContain("Access-Control-Max-Age", httpContext.Response.Headers.Keys);
+ }
+
+ [Fact]
+ public void ApplyResult_PreflightMaxAge_MaxAgeHeaderAdded()
+ {
+ // Arrange
+ var result = new CorsResult
+ {
+ PreflightMaxAge = TimeSpan.FromSeconds(30)
+ };
+ var httpContext = new DefaultHttpContext();
+ var service = new CorsService(new TestCorsOptions());
+
+ // Act
+ service.ApplyResult(result, httpContext.Response);
+
+ // Assert
+ Assert.Equal("30", httpContext.Response.Headers["Access-Control-Max-Age"]);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_MultiOriginsPolicy_ReturnsVaryByOriginHeader()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(origin: "http://example.com");
+ var policy = new CorsPolicy();
+ policy.Origins.Add("http://example.com");
+ policy.Origins.Add("http://example-two.com");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.NotNull(result.AllowedOrigin);
+ Assert.True(result.VaryByOrigin);
+ }
+
+ [Fact]
+ public void EvaluatePolicy_MultiOriginsPolicy_NoMatchingOrigin_ReturnsInvalidResult()
+ {
+ // Arrange
+ var corsService = new CorsService(new TestCorsOptions());
+ var requestContext = GetHttpContext(origin: "http://example.com");
+ var policy = new CorsPolicy();
+ policy.Origins.Add("http://example-two.com");
+ policy.Origins.Add("http://example-three.com");
+
+ // Act
+ var result = corsService.EvaluatePolicy(requestContext, policy);
+
+ // Assert
+ Assert.Null(result.AllowedOrigin);
+ Assert.False(result.VaryByOrigin);
+ }
+
+
+ private static HttpContext GetHttpContext(
+ string method = null,
+ string origin = null,
+ string accessControlRequestMethod = null,
+ string[] accessControlRequestHeaders = null)
+ {
+ var context = new DefaultHttpContext();
+
+ if (method != null)
+ {
+ context.Request.Method = method;
+ }
+
+ if (origin != null)
+ {
+ context.Request.Headers.Add(CorsConstants.Origin, new[] { origin });
+ }
+
+ if (accessControlRequestMethod != null)
+ {
+ context.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { accessControlRequestMethod });
+ }
+
+ if (accessControlRequestHeaders != null)
+ {
+ context.Request.Headers.Add(CorsConstants.AccessControlRequestHeaders, accessControlRequestHeaders);
+ }
+
+ return context;
+ }
+
+ public class LogData
+ {
+ public string Origin { get; set; }
+ public string Method { get; set; }
+ public string[] Headers { get; set; }
+ public string OriginLogMessage { get; set; }
+ public string PolicyLogMessage { get; set; }
+ public string FailureReason { get; set; }
+ }
+ }
+}
diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsTestFixtureOfT.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsTestFixtureOfT.cs
new file mode 100644
index 0000000000..aa698c62f2
--- /dev/null
+++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/CorsTestFixtureOfT.cs
@@ -0,0 +1,33 @@
+// 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.Net.Http;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ public class CorsTestFixture<TStartup> : IDisposable
+ where TStartup : class
+ {
+ private readonly TestServer _server;
+
+ public CorsTestFixture()
+ {
+ var builder = new WebHostBuilder().UseStartup<TStartup>();
+ _server = new TestServer(builder);
+
+ Client = _server.CreateClient();
+ Client.BaseAddress = new Uri("http://localhost");
+ }
+
+ public HttpClient Client { get; }
+
+ public void Dispose()
+ {
+ Client.Dispose();
+ _server.Dispose();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/DefaultCorsPolicyProviderTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/DefaultCorsPolicyProviderTests.cs
new file mode 100644
index 0000000000..d66b0e5653
--- /dev/null
+++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/DefaultCorsPolicyProviderTests.cs
@@ -0,0 +1,56 @@
+// 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;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ public class DefaultPolicyProviderTests
+ {
+ [Fact]
+ public async Task UsesTheDefaultPolicyName()
+ {
+ // Arrange
+ var options = new CorsOptions();
+ var policy = new CorsPolicy();
+ options.AddPolicy(options.DefaultPolicyName, policy);
+
+ var corsOptions = new TestCorsOptions
+ {
+ Value = options
+ };
+ var policyProvider = new DefaultCorsPolicyProvider(corsOptions);
+
+ // Act
+ var actualPolicy = await policyProvider.GetPolicyAsync(new DefaultHttpContext(), policyName: null);
+
+ // Assert
+ Assert.Same(policy, actualPolicy);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("policyName")]
+ public async Task GetsNamedPolicy(string policyName)
+ {
+ // Arrange
+ var options = new CorsOptions();
+ var policy = new CorsPolicy();
+ options.AddPolicy(policyName, policy);
+
+ var corsOptions = new TestCorsOptions
+ {
+ Value = options
+ };
+ var policyProvider = new DefaultCorsPolicyProvider(corsOptions);
+
+ // Act
+ var actualPolicy = await policyProvider.GetPolicyAsync(new DefaultHttpContext(), policyName);
+
+ // Assert
+ Assert.Same(policy, actualPolicy);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/Microsoft.AspNetCore.Cors.Test.csproj b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/Microsoft.AspNetCore.Cors.Test.csproj
new file mode 100644
index 0000000000..ff45928446
--- /dev/null
+++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/Microsoft.AspNetCore.Cors.Test.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\WebSites\CorsMiddlewareWebSite\CorsMiddlewareWebSite.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Cors\Microsoft.AspNetCore.Cors.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
+ <PackageReference Include="Moq" Version="$(MoqPackageVersion)" />
+ <PackageReference Include="xunit.analyzers" Version="$(XunitAnalyzersPackageVersion)" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualstudioPackageVersion)" />
+ <PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/TestCorsOptions.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/TestCorsOptions.cs
new file mode 100644
index 0000000000..782ebb2db7
--- /dev/null
+++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/TestCorsOptions.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.
+
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ public class TestCorsOptions : IOptions<CorsOptions>
+ {
+ public CorsOptions Value { get; set; }
+ }
+}
diff --git a/src/CORS/test/Microsoft.AspNetCore.Cors.Test/UriHelpersTests.cs b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/UriHelpersTests.cs
new file mode 100644
index 0000000000..ac04cfc3fd
--- /dev/null
+++ b/src/CORS/test/Microsoft.AspNetCore.Cors.Test/UriHelpersTests.cs
@@ -0,0 +1,66 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Cors.Infrastructure
+{
+ public sealed class UriHelpersTests
+ {
+ [Theory]
+ [MemberData(nameof(IsSubdomainOfTestData))]
+ public void TestIsSubdomainOf(Uri subdomain, Uri domain)
+ {
+ // Act
+ bool isSubdomain = UriHelpers.IsSubdomainOf(subdomain, domain);
+
+ // Assert
+ Assert.True(isSubdomain);
+ }
+
+ [Theory]
+ [MemberData(nameof(IsNotSubdomainOfTestData))]
+ public void TestIsSubdomainOf_ReturnsFalse_WhenNotSubdomain(Uri subdomain, Uri domain)
+ {
+ // Act
+ bool isSubdomain = UriHelpers.IsSubdomainOf(subdomain, domain);
+
+ // Assert
+ Assert.False(isSubdomain);
+ }
+
+ public static IEnumerable<object[]> IsSubdomainOfTestData
+ {
+ get
+ {
+ return new[]
+ {
+ new object[] {new Uri("http://sub.domain"), new Uri("http://domain")},
+ new object[] {new Uri("https://sub.domain"), new Uri("https://domain")},
+ new object[] {new Uri("https://sub.domain:5678"), new Uri("https://domain:5678")},
+ new object[] {new Uri("http://sub.sub.domain"), new Uri("http://domain")},
+ new object[] {new Uri("http://sub.sub.domain"), new Uri("http://sub.domain")}
+ };
+ }
+ }
+
+ public static IEnumerable<object[]> IsNotSubdomainOfTestData
+ {
+ get
+ {
+ return new[]
+ {
+ new object[] {new Uri("http://subdomain"), new Uri("http://domain")},
+ new object[] {new Uri("https://sub.domain"), new Uri("http://domain")},
+ new object[] {new Uri("https://sub.domain:1234"), new Uri("https://domain:5678")},
+ new object[] {new Uri("http://domain.tld"), new Uri("http://domain")},
+ new object[] {new Uri("http://sub.domain.tld"), new Uri("http://domain")},
+ new object[] {new Uri("/relativeUri", UriKind.Relative), new Uri("http://domain")},
+ new object[] {new Uri("http://sub.domain"), new Uri("/relative", UriKind.Relative)}
+ };
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/test/WebSites/CorsMiddlewareWebSite/CorsMiddlewareWebSite.csproj b/src/CORS/test/WebSites/CorsMiddlewareWebSite/CorsMiddlewareWebSite.csproj
new file mode 100644
index 0000000000..bd2ea479be
--- /dev/null
+++ b/src/CORS/test/WebSites/CorsMiddlewareWebSite/CorsMiddlewareWebSite.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.Cors\Microsoft.AspNetCore.Cors.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/CORS/test/WebSites/CorsMiddlewareWebSite/EchoMiddleware.cs b/src/CORS/test/WebSites/CorsMiddlewareWebSite/EchoMiddleware.cs
new file mode 100644
index 0000000000..9036b33bf6
--- /dev/null
+++ b/src/CORS/test/WebSites/CorsMiddlewareWebSite/EchoMiddleware.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;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace CorsMiddlewareWebSite
+{
+ public class EchoMiddleware
+ {
+ /// <summary>
+ /// Instantiates a new <see cref="EchoMiddleware"/>.
+ /// </summary>
+ /// <param name="next">The next middleware in the pipeline.</param>
+ public EchoMiddleware(RequestDelegate next)
+ {
+ }
+
+ /// <summary>
+ /// Echo the request's path in the response. Does not invoke later middleware in the pipeline.
+ /// </summary>
+ /// <param name="context">The <see cref="HttpContext"/> of the current request.</param>
+ /// <returns>A <see cref="Task"/> that completes when writing to the response is done.</returns>
+ public Task Invoke(HttpContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ context.Response.ContentType = "text/plain; charset=utf-8";
+ var path = context.Request.PathBase + context.Request.Path + context.Request.QueryString;
+ return context.Response.WriteAsync(path, Encoding.UTF8);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/test/WebSites/CorsMiddlewareWebSite/Startup.cs b/src/CORS/test/WebSites/CorsMiddlewareWebSite/Startup.cs
new file mode 100644
index 0000000000..3043f59bcd
--- /dev/null
+++ b/src/CORS/test/WebSites/CorsMiddlewareWebSite/Startup.cs
@@ -0,0 +1,33 @@
+// 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.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CorsMiddlewareWebSite
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddCors();
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseCors(policy => policy.WithOrigins("http://example.com"));
+ app.UseMiddleware<EchoMiddleware>();
+ }
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .UseKestrel()
+ .UseIISIntegration()
+ .UseStartup<Startup>()
+ .Build();
+
+ host.Run();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/CORS/test/WebSites/CorsMiddlewareWebSite/readme.md b/src/CORS/test/WebSites/CorsMiddlewareWebSite/readme.md
new file mode 100644
index 0000000000..d7f8b28106
--- /dev/null
+++ b/src/CORS/test/WebSites/CorsMiddlewareWebSite/readme.md
@@ -0,0 +1,4 @@
+CorsMiddlewareWebSite
+===
+
+This web site illustrates how to use CorsMiddleware to apply a policy for entire application.
diff --git a/src/CORS/test/WebSites/CorsMiddlewareWebSite/web.config b/src/CORS/test/WebSites/CorsMiddlewareWebSite/web.config
new file mode 100644
index 0000000000..f7ac679334
--- /dev/null
+++ b/src/CORS/test/WebSites/CorsMiddlewareWebSite/web.config
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<configuration>
+ <system.webServer>
+ <handlers>
+ <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
+ </handlers>
+ <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" />
+ </system.webServer>
+</configuration> \ No newline at end of file
diff --git a/src/CORS/version.props b/src/CORS/version.props
new file mode 100644
index 0000000000..669c874829
--- /dev/null
+++ b/src/CORS/version.props
@@ -0,0 +1,12 @@
+<Project>
+ <PropertyGroup>
+ <VersionPrefix>2.1.1</VersionPrefix>
+ <VersionSuffix>rtm</VersionSuffix>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' == 'rtm' ">$(VersionPrefix)</PackageVersion>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' != 'rtm' ">$(VersionPrefix)-$(VersionSuffix)-final</PackageVersion>
+ <BuildNumber Condition="'$(BuildNumber)' == ''">t000</BuildNumber>
+ <FeatureBranchVersionPrefix Condition="'$(FeatureBranchVersionPrefix)' == ''">a-</FeatureBranchVersionPrefix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
+ </PropertyGroup>
+</Project>
diff --git a/src/HttpSysServer/.gitignore b/src/HttpSysServer/.gitignore
new file mode 100644
index 0000000000..e062ff6d76
--- /dev/null
+++ b/src/HttpSysServer/.gitignore
@@ -0,0 +1,32 @@
+[Oo]bj/
+[Bb]in/
+TestResults/
+.nuget/
+_ReSharper.*/
+packages/
+artifacts/
+PublishProfiles/
+*.user
+*.suo
+*.cache
+*.docstates
+_ReSharper.*
+nuget.exe
+*net45.csproj
+*net451.csproj
+*k10.csproj
+*.psess
+*.vsp
+*.pidb
+*.userprefs
+*DS_Store
+*.ncrunchsolution
+*.*sdf
+*.ipch
+*.sln.ide
+project.lock.json
+/.vs
+.vscode/
+.build/
+.testPublish/
+global.json
diff --git a/src/HttpSysServer/Directory.Build.props b/src/HttpSysServer/Directory.Build.props
new file mode 100644
index 0000000000..f1986d9953
--- /dev/null
+++ b/src/HttpSysServer/Directory.Build.props
@@ -0,0 +1,20 @@
+<Project>
+ <Import
+ Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))\AspNetCoreSettings.props"
+ Condition=" '$(CI)' != 'true' AND '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))' != '' " />
+
+ <Import Project="version.props" />
+ <Import Project="build\dependencies.props" />
+ <Import Project="build\sources.props" />
+
+ <PropertyGroup>
+ <Product>Microsoft ASP.NET Core</Product>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
+ <RepositoryType>git</RepositoryType>
+ <RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
+ <AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)build\Key.snk</AssemblyOriginatorKeyFile>
+ <SignAssembly>true</SignAssembly>
+ <PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+</Project>
diff --git a/src/HttpSysServer/Directory.Build.targets b/src/HttpSysServer/Directory.Build.targets
new file mode 100644
index 0000000000..53b3f6e1da
--- /dev/null
+++ b/src/HttpSysServer/Directory.Build.targets
@@ -0,0 +1,7 @@
+<Project>
+ <PropertyGroup>
+ <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">$(MicrosoftNETCoreApp20PackageVersion)</RuntimeFrameworkVersion>
+ <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">$(MicrosoftNETCoreApp21PackageVersion)</RuntimeFrameworkVersion>
+ <NETStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard2.0' ">$(NETStandardLibrary20PackageVersion)</NETStandardImplicitPackageVersion>
+ </PropertyGroup>
+</Project>
diff --git a/src/HttpSysServer/HttpSysServer.sln b/src/HttpSysServer/HttpSysServer.sln
new file mode 100644
index 0000000000..adc3b6d351
--- /dev/null
+++ b/src/HttpSysServer/HttpSysServer.sln
@@ -0,0 +1,176 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26730.10
+MinimumVisualStudioVersion = 15.0.26730.03
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{99D5E5F3-88F5-4CCF-8D8C-717C8925DF09}"
+ ProjectSection(SolutionItems) = preProject
+ src\Directory.Build.props = src\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E183C826-1360-4DFF-9994-F33CED5C8525}"
+ ProjectSection(SolutionItems) = preProject
+ test\Directory.Build.props = test\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{3A1E31E3-2794-4CA3-B8E2-253E96BDE514}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5E9B546C-17AC-4BDF-BCB3-5955D4755ED8}"
+ ProjectSection(SolutionItems) = preProject
+ .appveyor.yml = .appveyor.yml
+ .travis.yml = .travis.yml
+ build.cmd = build.cmd
+ build.ps1 = build.ps1
+ build.sh = build.sh
+ Directory.Build.props = Directory.Build.props
+ Directory.Build.targets = Directory.Build.targets
+ NuGet.config = NuGet.config
+ version.xml = version.xml
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestClient", "samples\TestClient\TestClient.csproj", "{8B828433-B333-4C19-96AE-00BFFF9D8841}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SelfHostServer", "samples\SelfHostServer\SelfHostServer.csproj", "{1236F93A-AC5C-4A77-9477-C88F040151CA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.HttpSys.FunctionalTests", "test\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj", "{4492FF4C-9032-411D-853F-46B01755E504}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.HttpSys", "src\Microsoft.AspNetCore.Server.HttpSys\Microsoft.AspNetCore.Server.HttpSys.csproj", "{B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HotAddSample", "samples\HotAddSample\HotAddSample.csproj", "{8BFA392A-8B67-4454-916B-67C545EDFAEF}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.HttpSys.Tests", "test\Microsoft.AspNetCore.Server.HttpSys.Tests\Microsoft.AspNetCore.Server.HttpSys.Tests.csproj", "{E837249E-E666-4DF2-AFC3-7A4D70234F9F}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{85914BA9-4168-48C5-9C3F-E2E8B1479A6E}"
+ ProjectSection(SolutionItems) = preProject
+ build\dependencies.props = build\dependencies.props
+ build\Key.snk = build\Key.snk
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{AB6964C9-A7AF-4FAC-BEA1-C8A538EC989E}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.AspNetCore.HttpSys.Sources", "Microsoft.AspNetCore.HttpSys.Sources", "{4AB1E069-2A8A-4D46-98AE-CC82E3497038}"
+ ProjectSection(SolutionItems) = preProject
+ shared\Microsoft.AspNetCore.HttpSys.Sources\Constants.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\Constants.cs
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NativeInterop", "NativeInterop", "{94AD33C9-1BDD-4385-A850-4B24FD5D5012}"
+ ProjectSection(SolutionItems) = preProject
+ shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\CookedUrl.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\CookedUrl.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\HeapAllocHandle.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\HeapAllocHandle.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\HttpApiTypes.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\HttpApiTypes.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\HttpSysRequestHeader.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\HttpSysRequestHeader.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\HttpSysResponseHeader.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\HttpSysResponseHeader.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\NativeRequestInput.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\NativeRequestInput.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\NclUtilities.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\NclUtilities.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\SafeLocalFreeChannelBinding.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\SafeLocalFreeChannelBinding.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\SafeLocalMemHandle.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\SafeLocalMemHandle.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\SafeNativeOverlapped.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\SafeNativeOverlapped.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\SocketAddress.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\SocketAddress.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\UnsafeNativeMethods.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\NativeInterop\UnsafeNativeMethods.cs
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RequestProcessing", "RequestProcessing", "{AA8C91BD-D558-468B-9258-1E186884F78D}"
+ ProjectSection(SolutionItems) = preProject
+ shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\HeaderCollection.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\HeaderCollection.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\HeaderEncoding.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\HeaderEncoding.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\HeaderParser.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\HeaderParser.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\HttpKnownHeaderNames.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\HttpKnownHeaderNames.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\NativeRequestContext.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\NativeRequestContext.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\RequestHeaders.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\RequestHeaders.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\RequestHeaders.Generated.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\RequestHeaders.Generated.cs
+ shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\SslStatus.cs = shared\Microsoft.AspNetCore.HttpSys.Sources\RequestProcessing\SslStatus.cs
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|Mixed Platforms = Debug|Mixed Platforms
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|Mixed Platforms = Release|Mixed Platforms
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {8B828433-B333-4C19-96AE-00BFFF9D8841}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8B828433-B333-4C19-96AE-00BFFF9D8841}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8B828433-B333-4C19-96AE-00BFFF9D8841}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {8B828433-B333-4C19-96AE-00BFFF9D8841}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {8B828433-B333-4C19-96AE-00BFFF9D8841}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8B828433-B333-4C19-96AE-00BFFF9D8841}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8B828433-B333-4C19-96AE-00BFFF9D8841}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8B828433-B333-4C19-96AE-00BFFF9D8841}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {8B828433-B333-4C19-96AE-00BFFF9D8841}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {8B828433-B333-4C19-96AE-00BFFF9D8841}.Release|x86.ActiveCfg = Release|Any CPU
+ {1236F93A-AC5C-4A77-9477-C88F040151CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1236F93A-AC5C-4A77-9477-C88F040151CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1236F93A-AC5C-4A77-9477-C88F040151CA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {1236F93A-AC5C-4A77-9477-C88F040151CA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {1236F93A-AC5C-4A77-9477-C88F040151CA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1236F93A-AC5C-4A77-9477-C88F040151CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1236F93A-AC5C-4A77-9477-C88F040151CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1236F93A-AC5C-4A77-9477-C88F040151CA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {1236F93A-AC5C-4A77-9477-C88F040151CA}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {1236F93A-AC5C-4A77-9477-C88F040151CA}.Release|x86.ActiveCfg = Release|Any CPU
+ {4492FF4C-9032-411D-853F-46B01755E504}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4492FF4C-9032-411D-853F-46B01755E504}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4492FF4C-9032-411D-853F-46B01755E504}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {4492FF4C-9032-411D-853F-46B01755E504}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {4492FF4C-9032-411D-853F-46B01755E504}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4492FF4C-9032-411D-853F-46B01755E504}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4492FF4C-9032-411D-853F-46B01755E504}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4492FF4C-9032-411D-853F-46B01755E504}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {4492FF4C-9032-411D-853F-46B01755E504}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {4492FF4C-9032-411D-853F-46B01755E504}.Release|x86.ActiveCfg = Release|Any CPU
+ {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92}.Release|x86.ActiveCfg = Release|Any CPU
+ {8BFA392A-8B67-4454-916B-67C545EDFAEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8BFA392A-8B67-4454-916B-67C545EDFAEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8BFA392A-8B67-4454-916B-67C545EDFAEF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {8BFA392A-8B67-4454-916B-67C545EDFAEF}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {8BFA392A-8B67-4454-916B-67C545EDFAEF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8BFA392A-8B67-4454-916B-67C545EDFAEF}.Debug|x86.Build.0 = Debug|Any CPU
+ {8BFA392A-8B67-4454-916B-67C545EDFAEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8BFA392A-8B67-4454-916B-67C545EDFAEF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8BFA392A-8B67-4454-916B-67C545EDFAEF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {8BFA392A-8B67-4454-916B-67C545EDFAEF}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {8BFA392A-8B67-4454-916B-67C545EDFAEF}.Release|x86.ActiveCfg = Release|Any CPU
+ {8BFA392A-8B67-4454-916B-67C545EDFAEF}.Release|x86.Build.0 = Release|Any CPU
+ {E837249E-E666-4DF2-AFC3-7A4D70234F9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E837249E-E666-4DF2-AFC3-7A4D70234F9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E837249E-E666-4DF2-AFC3-7A4D70234F9F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {E837249E-E666-4DF2-AFC3-7A4D70234F9F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {E837249E-E666-4DF2-AFC3-7A4D70234F9F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E837249E-E666-4DF2-AFC3-7A4D70234F9F}.Debug|x86.Build.0 = Debug|Any CPU
+ {E837249E-E666-4DF2-AFC3-7A4D70234F9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E837249E-E666-4DF2-AFC3-7A4D70234F9F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E837249E-E666-4DF2-AFC3-7A4D70234F9F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {E837249E-E666-4DF2-AFC3-7A4D70234F9F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {E837249E-E666-4DF2-AFC3-7A4D70234F9F}.Release|x86.ActiveCfg = Release|Any CPU
+ {E837249E-E666-4DF2-AFC3-7A4D70234F9F}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {8B828433-B333-4C19-96AE-00BFFF9D8841} = {3A1E31E3-2794-4CA3-B8E2-253E96BDE514}
+ {1236F93A-AC5C-4A77-9477-C88F040151CA} = {3A1E31E3-2794-4CA3-B8E2-253E96BDE514}
+ {4492FF4C-9032-411D-853F-46B01755E504} = {E183C826-1360-4DFF-9994-F33CED5C8525}
+ {B9F45F9D-D206-47F0-8E5F-54CE2F0BDF92} = {99D5E5F3-88F5-4CCF-8D8C-717C8925DF09}
+ {8BFA392A-8B67-4454-916B-67C545EDFAEF} = {3A1E31E3-2794-4CA3-B8E2-253E96BDE514}
+ {E837249E-E666-4DF2-AFC3-7A4D70234F9F} = {E183C826-1360-4DFF-9994-F33CED5C8525}
+ {85914BA9-4168-48C5-9C3F-E2E8B1479A6E} = {5E9B546C-17AC-4BDF-BCB3-5955D4755ED8}
+ {4AB1E069-2A8A-4D46-98AE-CC82E3497038} = {AB6964C9-A7AF-4FAC-BEA1-C8A538EC989E}
+ {94AD33C9-1BDD-4385-A850-4B24FD5D5012} = {4AB1E069-2A8A-4D46-98AE-CC82E3497038}
+ {AA8C91BD-D558-468B-9258-1E186884F78D} = {4AB1E069-2A8A-4D46-98AE-CC82E3497038}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {34B42B42-FA09-41AB-9216-14073990C504}
+ EndGlobalSection
+EndGlobal
diff --git a/src/HttpSysServer/NuGetPackageVerifier.json b/src/HttpSysServer/NuGetPackageVerifier.json
new file mode 100644
index 0000000000..c02b36b40a
--- /dev/null
+++ b/src/HttpSysServer/NuGetPackageVerifier.json
@@ -0,0 +1,13 @@
+{
+ "adx-nonshipping": {
+ "rules": [],
+ "packages": {
+ "Microsoft.AspNetCore.HttpSys.Sources": {}
+ }
+ },
+ "Default": {
+ "rules": [
+ "DefaultCompositeRule"
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/README.md b/src/HttpSysServer/README.md
new file mode 100644
index 0000000000..b9c9f22edf
--- /dev/null
+++ b/src/HttpSysServer/README.md
@@ -0,0 +1,10 @@
+HttpSysServer
+=================
+
+| AppVeyor | Travis |
+| ---- | ----
+| [![AppVeyor](https://ci.appveyor.com/api/projects/status/47fv9qoe862xlr25/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/HttpSysServer/branch/dev) | [![Travis](https://travis-ci.org/aspnet/HttpSysServer.svg?branch=dev)](https://travis-ci.org/aspnet/HttpSysServer) |
+
+This repo contains a web server for ASP.NET Core based on the Windows [Http Server API](https://msdn.microsoft.com/en-us/library/windows/desktop/aa364510.aspx).
+
+This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
diff --git a/src/HttpSysServer/build/Key.snk b/src/HttpSysServer/build/Key.snk
new file mode 100644
index 0000000000..e10e4889c1
--- /dev/null
+++ b/src/HttpSysServer/build/Key.snk
Binary files differ
diff --git a/src/HttpSysServer/build/dependencies.props b/src/HttpSysServer/build/dependencies.props
new file mode 100644
index 0000000000..0a90c83355
--- /dev/null
+++ b/src/HttpSysServer/build/dependencies.props
@@ -0,0 +1,31 @@
+<Project>
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ </PropertyGroup>
+
+ <!-- These package versions may be overridden or updated by automation. -->
+ <PropertyGroup Label="Package Versions: Auto">
+ <InternalAspNetCoreSdkPackageVersion>2.1.3-rtm-15802</InternalAspNetCoreSdkPackageVersion>
+ <MicrosoftNETCoreApp20PackageVersion>2.0.0</MicrosoftNETCoreApp20PackageVersion>
+ <MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion>
+ <MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
+ <MicrosoftWin32RegistryPackageVersion>4.5.0</MicrosoftWin32RegistryPackageVersion>
+ <NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
+ <SystemNetHttpWinHttpHandlerPackageVersion>4.5.0</SystemNetHttpWinHttpHandlerPackageVersion>
+ <SystemSecurityPrincipalWindowsPackageVersion>4.5.0</SystemSecurityPrincipalWindowsPackageVersion>
+ <XunitPackageVersion>2.3.1</XunitPackageVersion>
+ <XunitRunnerVisualStudioPackageVersion>2.4.0-beta.1.build3945</XunitRunnerVisualStudioPackageVersion>
+ </PropertyGroup>
+
+ <!-- This may import a generated file which may override the variables above. -->
+ <Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
+
+ <!-- These are package versions that should not be overridden or updated by automation. -->
+ <PropertyGroup Label="Package Versions: Pinned">
+ <MicrosoftAspNetCoreAuthenticationCorePackageVersion>2.1.1</MicrosoftAspNetCoreAuthenticationCorePackageVersion>
+ <MicrosoftAspNetCoreHostingPackageVersion>2.1.1</MicrosoftAspNetCoreHostingPackageVersion>
+ <MicrosoftAspNetCoreTestingPackageVersion>2.1.0</MicrosoftAspNetCoreTestingPackageVersion>
+ <MicrosoftExtensionsLoggingConsolePackageVersion>2.1.1</MicrosoftExtensionsLoggingConsolePackageVersion>
+ <MicrosoftNetHttpHeadersPackageVersion>2.1.1</MicrosoftNetHttpHeadersPackageVersion>
+ </PropertyGroup>
+</Project> \ No newline at end of file
diff --git a/src/HttpSysServer/build/repo.props b/src/HttpSysServer/build/repo.props
new file mode 100644
index 0000000000..dab1601c88
--- /dev/null
+++ b/src/HttpSysServer/build/repo.props
@@ -0,0 +1,15 @@
+<Project>
+ <Import Project="dependencies.props" />
+
+ <PropertyGroup>
+ <!-- These properties are use by the automation that updates dependencies.props -->
+ <LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
+ <LineupPackageVersion>2.1.0-rc1-*</LineupPackageVersion>
+ <LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp20PackageVersion)" />
+ <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/HttpSysServer/build/sources.props b/src/HttpSysServer/build/sources.props
new file mode 100644
index 0000000000..9215df9751
--- /dev/null
+++ b/src/HttpSysServer/build/sources.props
@@ -0,0 +1,17 @@
+<Project>
+ <Import Project="$(DotNetRestoreSourcePropsPath)" Condition="'$(DotNetRestoreSourcePropsPath)' != ''"/>
+
+ <PropertyGroup Label="RestoreSources">
+ <RestoreSources>$(DotNetRestoreSources)</RestoreSources>
+ <RestoreSources Condition="'$(DotNetBuildOffline)' != 'true' AND '$(AspNetUniverseBuildOffline)' != 'true' ">
+ $(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>
+ <RestoreSources Condition="'$(DotNetBuildOffline)' != 'true'">
+ $(RestoreSources);
+ https://api.nuget.org/v3/index.json;
+ </RestoreSources>
+ </PropertyGroup>
+</Project>
diff --git a/src/HttpSysServer/samples/HotAddSample/HotAddSample.csproj b/src/HttpSysServer/samples/HotAddSample/HotAddSample.csproj
new file mode 100644
index 0000000000..caf6da74fe
--- /dev/null
+++ b/src/HttpSysServer/samples/HotAddSample/HotAddSample.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
+ <OutputType>Exe</OutputType>
+ <ServerGarbageCollection>true</ServerGarbageCollection>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Server.HttpSys\Microsoft.AspNetCore.Server.HttpSys.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/HttpSysServer/samples/HotAddSample/Properties/launchSettings.json b/src/HttpSysServer/samples/HotAddSample/Properties/launchSettings.json
new file mode 100644
index 0000000000..5df19c1327
--- /dev/null
+++ b/src/HttpSysServer/samples/HotAddSample/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "profiles": {
+ "HotAddSample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "http://localhost:12345",
+ "environmentVariables": {
+ "ASPNETCORE_URLS": "http://localhost:12345",
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/samples/HotAddSample/Startup.cs b/src/HttpSysServer/samples/HotAddSample/Startup.cs
new file mode 100644
index 0000000000..58975d6aa6
--- /dev/null
+++ b/src/HttpSysServer/samples/HotAddSample/Startup.cs
@@ -0,0 +1,110 @@
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Server.HttpSys;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace HotAddSample
+{
+ // This sample shows how to dynamically add or remove prefixes for the underlying server.
+ // Be careful not to remove the prefix you're currently accessing because the connection
+ // will be reset before the end of the request.
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.Configure<HttpSysOptions>(options =>
+ {
+ ServerOptions = options;
+ });
+ }
+
+ public HttpSysOptions ServerOptions { get; set; }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ var addresses = ServerOptions.UrlPrefixes;
+ addresses.Add("http://localhost:12346/pathBase/");
+
+ app.Use(async (context, next) =>
+ {
+ // Note: To add any prefix other than localhost you must run this sample as an administrator.
+ var toAdd = context.Request.Query["add"];
+ if (!string.IsNullOrEmpty(toAdd))
+ {
+ context.Response.ContentType = "text/html";
+ await context.Response.WriteAsync("<html><body>");
+ try
+ {
+ addresses.Add(toAdd);
+ await context.Response.WriteAsync("Added: <a href=\"" + toAdd + "\">" + toAdd + "</a>");
+ }
+ catch (Exception ex)
+ {
+ await context.Response.WriteAsync("Error adding: " + toAdd + "<br>");
+ await context.Response.WriteAsync(ex.ToString().Replace(Environment.NewLine, "<br>"));
+ }
+ await context.Response.WriteAsync("<br><a href=\"" + context.Request.PathBase.ToUriComponent() + "\">back</a>");
+ await context.Response.WriteAsync("</body></html>");
+ return;
+ }
+ await next();
+ });
+
+ app.Use(async (context, next) =>
+ {
+ // Be careful not to remove the prefix you're currently accessing because the connection
+ // will be reset before the response is sent.
+ var toRemove = context.Request.Query["remove"];
+ if (!string.IsNullOrEmpty(toRemove))
+ {
+ context.Response.ContentType = "text/html";
+ await context.Response.WriteAsync("<html><body>");
+ if (addresses.Remove(toRemove))
+ {
+ await context.Response.WriteAsync("Removed: " + toRemove);
+ }
+ else
+ {
+ await context.Response.WriteAsync("Not found: " + toRemove);
+ }
+ await context.Response.WriteAsync("<br><a href=\"" + context.Request.PathBase.ToUriComponent() + "\">back</a>");
+ await context.Response.WriteAsync("</body></html>");
+ return;
+ }
+ await next();
+ });
+
+ app.Run(async context =>
+ {
+ context.Response.ContentType = "text/html";
+ await context.Response.WriteAsync("<html><body>");
+ await context.Response.WriteAsync("Listening on these prefixes: <br>");
+ foreach (var prefix in addresses)
+ {
+ await context.Response.WriteAsync("<a href=\"" + prefix + "\">" + prefix + "</a> <a href=\"?remove=" + prefix + "\">(remove)</a><br>");
+ }
+
+ await context.Response.WriteAsync("<form action=\"" + context.Request.PathBase.ToUriComponent() + "\" method=\"GET\">");
+ await context.Response.WriteAsync("<input type=\"text\" name=\"add\" value=\"http://localhost:12348\" >");
+ await context.Response.WriteAsync("<input type=\"submit\" value=\"Add\">");
+ await context.Response.WriteAsync("</form>");
+
+ await context.Response.WriteAsync("</body></html>");
+ });
+ }
+
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging(factory => factory.AddConsole())
+ .UseStartup<Startup>()
+ .UseHttpSys()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/HttpSysServer/samples/SelfHostServer/App.config b/src/HttpSysServer/samples/SelfHostServer/App.config
new file mode 100644
index 0000000000..3048ab74e7
--- /dev/null
+++ b/src/HttpSysServer/samples/SelfHostServer/App.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+ <runtime>
+ <gcServer enabled="true"/>
+ </runtime>
+</configuration> \ No newline at end of file
diff --git a/src/HttpSysServer/samples/SelfHostServer/Properties/launchSettings.json b/src/HttpSysServer/samples/SelfHostServer/Properties/launchSettings.json
new file mode 100644
index 0000000000..54fa816762
--- /dev/null
+++ b/src/HttpSysServer/samples/SelfHostServer/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "SelfHostServer": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "http://localhost:5000/",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/samples/SelfHostServer/Public/1kb.txt b/src/HttpSysServer/samples/SelfHostServer/Public/1kb.txt
new file mode 100644
index 0000000000..1d43866603
--- /dev/null
+++ b/src/HttpSysServer/samples/SelfHostServer/Public/1kb.txt
@@ -0,0 +1 @@
+asdfqweruoiasdfnsngdfioenrglknsgilhasdgha;gu;agnaknusgnjkadfgnknjksdfk asdhfhasdf nklasdgnasg njagnjasdfqweruoiasdfnsngdfioenrglknsgilhasdgha;gu;agnaknusgnjkadfgnknjksdfk asdhfhasdf nklasdgnasg njagnjasdfqweruoiasdfnsngdfioenrglknsgilhasdgha;gu;agnaknusgnjkadfgnknjksdfk asdhfhasdf nklasdgnasg njagnjasdfqweruoiasdfnsngdfioenrglknsgilhasdgha;gu;agnaknusgnjkadfgnknjksdfk asdhfhasdf nklasdgnasg njagnjasdfqweruoiasdfnsngdfioenrglknsgilhasdgha;gu;agnaknusgnjkadfgnknjksdfk asdhfhasdf nklasdgnasg njagnjasdfqweruoiasdfnsngdfioenrglknsgilhasdgha;gu;agnaknusgnjkadfgnknjksdfk asdhfhasdf nklasdgnasg njagnjasdfqweruoiasdfnsngdfioenrglknsgilhasdgha;gu;agnaknusgnjkadfgnknjksdfk asdhfhasdf nklasdgnasg njagnjasdfqweruoiasdfnsngdfioenrglknsgilhasdgha;gu;agnaknusgnjkadfgnknjksdfk asdhfhasdf nklasdgnasg njagnjasdfqweruoiasdfnsngdfioenrglknsgilhasdgha;gu;agnaknusgnjkadfgnknjksdfk asdhfhasdf nklasdgnasg njagnjasdfqweruoiasdfnsngdfioenrglknsgilhasdgha;gu;agnaknusgnjkadfgnknjksdfk asdhfhasdf nklasdgnasg njagnjasdfasdfasdfasdfasdfasd \ No newline at end of file
diff --git a/src/HttpSysServer/samples/SelfHostServer/SelfHostServer.csproj b/src/HttpSysServer/samples/SelfHostServer/SelfHostServer.csproj
new file mode 100644
index 0000000000..caf6da74fe
--- /dev/null
+++ b/src/HttpSysServer/samples/SelfHostServer/SelfHostServer.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
+ <OutputType>Exe</OutputType>
+ <ServerGarbageCollection>true</ServerGarbageCollection>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Server.HttpSys\Microsoft.AspNetCore.Server.HttpSys.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/HttpSysServer/samples/SelfHostServer/Startup.cs b/src/HttpSysServer/samples/SelfHostServer/Startup.cs
new file mode 100644
index 0000000000..52b65bd248
--- /dev/null
+++ b/src/HttpSysServer/samples/SelfHostServer/Startup.cs
@@ -0,0 +1,48 @@
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Server.HttpSys;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace SelfHostServer
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ // Server options can be configured here instead of in Main.
+ services.Configure<HttpSysOptions>(options =>
+ {
+ options.Authentication.Schemes = AuthenticationSchemes.None;
+ options.Authentication.AllowAnonymous = true;
+ });
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.Run(async context =>
+ {
+ context.Response.ContentType = "text/plain";
+ await context.Response.WriteAsync("Hello world from " + context.Request.Host + " at " + DateTime.Now);
+ });
+ }
+
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging(factory => factory.AddConsole())
+ .UseStartup<Startup>()
+ .UseHttpSys(options =>
+ {
+ options.UrlPrefixes.Add("http://localhost:5000");
+ options.Authentication.Schemes = AuthenticationSchemes.None;
+ options.Authentication.AllowAnonymous = true;
+ })
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/HttpSysServer/samples/TestClient/App.config b/src/HttpSysServer/samples/TestClient/App.config
new file mode 100644
index 0000000000..2d2a12d81b
--- /dev/null
+++ b/src/HttpSysServer/samples/TestClient/App.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <startup>
+ <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/>
+ </startup>
+</configuration>
diff --git a/src/HttpSysServer/samples/TestClient/Program.cs b/src/HttpSysServer/samples/TestClient/Program.cs
new file mode 100644
index 0000000000..f57945de42
--- /dev/null
+++ b/src/HttpSysServer/samples/TestClient/Program.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace TestClient
+{
+ public class Program
+ {
+ private const string Address =
+ // "http://localhost:5000/public/1kb.txt";
+ "https://localhost:9090/public/1kb.txt";
+
+ public static void Main(string[] args)
+ {
+ WebRequestHandler handler = new WebRequestHandler();
+ handler.ServerCertificateValidationCallback = (_, __, ___, ____) => true;
+ // handler.UseDefaultCredentials = true;
+ handler.Credentials = new NetworkCredential(@"redmond\chrross", "passwird");
+ HttpClient client = new HttpClient(handler);
+
+ /*
+ int completionCount = 0;
+ int iterations = 30000;
+ for (int i = 0; i < iterations; i++)
+ {
+ client.GetAsync(Address)
+ .ContinueWith(t => Interlocked.Increment(ref completionCount));
+ }
+
+ while (completionCount < iterations)
+ {
+ Thread.Sleep(10);
+ }*/
+
+ while (true)
+ {
+ Console.WriteLine("Press any key to send request");
+ Console.ReadKey();
+ var result = client.GetAsync(Address).Result;
+ Console.WriteLine(result);
+ }
+
+ // RunWebSocketClient().Wait();
+ // Console.WriteLine("Done");
+ // Console.ReadKey();
+ }
+
+ public static async Task RunWebSocketClient()
+ {
+ ClientWebSocket websocket = new ClientWebSocket();
+
+ string url = "ws://localhost:5000/";
+ Console.WriteLine("Connecting to: " + url);
+ await websocket.ConnectAsync(new Uri(url), CancellationToken.None);
+
+ string message = "Hello World";
+ Console.WriteLine("Sending message: " + message);
+ byte[] messageBytes = Encoding.UTF8.GetBytes(message);
+ await websocket.SendAsync(new ArraySegment<byte>(messageBytes), WebSocketMessageType.Text, true, CancellationToken.None);
+
+ byte[] incomingData = new byte[1024];
+ WebSocketReceiveResult result = await websocket.ReceiveAsync(new ArraySegment<byte>(incomingData), CancellationToken.None);
+
+ if (result.CloseStatus.HasValue)
+ {
+ Console.WriteLine("Closed; Status: " + result.CloseStatus + ", " + result.CloseStatusDescription);
+ }
+ else
+ {
+ Console.WriteLine("Received message: " + Encoding.UTF8.GetString(incomingData, 0, result.Count));
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/samples/TestClient/Properties/AssemblyInfo.cs b/src/HttpSysServer/samples/TestClient/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..249372ae2d
--- /dev/null
+++ b/src/HttpSysServer/samples/TestClient/Properties/AssemblyInfo.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft Open Technologies, Inc.
+// All Rights Reserved
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR
+// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
+// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF
+// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR
+// NON-INFRINGEMENT.
+// See the Apache 2 License for the specific language governing
+// permissions and limitations under the License.
+
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("TestClient")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("TestClient")]
+[assembly: AssemblyCopyright("Copyright © 2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("8db62eb3-48c0-4049-b33e-271c738140a0")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("0.5")]
+[assembly: AssemblyVersion("0.5")]
+[assembly: AssemblyFileVersion("0.5.40117.0")]
diff --git a/src/HttpSysServer/samples/TestClient/TestClient.csproj b/src/HttpSysServer/samples/TestClient/TestClient.csproj
new file mode 100644
index 0000000000..781a187052
--- /dev/null
+++ b/src/HttpSysServer/samples/TestClient/TestClient.csproj
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{8B828433-B333-4C19-96AE-00BFFF9D8841}</ProjectGuid>
+ <OutputType>Exe</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>TestClient</RootNamespace>
+ <AssemblyName>TestClient</AssemblyName>
+ <TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
+ <RestorePackages>true</RestorePackages>
+ <TargetFrameworkProfile />
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <PlatformTarget>AnyCPU</PlatformTarget>
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <PlatformTarget>AnyCPU</PlatformTarget>
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Net.Http" />
+ <Reference Include="System.Net.Http.WebRequest" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Program.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="App.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/Constants.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/Constants.cs
new file mode 100644
index 0000000000..0982badab8
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/Constants.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;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ internal static class Constants
+ {
+ internal const string HttpScheme = "http";
+ internal const string HttpsScheme = "https";
+ internal const string Chunked = "chunked";
+ internal const string Close = "close";
+ internal const string Zero = "0";
+ internal const string SchemeDelimiter = "://";
+ internal const string DefaultServerAddress = "http://localhost:5000";
+
+ internal static Version V1_0 = new Version(1, 0);
+ internal static Version V1_1 = new Version(1, 1);
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/CookedUrl.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/CookedUrl.cs
new file mode 100644
index 0000000000..ee5b2ec53d
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/CookedUrl.cs
@@ -0,0 +1,55 @@
+// 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.Runtime.InteropServices;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ // Note this type should only be used while the request buffer remains pinned
+ internal class CookedUrl
+ {
+ private readonly HttpApiTypes.HTTP_COOKED_URL _nativeCookedUrl;
+
+ internal CookedUrl(HttpApiTypes.HTTP_COOKED_URL nativeCookedUrl)
+ {
+ _nativeCookedUrl = nativeCookedUrl;
+ }
+
+ internal unsafe string GetFullUrl()
+ {
+ if (_nativeCookedUrl.pFullUrl != null && _nativeCookedUrl.FullUrlLength > 0)
+ {
+ return Marshal.PtrToStringUni((IntPtr)_nativeCookedUrl.pFullUrl, _nativeCookedUrl.FullUrlLength / 2);
+ }
+ return null;
+ }
+
+ internal unsafe string GetHost()
+ {
+ if (_nativeCookedUrl.pHost != null && _nativeCookedUrl.HostLength > 0)
+ {
+ return Marshal.PtrToStringUni((IntPtr)_nativeCookedUrl.pHost, _nativeCookedUrl.HostLength / 2);
+ }
+ return null;
+ }
+
+ internal unsafe string GetAbsPath()
+ {
+ if (_nativeCookedUrl.pAbsPath != null && _nativeCookedUrl.AbsPathLength > 0)
+ {
+ return Marshal.PtrToStringUni((IntPtr)_nativeCookedUrl.pAbsPath, _nativeCookedUrl.AbsPathLength / 2);
+ }
+ return null;
+ }
+
+ internal unsafe string GetQueryString()
+ {
+ if (_nativeCookedUrl.pQueryString != null && _nativeCookedUrl.QueryStringLength > 0)
+ {
+ return Marshal.PtrToStringUni((IntPtr)_nativeCookedUrl.pQueryString, _nativeCookedUrl.QueryStringLength / 2);
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HeapAllocHandle.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HeapAllocHandle.cs
new file mode 100644
index 0000000000..9175fd8572
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HeapAllocHandle.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;
+using Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ internal sealed class HeapAllocHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ private static readonly IntPtr ProcessHeap = UnsafeNclNativeMethods.GetProcessHeap();
+
+ // Called by P/Invoke when returning SafeHandles
+ private HeapAllocHandle()
+ : base(ownsHandle: true)
+ {
+ }
+
+ // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you.
+ protected override bool ReleaseHandle()
+ {
+ return UnsafeNclNativeMethods.HeapFree(ProcessHeap, 0, handle);
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HttpApiTypes.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HttpApiTypes.cs
new file mode 100644
index 0000000000..6b3a3b6908
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HttpApiTypes.cs
@@ -0,0 +1,694 @@
+// 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.Runtime.InteropServices;
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ internal static unsafe class HttpApiTypes
+ {
+ internal enum HTTP_API_VERSION
+ {
+ Invalid,
+ Version10,
+ Version20,
+ }
+
+ // see http.w for definitions
+ internal enum HTTP_SERVER_PROPERTY
+ {
+ HttpServerAuthenticationProperty,
+ HttpServerLoggingProperty,
+ HttpServerQosProperty,
+ HttpServerTimeoutsProperty,
+ HttpServerQueueLengthProperty,
+ HttpServerStateProperty,
+ HttpServer503VerbosityProperty,
+ HttpServerBindingProperty,
+ HttpServerExtendedAuthenticationProperty,
+ HttpServerListenEndpointProperty,
+ HttpServerChannelBindProperty,
+ HttpServerProtectionLevelProperty,
+ }
+
+ // Currently only one request info type is supported but the enum is for future extensibility.
+
+ internal enum HTTP_REQUEST_INFO_TYPE
+ {
+ HttpRequestInfoTypeAuth,
+ HttpRequestInfoTypeChannelBind,
+ HttpRequestInfoTypeSslProtocol,
+ HttpRequestInfoTypeSslTokenBinding
+ }
+
+ internal enum HTTP_RESPONSE_INFO_TYPE
+ {
+ HttpResponseInfoTypeMultipleKnownHeaders,
+ HttpResponseInfoTypeAuthenticationProperty,
+ HttpResponseInfoTypeQosProperty,
+ }
+
+ internal enum HTTP_TIMEOUT_TYPE
+ {
+ EntityBody,
+ DrainEntityBody,
+ RequestQueue,
+ IdleConnection,
+ HeaderWait,
+ MinSendRate,
+ }
+
+ internal const int MaxTimeout = 6;
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_VERSION
+ {
+ internal ushort MajorVersion;
+ internal ushort MinorVersion;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_KNOWN_HEADER
+ {
+ internal ushort RawValueLength;
+ internal byte* pRawValue;
+ }
+
+ [StructLayout(LayoutKind.Explicit)]
+ internal struct HTTP_DATA_CHUNK
+ {
+ [FieldOffset(0)]
+ internal HTTP_DATA_CHUNK_TYPE DataChunkType;
+
+ [FieldOffset(8)]
+ internal FromMemory fromMemory;
+
+ [FieldOffset(8)]
+ internal FromFileHandle fromFile;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct FromMemory
+ {
+ // 4 bytes for 32bit, 8 bytes for 64bit
+ internal IntPtr pBuffer;
+ internal uint BufferLength;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct FromFileHandle
+ {
+ internal ulong offset;
+ internal ulong count;
+ internal IntPtr fileHandle;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTPAPI_VERSION
+ {
+ internal ushort HttpApiMajorVersion;
+ internal ushort HttpApiMinorVersion;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_COOKED_URL
+ {
+ internal ushort FullUrlLength;
+ internal ushort HostLength;
+ internal ushort AbsPathLength;
+ internal ushort QueryStringLength;
+ internal ushort* pFullUrl;
+ internal ushort* pHost;
+ internal ushort* pAbsPath;
+ internal ushort* pQueryString;
+ }
+
+ // Only cache unauthorized GETs + HEADs.
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_CACHE_POLICY
+ {
+ internal HTTP_CACHE_POLICY_TYPE Policy;
+ internal uint SecondsToLive;
+ }
+
+ internal enum HTTP_CACHE_POLICY_TYPE : int
+ {
+ HttpCachePolicyNocache = 0,
+ HttpCachePolicyUserInvalidates = 1,
+ HttpCachePolicyTimeToLive = 2,
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct SOCKADDR
+ {
+ internal ushort sa_family;
+ internal byte sa_data;
+ internal byte sa_data_02;
+ internal byte sa_data_03;
+ internal byte sa_data_04;
+ internal byte sa_data_05;
+ internal byte sa_data_06;
+ internal byte sa_data_07;
+ internal byte sa_data_08;
+ internal byte sa_data_09;
+ internal byte sa_data_10;
+ internal byte sa_data_11;
+ internal byte sa_data_12;
+ internal byte sa_data_13;
+ internal byte sa_data_14;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_TRANSPORT_ADDRESS
+ {
+ internal SOCKADDR* pRemoteAddress;
+ internal SOCKADDR* pLocalAddress;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_SSL_CLIENT_CERT_INFO
+ {
+ internal uint CertFlags;
+ internal uint CertEncodedSize;
+ internal byte* pCertEncoded;
+ internal void* Token;
+ internal byte CertDeniedByMapper;
+ }
+
+ internal enum HTTP_SERVICE_BINDING_TYPE : uint
+ {
+ HttpServiceBindingTypeNone = 0,
+ HttpServiceBindingTypeW,
+ HttpServiceBindingTypeA
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_SERVICE_BINDING_BASE
+ {
+ internal HTTP_SERVICE_BINDING_TYPE Type;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_REQUEST_CHANNEL_BIND_STATUS
+ {
+ internal IntPtr ServiceName;
+ internal IntPtr ChannelToken;
+ internal uint ChannelTokenSize;
+ internal uint Flags;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_UNKNOWN_HEADER
+ {
+ internal ushort NameLength;
+ internal ushort RawValueLength;
+ internal byte* pName;
+ internal byte* pRawValue;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_SSL_INFO
+ {
+ internal ushort ServerCertKeySize;
+ internal ushort ConnectionKeySize;
+ internal uint ServerCertIssuerSize;
+ internal uint ServerCertSubjectSize;
+ internal byte* pServerCertIssuer;
+ internal byte* pServerCertSubject;
+ internal HTTP_SSL_CLIENT_CERT_INFO* pClientCertInfo;
+ internal uint SslClientCertNegotiated;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_RESPONSE_HEADERS
+ {
+ internal ushort UnknownHeaderCount;
+ internal HTTP_UNKNOWN_HEADER* pUnknownHeaders;
+ internal ushort TrailerCount;
+ internal HTTP_UNKNOWN_HEADER* pTrailers;
+ internal HTTP_KNOWN_HEADER KnownHeaders;
+ internal HTTP_KNOWN_HEADER KnownHeaders_02;
+ internal HTTP_KNOWN_HEADER KnownHeaders_03;
+ internal HTTP_KNOWN_HEADER KnownHeaders_04;
+ internal HTTP_KNOWN_HEADER KnownHeaders_05;
+ internal HTTP_KNOWN_HEADER KnownHeaders_06;
+ internal HTTP_KNOWN_HEADER KnownHeaders_07;
+ internal HTTP_KNOWN_HEADER KnownHeaders_08;
+ internal HTTP_KNOWN_HEADER KnownHeaders_09;
+ internal HTTP_KNOWN_HEADER KnownHeaders_10;
+ internal HTTP_KNOWN_HEADER KnownHeaders_11;
+ internal HTTP_KNOWN_HEADER KnownHeaders_12;
+ internal HTTP_KNOWN_HEADER KnownHeaders_13;
+ internal HTTP_KNOWN_HEADER KnownHeaders_14;
+ internal HTTP_KNOWN_HEADER KnownHeaders_15;
+ internal HTTP_KNOWN_HEADER KnownHeaders_16;
+ internal HTTP_KNOWN_HEADER KnownHeaders_17;
+ internal HTTP_KNOWN_HEADER KnownHeaders_18;
+ internal HTTP_KNOWN_HEADER KnownHeaders_19;
+ internal HTTP_KNOWN_HEADER KnownHeaders_20;
+ internal HTTP_KNOWN_HEADER KnownHeaders_21;
+ internal HTTP_KNOWN_HEADER KnownHeaders_22;
+ internal HTTP_KNOWN_HEADER KnownHeaders_23;
+ internal HTTP_KNOWN_HEADER KnownHeaders_24;
+ internal HTTP_KNOWN_HEADER KnownHeaders_25;
+ internal HTTP_KNOWN_HEADER KnownHeaders_26;
+ internal HTTP_KNOWN_HEADER KnownHeaders_27;
+ internal HTTP_KNOWN_HEADER KnownHeaders_28;
+ internal HTTP_KNOWN_HEADER KnownHeaders_29;
+ internal HTTP_KNOWN_HEADER KnownHeaders_30;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_REQUEST_HEADERS
+ {
+ internal ushort UnknownHeaderCount;
+ internal HTTP_UNKNOWN_HEADER* pUnknownHeaders;
+ internal ushort TrailerCount;
+ internal HTTP_UNKNOWN_HEADER* pTrailers;
+ internal HTTP_KNOWN_HEADER KnownHeaders;
+ internal HTTP_KNOWN_HEADER KnownHeaders_02;
+ internal HTTP_KNOWN_HEADER KnownHeaders_03;
+ internal HTTP_KNOWN_HEADER KnownHeaders_04;
+ internal HTTP_KNOWN_HEADER KnownHeaders_05;
+ internal HTTP_KNOWN_HEADER KnownHeaders_06;
+ internal HTTP_KNOWN_HEADER KnownHeaders_07;
+ internal HTTP_KNOWN_HEADER KnownHeaders_08;
+ internal HTTP_KNOWN_HEADER KnownHeaders_09;
+ internal HTTP_KNOWN_HEADER KnownHeaders_10;
+ internal HTTP_KNOWN_HEADER KnownHeaders_11;
+ internal HTTP_KNOWN_HEADER KnownHeaders_12;
+ internal HTTP_KNOWN_HEADER KnownHeaders_13;
+ internal HTTP_KNOWN_HEADER KnownHeaders_14;
+ internal HTTP_KNOWN_HEADER KnownHeaders_15;
+ internal HTTP_KNOWN_HEADER KnownHeaders_16;
+ internal HTTP_KNOWN_HEADER KnownHeaders_17;
+ internal HTTP_KNOWN_HEADER KnownHeaders_18;
+ internal HTTP_KNOWN_HEADER KnownHeaders_19;
+ internal HTTP_KNOWN_HEADER KnownHeaders_20;
+ internal HTTP_KNOWN_HEADER KnownHeaders_21;
+ internal HTTP_KNOWN_HEADER KnownHeaders_22;
+ internal HTTP_KNOWN_HEADER KnownHeaders_23;
+ internal HTTP_KNOWN_HEADER KnownHeaders_24;
+ internal HTTP_KNOWN_HEADER KnownHeaders_25;
+ internal HTTP_KNOWN_HEADER KnownHeaders_26;
+ internal HTTP_KNOWN_HEADER KnownHeaders_27;
+ internal HTTP_KNOWN_HEADER KnownHeaders_28;
+ internal HTTP_KNOWN_HEADER KnownHeaders_29;
+ internal HTTP_KNOWN_HEADER KnownHeaders_30;
+ internal HTTP_KNOWN_HEADER KnownHeaders_31;
+ internal HTTP_KNOWN_HEADER KnownHeaders_32;
+ internal HTTP_KNOWN_HEADER KnownHeaders_33;
+ internal HTTP_KNOWN_HEADER KnownHeaders_34;
+ internal HTTP_KNOWN_HEADER KnownHeaders_35;
+ internal HTTP_KNOWN_HEADER KnownHeaders_36;
+ internal HTTP_KNOWN_HEADER KnownHeaders_37;
+ internal HTTP_KNOWN_HEADER KnownHeaders_38;
+ internal HTTP_KNOWN_HEADER KnownHeaders_39;
+ internal HTTP_KNOWN_HEADER KnownHeaders_40;
+ internal HTTP_KNOWN_HEADER KnownHeaders_41;
+ }
+
+ internal enum HTTP_VERB : int
+ {
+ HttpVerbUnparsed = 0,
+ HttpVerbUnknown = 1,
+ HttpVerbInvalid = 2,
+ HttpVerbOPTIONS = 3,
+ HttpVerbGET = 4,
+ HttpVerbHEAD = 5,
+ HttpVerbPOST = 6,
+ HttpVerbPUT = 7,
+ HttpVerbDELETE = 8,
+ HttpVerbTRACE = 9,
+ HttpVerbCONNECT = 10,
+ HttpVerbTRACK = 11,
+ HttpVerbMOVE = 12,
+ HttpVerbCOPY = 13,
+ HttpVerbPROPFIND = 14,
+ HttpVerbPROPPATCH = 15,
+ HttpVerbMKCOL = 16,
+ HttpVerbLOCK = 17,
+ HttpVerbUNLOCK = 18,
+ HttpVerbSEARCH = 19,
+ HttpVerbMaximum = 20,
+ }
+
+ internal static readonly string[] HttpVerbs = new string[]
+ {
+ null,
+ "Unknown",
+ "Invalid",
+ "OPTIONS",
+ "GET",
+ "HEAD",
+ "POST",
+ "PUT",
+ "DELETE",
+ "TRACE",
+ "CONNECT",
+ "TRACK",
+ "MOVE",
+ "COPY",
+ "PROPFIND",
+ "PROPPATCH",
+ "MKCOL",
+ "LOCK",
+ "UNLOCK",
+ "SEARCH",
+ };
+
+ internal enum HTTP_DATA_CHUNK_TYPE : int
+ {
+ HttpDataChunkFromMemory = 0,
+ HttpDataChunkFromFileHandle = 1,
+ HttpDataChunkFromFragmentCache = 2,
+ HttpDataChunkMaximum = 3,
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_RESPONSE_INFO
+ {
+ internal HTTP_RESPONSE_INFO_TYPE Type;
+ internal uint Length;
+ internal HTTP_MULTIPLE_KNOWN_HEADERS* pInfo;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_RESPONSE
+ {
+ internal uint Flags;
+ internal HTTP_VERSION Version;
+ internal ushort StatusCode;
+ internal ushort ReasonLength;
+ internal byte* pReason;
+ internal HTTP_RESPONSE_HEADERS Headers;
+ internal ushort EntityChunkCount;
+ internal HTTP_DATA_CHUNK* pEntityChunks;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_RESPONSE_V2
+ {
+ internal HTTP_RESPONSE Response_V1;
+ internal ushort ResponseInfoCount;
+ internal HTTP_RESPONSE_INFO* pResponseInfo;
+ }
+
+ internal enum HTTP_RESPONSE_INFO_FLAGS : uint
+ {
+ None = 0,
+ PreserveOrder = 1,
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_MULTIPLE_KNOWN_HEADERS
+ {
+ internal HTTP_RESPONSE_HEADER_ID.Enum HeaderId;
+ internal HTTP_RESPONSE_INFO_FLAGS Flags;
+ internal ushort KnownHeaderCount;
+ internal HTTP_KNOWN_HEADER* KnownHeaders;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_REQUEST_AUTH_INFO
+ {
+ internal HTTP_AUTH_STATUS AuthStatus;
+ internal uint SecStatus;
+ internal uint Flags;
+ internal HTTP_REQUEST_AUTH_TYPE AuthType;
+ internal IntPtr AccessToken;
+ internal uint ContextAttributes;
+ internal uint PackedContextLength;
+ internal uint PackedContextType;
+ internal IntPtr PackedContext;
+ internal uint MutualAuthDataLength;
+ internal char* pMutualAuthData;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_REQUEST_INFO
+ {
+ internal HTTP_REQUEST_INFO_TYPE InfoType;
+ internal uint InfoLength;
+ internal HTTP_REQUEST_AUTH_INFO* pInfo;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_REQUEST
+ {
+ internal uint Flags;
+ internal ulong ConnectionId;
+ internal ulong RequestId;
+ internal ulong UrlContext;
+ internal HTTP_VERSION Version;
+ internal HTTP_VERB Verb;
+ internal ushort UnknownVerbLength;
+ internal ushort RawUrlLength;
+ internal byte* pUnknownVerb;
+ internal byte* pRawUrl;
+ internal HTTP_COOKED_URL CookedUrl;
+ internal HTTP_TRANSPORT_ADDRESS Address;
+ internal HTTP_REQUEST_HEADERS Headers;
+ internal ulong BytesReceived;
+ internal ushort EntityChunkCount;
+ internal HTTP_DATA_CHUNK* pEntityChunks;
+ internal ulong RawConnectionId;
+ internal HTTP_SSL_INFO* pSslInfo;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_REQUEST_V2
+ {
+ internal HTTP_REQUEST Request;
+ internal ushort RequestInfoCount;
+ internal HTTP_REQUEST_INFO* pRequestInfo;
+ }
+
+ internal enum HTTP_AUTH_STATUS
+ {
+ HttpAuthStatusSuccess,
+ HttpAuthStatusNotAuthenticated,
+ HttpAuthStatusFailure,
+ }
+
+ internal enum HTTP_REQUEST_AUTH_TYPE
+ {
+ HttpRequestAuthTypeNone = 0,
+ HttpRequestAuthTypeBasic,
+ HttpRequestAuthTypeDigest,
+ HttpRequestAuthTypeNTLM,
+ HttpRequestAuthTypeNegotiate,
+ HttpRequestAuthTypeKerberos
+ }
+
+ internal enum HTTP_QOS_SETTING_TYPE
+ {
+ HttpQosSettingTypeBandwidth,
+ HttpQosSettingTypeConnectionLimit,
+ HttpQosSettingTypeFlowRate
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_SERVER_AUTHENTICATION_INFO
+ {
+ internal HTTP_FLAGS Flags;
+ internal HTTP_AUTH_TYPES AuthSchemes;
+ internal bool ReceiveMutualAuth;
+ internal bool ReceiveContextHandle;
+ internal bool DisableNTLMCredentialCaching;
+ internal ulong ExFlags;
+ HTTP_SERVER_AUTHENTICATION_DIGEST_PARAMS DigestParams;
+ HTTP_SERVER_AUTHENTICATION_BASIC_PARAMS BasicParams;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_SERVER_AUTHENTICATION_DIGEST_PARAMS
+ {
+ internal ushort DomainNameLength;
+ internal char* DomainName;
+ internal ushort RealmLength;
+ internal char* Realm;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_SERVER_AUTHENTICATION_BASIC_PARAMS
+ {
+ ushort RealmLength;
+ char* Realm;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_REQUEST_TOKEN_BINDING_INFO
+ {
+ public byte* TokenBinding;
+ public uint TokenBindingSize;
+
+ public byte* TlsUnique;
+ public uint TlsUniqueSize;
+
+ public char* KeyType;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_TIMEOUT_LIMIT_INFO
+ {
+ internal HTTP_FLAGS Flags;
+ internal ushort EntityBody;
+ internal ushort DrainEntityBody;
+ internal ushort RequestQueue;
+ internal ushort IdleConnection;
+ internal ushort HeaderWait;
+ internal uint MinSendRate;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_BINDING_INFO
+ {
+ internal HTTP_FLAGS Flags;
+ internal IntPtr RequestQueueHandle;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_CONNECTION_LIMIT_INFO
+ {
+ internal HTTP_FLAGS Flags;
+ internal uint MaxConnections;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_QOS_SETTING_INFO
+ {
+ internal HTTP_QOS_SETTING_TYPE QosType;
+ internal IntPtr QosSetting;
+ }
+
+ // see http.w for definitions
+ [Flags]
+ internal enum HTTP_FLAGS : uint
+ {
+ NONE = 0x00000000,
+ HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY = 0x00000001,
+ HTTP_RECEIVE_SECURE_CHANNEL_TOKEN = 0x00000001,
+ HTTP_SEND_RESPONSE_FLAG_DISCONNECT = 0x00000001,
+ HTTP_SEND_RESPONSE_FLAG_MORE_DATA = 0x00000002,
+ HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA = 0x00000004,
+ HTTP_SEND_RESPONSE_FLAG_RAW_HEADER = 0x00000004,
+ HTTP_SEND_REQUEST_FLAG_MORE_DATA = 0x00000001,
+ HTTP_PROPERTY_FLAG_PRESENT = 0x00000001,
+ HTTP_INITIALIZE_SERVER = 0x00000001,
+ HTTP_INITIALIZE_CBT = 0x00000004,
+ HTTP_SEND_RESPONSE_FLAG_OPAQUE = 0x00000040,
+ }
+
+ [Flags]
+ internal enum HTTP_AUTH_TYPES : uint
+ {
+ NONE = 0x00000000,
+ HTTP_AUTH_ENABLE_BASIC = 0x00000001,
+ HTTP_AUTH_ENABLE_DIGEST = 0x00000002,
+ HTTP_AUTH_ENABLE_NTLM = 0x00000004,
+ HTTP_AUTH_ENABLE_NEGOTIATE = 0x00000008,
+ HTTP_AUTH_ENABLE_KERBEROS = 0x00000010,
+ }
+
+ internal static class HTTP_RESPONSE_HEADER_ID
+ {
+ private static string[] _strings =
+ {
+ "Cache-Control",
+ "Connection",
+ "Date",
+ "Keep-Alive",
+ "Pragma",
+ "Trailer",
+ "Transfer-Encoding",
+ "Upgrade",
+ "Via",
+ "Warning",
+
+ "Allow",
+ "Content-Length",
+ "Content-Type",
+ "Content-Encoding",
+ "Content-Language",
+ "Content-Location",
+ "Content-MD5",
+ "Content-Range",
+ "Expires",
+ "Last-Modified",
+
+ "Accept-Ranges",
+ "Age",
+ "ETag",
+ "Location",
+ "Proxy-Authenticate",
+ "Retry-After",
+ "Server",
+ "Set-Cookie",
+ "Vary",
+ "WWW-Authenticate",
+ };
+
+ private static Dictionary<string, int> _lookupTable = CreateLookupTable();
+
+ private static Dictionary<string, int> CreateLookupTable()
+ {
+ Dictionary<string, int> lookupTable = new Dictionary<string, int>((int)Enum.HttpHeaderResponseMaximum, StringComparer.OrdinalIgnoreCase);
+ for (int i = 0; i < (int)Enum.HttpHeaderResponseMaximum; i++)
+ {
+ lookupTable.Add(_strings[i], i);
+ }
+ return lookupTable;
+ }
+
+ internal static int IndexOfKnownHeader(string HeaderName)
+ {
+ int index;
+ return _lookupTable.TryGetValue(HeaderName, out index) ? index : -1;
+ }
+
+ internal enum Enum
+ {
+ HttpHeaderCacheControl = 0, // general-header [section 4.5]
+ HttpHeaderConnection = 1, // general-header [section 4.5]
+ HttpHeaderDate = 2, // general-header [section 4.5]
+ HttpHeaderKeepAlive = 3, // general-header [not in rfc]
+ HttpHeaderPragma = 4, // general-header [section 4.5]
+ HttpHeaderTrailer = 5, // general-header [section 4.5]
+ HttpHeaderTransferEncoding = 6, // general-header [section 4.5]
+ HttpHeaderUpgrade = 7, // general-header [section 4.5]
+ HttpHeaderVia = 8, // general-header [section 4.5]
+ HttpHeaderWarning = 9, // general-header [section 4.5]
+
+ HttpHeaderAllow = 10, // entity-header [section 7.1]
+ HttpHeaderContentLength = 11, // entity-header [section 7.1]
+ HttpHeaderContentType = 12, // entity-header [section 7.1]
+ HttpHeaderContentEncoding = 13, // entity-header [section 7.1]
+ HttpHeaderContentLanguage = 14, // entity-header [section 7.1]
+ HttpHeaderContentLocation = 15, // entity-header [section 7.1]
+ HttpHeaderContentMd5 = 16, // entity-header [section 7.1]
+ HttpHeaderContentRange = 17, // entity-header [section 7.1]
+ HttpHeaderExpires = 18, // entity-header [section 7.1]
+ HttpHeaderLastModified = 19, // entity-header [section 7.1]
+
+ // Response Headers
+
+ HttpHeaderAcceptRanges = 20, // response-header [section 6.2]
+ HttpHeaderAge = 21, // response-header [section 6.2]
+ HttpHeaderEtag = 22, // response-header [section 6.2]
+ HttpHeaderLocation = 23, // response-header [section 6.2]
+ HttpHeaderProxyAuthenticate = 24, // response-header [section 6.2]
+ HttpHeaderRetryAfter = 25, // response-header [section 6.2]
+ HttpHeaderServer = 26, // response-header [section 6.2]
+ HttpHeaderSetCookie = 27, // response-header [not in rfc]
+ HttpHeaderVary = 28, // response-header [section 6.2]
+ HttpHeaderWwwAuthenticate = 29, // response-header [section 6.2]
+
+ HttpHeaderResponseMaximum = 30,
+
+ HttpHeaderMaximum = 41
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HttpSysRequestHeader.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HttpSysRequestHeader.cs
new file mode 100644
index 0000000000..c52444b1be
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HttpSysRequestHeader.cs
@@ -0,0 +1,51 @@
+// 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.HttpSys.Internal
+{
+ internal enum HttpSysRequestHeader
+ {
+ CacheControl = 0, // general-header [section 4.5]
+ Connection = 1, // general-header [section 4.5]
+ Date = 2, // general-header [section 4.5]
+ KeepAlive = 3, // general-header [not in rfc]
+ Pragma = 4, // general-header [section 4.5]
+ Trailer = 5, // general-header [section 4.5]
+ TransferEncoding = 6, // general-header [section 4.5]
+ Upgrade = 7, // general-header [section 4.5]
+ Via = 8, // general-header [section 4.5]
+ Warning = 9, // general-header [section 4.5]
+ Allow = 10, // entity-header [section 7.1]
+ ContentLength = 11, // entity-header [section 7.1]
+ ContentType = 12, // entity-header [section 7.1]
+ ContentEncoding = 13, // entity-header [section 7.1]
+ ContentLanguage = 14, // entity-header [section 7.1]
+ ContentLocation = 15, // entity-header [section 7.1]
+ ContentMd5 = 16, // entity-header [section 7.1]
+ ContentRange = 17, // entity-header [section 7.1]
+ Expires = 18, // entity-header [section 7.1]
+ LastModified = 19, // entity-header [section 7.1]
+
+ Accept = 20, // request-header [section 5.3]
+ AcceptCharset = 21, // request-header [section 5.3]
+ AcceptEncoding = 22, // request-header [section 5.3]
+ AcceptLanguage = 23, // request-header [section 5.3]
+ Authorization = 24, // request-header [section 5.3]
+ Cookie = 25, // request-header [not in rfc]
+ Expect = 26, // request-header [section 5.3]
+ From = 27, // request-header [section 5.3]
+ Host = 28, // request-header [section 5.3]
+ IfMatch = 29, // request-header [section 5.3]
+ IfModifiedSince = 30, // request-header [section 5.3]
+ IfNoneMatch = 31, // request-header [section 5.3]
+ IfRange = 32, // request-header [section 5.3]
+ IfUnmodifiedSince = 33, // request-header [section 5.3]
+ MaxForwards = 34, // request-header [section 5.3]
+ ProxyAuthorization = 35, // request-header [section 5.3]
+ Referer = 36, // request-header [section 5.3]
+ Range = 37, // request-header [section 5.3]
+ Te = 38, // request-header [section 5.3]
+ Translate = 39, // request-header [webDAV, not in rfc 2518]
+ UserAgent = 40, // request-header [section 5.3]
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HttpSysResponseHeader.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HttpSysResponseHeader.cs
new file mode 100644
index 0000000000..5b00f35d29
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/HttpSysResponseHeader.cs
@@ -0,0 +1,40 @@
+// 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.HttpSys.Internal
+{
+ internal enum HttpSysResponseHeader
+ {
+ CacheControl = 0, // general-header [section 4.5]
+ Connection = 1, // general-header [section 4.5]
+ Date = 2, // general-header [section 4.5]
+ KeepAlive = 3, // general-header [not in rfc]
+ Pragma = 4, // general-header [section 4.5]
+ Trailer = 5, // general-header [section 4.5]
+ TransferEncoding = 6, // general-header [section 4.5]
+ Upgrade = 7, // general-header [section 4.5]
+ Via = 8, // general-header [section 4.5]
+ Warning = 9, // general-header [section 4.5]
+ Allow = 10, // entity-header [section 7.1]
+ ContentLength = 11, // entity-header [section 7.1]
+ ContentType = 12, // entity-header [section 7.1]
+ ContentEncoding = 13, // entity-header [section 7.1]
+ ContentLanguage = 14, // entity-header [section 7.1]
+ ContentLocation = 15, // entity-header [section 7.1]
+ ContentMd5 = 16, // entity-header [section 7.1]
+ ContentRange = 17, // entity-header [section 7.1]
+ Expires = 18, // entity-header [section 7.1]
+ LastModified = 19, // entity-header [section 7.1]
+
+ AcceptRanges = 20, // response-header [section 6.2]
+ Age = 21, // response-header [section 6.2]
+ ETag = 22, // response-header [section 6.2]
+ Location = 23, // response-header [section 6.2]
+ ProxyAuthenticate = 24, // response-header [section 6.2]
+ RetryAfter = 25, // response-header [section 6.2]
+ Server = 26, // response-header [section 6.2]
+ SetCookie = 27, // response-header [not in rfc]
+ Vary = 28, // response-header [section 6.2]
+ WwwAuthenticate = 29, // response-header [section 6.2]
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/NclUtilities.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/NclUtilities.cs
new file mode 100644
index 0000000000..504e82a300
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/NclUtilities.cs
@@ -0,0 +1,19 @@
+// 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.HttpSys.Internal
+{
+ internal static class NclUtilities
+ {
+ internal static bool HasShutdownStarted
+ {
+ get
+ {
+ return Environment.HasShutdownStarted
+ || AppDomain.CurrentDomain.IsFinalizingForUnload();
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SafeLocalFreeChannelBinding.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SafeLocalFreeChannelBinding.cs
new file mode 100644
index 0000000000..4c83257e5d
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SafeLocalFreeChannelBinding.cs
@@ -0,0 +1,47 @@
+// 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.Authentication.ExtendedProtection;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ internal class SafeLocalFreeChannelBinding : ChannelBinding
+ {
+ private const int LMEM_FIXED = 0;
+ private int size;
+
+ public override int Size
+ {
+ get { return size; }
+ }
+
+ public static SafeLocalFreeChannelBinding LocalAlloc(int cb)
+ {
+ SafeLocalFreeChannelBinding result;
+
+ result = UnsafeNclNativeMethods.SafeNetHandles.LocalAllocChannelBinding(LMEM_FIXED, (UIntPtr)cb);
+ if (result.IsInvalid)
+ {
+ result.SetHandleAsInvalid();
+ throw new OutOfMemoryException();
+ }
+
+ result.size = cb;
+ return result;
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ return UnsafeNclNativeMethods.SafeNetHandles.LocalFree(handle) == IntPtr.Zero;
+ }
+
+ public override bool IsInvalid
+ {
+ get
+ {
+ return handle == IntPtr.Zero || handle.ToInt32() == -1;
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SafeLocalMemHandle.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SafeLocalMemHandle.cs
new file mode 100644
index 0000000000..75fb508714
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SafeLocalMemHandle.cs
@@ -0,0 +1,27 @@
+// 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.Win32.SafeHandles;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ internal sealed class SafeLocalMemHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ internal SafeLocalMemHandle()
+ : base(true)
+ {
+ }
+
+ internal SafeLocalMemHandle(IntPtr existingHandle, bool ownsHandle)
+ : base(ownsHandle)
+ {
+ SetHandle(existingHandle);
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ return UnsafeNclNativeMethods.SafeNetHandles.LocalFree(handle) == IntPtr.Zero;
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SafeNativeOverlapped.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SafeNativeOverlapped.cs
new file mode 100644
index 0000000000..01ba3c32f2
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SafeNativeOverlapped.cs
@@ -0,0 +1,62 @@
+// 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.Runtime.InteropServices;
+using System.Threading;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ internal class SafeNativeOverlapped : SafeHandle
+ {
+ internal static readonly SafeNativeOverlapped Zero = new SafeNativeOverlapped();
+ private ThreadPoolBoundHandle _boundHandle;
+
+ internal SafeNativeOverlapped()
+ : base(IntPtr.Zero, true)
+ {
+ }
+
+ internal unsafe SafeNativeOverlapped(ThreadPoolBoundHandle boundHandle, NativeOverlapped* handle)
+ : base(IntPtr.Zero, true)
+ {
+ SetHandle((IntPtr)handle);
+ _boundHandle = boundHandle;
+ }
+
+ public override bool IsInvalid
+ {
+ get { return handle == IntPtr.Zero; }
+ }
+
+ public void ReinitializeNativeOverlapped()
+ {
+ IntPtr handleSnapshot = handle;
+
+ if (handleSnapshot != IntPtr.Zero)
+ {
+ unsafe
+ {
+ ((NativeOverlapped*)handleSnapshot)->InternalHigh = IntPtr.Zero;
+ ((NativeOverlapped*)handleSnapshot)->InternalLow = IntPtr.Zero;
+ ((NativeOverlapped*)handleSnapshot)->EventHandle = IntPtr.Zero;
+ }
+ }
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ IntPtr oldHandle = Interlocked.Exchange(ref handle, IntPtr.Zero);
+ // Do not call free durring AppDomain shutdown, there may be an outstanding operation.
+ // Overlapped will take care calling free when the native callback completes.
+ if (oldHandle != IntPtr.Zero && !NclUtilities.HasShutdownStarted)
+ {
+ unsafe
+ {
+ _boundHandle.FreeNativeOverlapped((NativeOverlapped*)oldHandle);
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SocketAddress.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SocketAddress.cs
new file mode 100644
index 0000000000..fbe82fa7bc
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/SocketAddress.cs
@@ -0,0 +1,371 @@
+// 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.Contracts;
+using System.Globalization;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ // a little perf app measured these times when comparing the internal
+ // buffer implemented as a managed byte[] or unmanaged memory IntPtr
+ // that's why we use byte[]
+ // byte[] total ms:19656
+ // IntPtr total ms:25671
+
+ /// <devdoc>
+ /// <para>
+ /// This class is used when subclassing EndPoint, and provides indication
+ /// on how to format the memory buffers that winsock uses for network addresses.
+ /// </para>
+ /// </devdoc>
+ internal class SocketAddress
+ {
+ private const int NumberOfIPv6Labels = 8;
+ // Lower case hex, no leading zeros
+ private const string IPv6NumberFormat = "{0:x}";
+ private const string IPv6StringSeparator = ":";
+ private const string IPv4StringFormat = "{0:d}.{1:d}.{2:d}.{3:d}";
+
+ internal const int IPv6AddressSize = 28;
+ internal const int IPv4AddressSize = 16;
+
+ private const int WriteableOffset = 2;
+
+ private int _size;
+ private byte[] _buffer;
+ private int _hash;
+
+ /// <devdoc>
+ /// <para>[To be supplied.]</para>
+ /// </devdoc>
+ public SocketAddress(AddressFamily family, int size)
+ {
+ if (size < WriteableOffset)
+ {
+ // it doesn't make sense to create a socket address with less tha
+ // 2 bytes, that's where we store the address family.
+
+ throw new ArgumentOutOfRangeException("size");
+ }
+ _size = size;
+ _buffer = new byte[((size / IntPtr.Size) + 2) * IntPtr.Size]; // sizeof DWORD
+
+#if BIGENDIAN
+ m_Buffer[0] = unchecked((byte)((int)family>>8));
+ m_Buffer[1] = unchecked((byte)((int)family ));
+#else
+ _buffer[0] = unchecked((byte)((int)family));
+ _buffer[1] = unchecked((byte)((int)family >> 8));
+#endif
+ }
+
+ internal byte[] Buffer
+ {
+ get { return _buffer; }
+ }
+
+ internal AddressFamily Family
+ {
+ get
+ {
+ int family;
+#if BIGENDIAN
+ family = ((int)m_Buffer[0]<<8) | m_Buffer[1];
+#else
+ family = _buffer[0] | ((int)_buffer[1] << 8);
+#endif
+ return (AddressFamily)family;
+ }
+ }
+
+ internal int Size
+ {
+ get
+ {
+ return _size;
+ }
+ }
+
+ // access to unmanaged serialized data. this doesn't
+ // allow access to the first 2 bytes of unmanaged memory
+ // that are supposed to contain the address family which
+ // is readonly.
+ //
+ // <SECREVIEW> you can still use negative offsets as a back door in case
+ // winsock changes the way it uses SOCKADDR. maybe we want to prohibit it?
+ // maybe we should make the class sealed to avoid potentially dangerous calls
+ // into winsock with unproperly formatted data? </SECREVIEW>
+
+ /// <devdoc>
+ /// <para>[To be supplied.]</para>
+ /// </devdoc>
+ private byte this[int offset]
+ {
+ get
+ {
+ // access
+ if (offset < 0 || offset >= Size)
+ {
+ throw new ArgumentOutOfRangeException("offset");
+ }
+ return _buffer[offset];
+ }
+ }
+
+ internal int GetPort()
+ {
+ return (int)((_buffer[2] << 8 & 0xFF00) | (_buffer[3]));
+ }
+
+ public override bool Equals(object comparand)
+ {
+ SocketAddress castedComparand = comparand as SocketAddress;
+ if (castedComparand == null || this.Size != castedComparand.Size)
+ {
+ return false;
+ }
+ for (int i = 0; i < this.Size; i++)
+ {
+ if (this[i] != castedComparand[i])
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public override int GetHashCode()
+ {
+ if (_hash == 0)
+ {
+ int i;
+ int size = Size & ~3;
+
+ for (i = 0; i < size; i += 4)
+ {
+ _hash ^= (int)_buffer[i]
+ | ((int)_buffer[i + 1] << 8)
+ | ((int)_buffer[i + 2] << 16)
+ | ((int)_buffer[i + 3] << 24);
+ }
+ if ((Size & 3) != 0)
+ {
+ int remnant = 0;
+ int shift = 0;
+
+ for (; i < Size; ++i)
+ {
+ remnant |= ((int)_buffer[i]) << shift;
+ shift += 8;
+ }
+ _hash ^= remnant;
+ }
+ }
+ return _hash;
+ }
+
+ internal IPAddress GetIPAddress()
+ {
+ if (Family == AddressFamily.InterNetworkV6)
+ {
+ return GetIpv6Address();
+ }
+ else if (Family == AddressFamily.InterNetwork)
+ {
+ return GetIPv4Address();
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ private IPAddress GetIpv6Address()
+ {
+ Contract.Assert(Size >= IPv6AddressSize);
+ byte[] bytes = new byte[NumberOfIPv6Labels * 2];
+ Array.Copy(_buffer, 8, bytes, 0, NumberOfIPv6Labels * 2);
+ return new IPAddress(bytes); // TODO: Does scope id matter?
+ }
+
+ private IPAddress GetIPv4Address()
+ {
+ Contract.Assert(Size >= IPv4AddressSize);
+ return new IPAddress(new byte[] { _buffer[4], _buffer[5], _buffer[6], _buffer[7] });
+ }
+
+ public override string ToString()
+ {
+ StringBuilder bytes = new StringBuilder();
+ for (int i = WriteableOffset; i < this.Size; i++)
+ {
+ if (i > WriteableOffset)
+ {
+ bytes.Append(",");
+ }
+ bytes.Append(this[i].ToString(NumberFormatInfo.InvariantInfo));
+ }
+ return Family.ToString() + ":" + Size.ToString(NumberFormatInfo.InvariantInfo) + ":{" + bytes.ToString() + "}";
+ }
+
+ internal string GetIPAddressString()
+ {
+ if (Family == AddressFamily.InterNetworkV6)
+ {
+ return GetIpv6AddressString();
+ }
+ else if (Family == AddressFamily.InterNetwork)
+ {
+ return GetIPv4AddressString();
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ private string GetIPv4AddressString()
+ {
+ Contract.Assert(Size >= IPv4AddressSize);
+
+ return string.Format(CultureInfo.InvariantCulture, IPv4StringFormat,
+ _buffer[4], _buffer[5], _buffer[6], _buffer[7]);
+ }
+
+ // TODO: Does scope ID ever matter?
+ private unsafe string GetIpv6AddressString()
+ {
+ Contract.Assert(Size >= IPv6AddressSize);
+
+ fixed (byte* rawBytes = _buffer)
+ {
+ // Convert from bytes to shorts.
+ ushort* rawShorts = stackalloc ushort[NumberOfIPv6Labels];
+ int numbersOffset = 0;
+ // The address doesn't start at the beginning of the buffer.
+ for (int i = 8; i < ((NumberOfIPv6Labels * 2) + 8); i += 2)
+ {
+ rawShorts[numbersOffset++] = (ushort)(rawBytes[i] << 8 | rawBytes[i + 1]);
+ }
+ return GetIPv6AddressString(rawShorts);
+ }
+ }
+
+ private static unsafe string GetIPv6AddressString(ushort* numbers)
+ {
+ // RFC 5952 Sections 4 & 5 - Compressed, lower case, with possible embedded IPv4 addresses.
+
+ // Start to finish, inclusive. <-1, -1> for no compression
+ KeyValuePair<int, int> range = FindCompressionRange(numbers);
+ bool ipv4Embedded = ShouldHaveIpv4Embedded(numbers);
+
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < NumberOfIPv6Labels; i++)
+ {
+ if (ipv4Embedded && i == (NumberOfIPv6Labels - 2))
+ {
+ // Write the remaining digits as an IPv4 address
+ builder.Append(IPv6StringSeparator);
+ builder.Append(string.Format(CultureInfo.InvariantCulture, IPv4StringFormat,
+ numbers[i] >> 8, numbers[i] & 0xFF, numbers[i + 1] >> 8, numbers[i + 1] & 0xFF));
+ break;
+ }
+
+ // Compression; 1::1, ::1, 1::
+ if (range.Key == i)
+ {
+ // Start compression, add :
+ builder.Append(IPv6StringSeparator);
+ }
+ if (range.Key <= i && range.Value == (NumberOfIPv6Labels - 1))
+ {
+ // Remainder compressed; 1::
+ builder.Append(IPv6StringSeparator);
+ break;
+ }
+ if (range.Key <= i && i <= range.Value)
+ {
+ continue; // Compressed
+ }
+
+ if (i != 0)
+ {
+ builder.Append(IPv6StringSeparator);
+ }
+ builder.Append(string.Format(CultureInfo.InvariantCulture, IPv6NumberFormat, numbers[i]));
+ }
+
+ return builder.ToString();
+ }
+
+ // RFC 5952 Section 4.2.3
+ // Longest consecutive sequence of zero segments, minimum 2.
+ // On equal, first sequence wins.
+ // <-1, -1> for no compression.
+ private static unsafe KeyValuePair<int, int> FindCompressionRange(ushort* numbers)
+ {
+ int longestSequenceLength = 0;
+ int longestSequenceStart = -1;
+
+ int currentSequenceLength = 0;
+ for (int i = 0; i < NumberOfIPv6Labels; i++)
+ {
+ if (numbers[i] == 0)
+ {
+ // In a sequence
+ currentSequenceLength++;
+ if (currentSequenceLength > longestSequenceLength)
+ {
+ longestSequenceLength = currentSequenceLength;
+ longestSequenceStart = i - currentSequenceLength + 1;
+ }
+ }
+ else
+ {
+ currentSequenceLength = 0;
+ }
+ }
+
+ if (longestSequenceLength >= 2)
+ {
+ return new KeyValuePair<int, int>(longestSequenceStart,
+ longestSequenceStart + longestSequenceLength - 1);
+ }
+
+ return new KeyValuePair<int, int>(-1, -1); // No compression
+ }
+
+ // Returns true if the IPv6 address should be formated with an embedded IPv4 address:
+ // ::192.168.1.1
+ private static unsafe bool ShouldHaveIpv4Embedded(ushort* numbers)
+ {
+ // 0:0 : 0:0 : x:x : x.x.x.x
+ if (numbers[0] == 0 && numbers[1] == 0 && numbers[2] == 0 && numbers[3] == 0 && numbers[6] != 0)
+ {
+ // RFC 5952 Section 5 - 0:0 : 0:0 : 0:[0 | FFFF] : x.x.x.x
+ if (numbers[4] == 0 && (numbers[5] == 0 || numbers[5] == 0xFFFF))
+ {
+ return true;
+
+ // SIIT - 0:0 : 0:0 : FFFF:0 : x.x.x.x
+ }
+ else if (numbers[4] == 0xFFFF && numbers[5] == 0)
+ {
+ return true;
+ }
+ }
+ // ISATAP
+ if (numbers[4] == 0 && numbers[5] == 0x5EFE)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ } // class SocketAddress
+} // namespace System.Net
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/UnsafeNativeMethods.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/UnsafeNativeMethods.cs
new file mode 100644
index 0000000000..4483cbc306
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/NativeInterop/UnsafeNativeMethods.cs
@@ -0,0 +1,155 @@
+// 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.CodeAnalysis;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ internal static unsafe class UnsafeNclNativeMethods
+ {
+ private const string sspicli_LIB = "sspicli.dll";
+ private const string api_ms_win_core_processthreads_LIB = "api-ms-win-core-processthreads-l1-1-1.dll";
+ private const string api_ms_win_core_io_LIB = "api-ms-win-core-io-l1-1-0.dll";
+ private const string api_ms_win_core_handle_LIB = "api-ms-win-core-handle-l1-1-0.dll";
+ private const string api_ms_win_core_libraryloader_LIB = "api-ms-win-core-libraryloader-l1-1-0.dll";
+ private const string api_ms_win_core_heap_LIB = "api-ms-win-core-heap-L1-2-0.dll";
+ private const string api_ms_win_core_heap_obsolete_LIB = "api-ms-win-core-heap-obsolete-L1-1-0.dll";
+ private const string api_ms_win_core_kernel32_legacy_LIB = "api-ms-win-core-kernel32-legacy-l1-1-0.dll";
+
+ private const string TOKENBINDING = "tokenbinding.dll";
+
+ // CONSIDER: Make this an enum, requires changing a lot of types from uint to ErrorCodes.
+ internal static class ErrorCodes
+ {
+ internal const uint ERROR_SUCCESS = 0;
+ internal const uint ERROR_HANDLE_EOF = 38;
+ internal const uint ERROR_NOT_SUPPORTED = 50;
+ internal const uint ERROR_INVALID_PARAMETER = 87;
+ internal const uint ERROR_ALREADY_EXISTS = 183;
+ internal const uint ERROR_MORE_DATA = 234;
+ internal const uint ERROR_OPERATION_ABORTED = 995;
+ internal const uint ERROR_IO_PENDING = 997;
+ internal const uint ERROR_NOT_FOUND = 1168;
+ internal const uint ERROR_CONNECTION_INVALID = 1229;
+ }
+
+ [DllImport(api_ms_win_core_io_LIB, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static unsafe extern uint CancelIoEx(SafeHandle handle, SafeNativeOverlapped overlapped);
+
+ [DllImport(api_ms_win_core_kernel32_legacy_LIB, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static unsafe extern bool SetFileCompletionNotificationModes(SafeHandle handle, FileCompletionNotificationModes modes);
+
+ [Flags]
+ internal enum FileCompletionNotificationModes : byte
+ {
+ None = 0,
+ SkipCompletionPortOnSuccess = 1,
+ SkipSetEventOnHandle = 2
+ }
+
+ [DllImport(TOKENBINDING, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
+ public static extern int TokenBindingVerifyMessage(
+ [In] byte* tokenBindingMessage,
+ [In] uint tokenBindingMessageSize,
+ [In] char* keyType,
+ [In] byte* tlsUnique,
+ [In] uint tlsUniqueSize,
+ [Out] out HeapAllocHandle resultList);
+
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa366569(v=vs.85).aspx
+ [DllImport(api_ms_win_core_heap_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ internal static extern IntPtr GetProcessHeap();
+
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa366701(v=vs.85).aspx
+ [DllImport(api_ms_win_core_heap_LIB, CallingConvention = CallingConvention.Winapi, SetLastError = true)]
+ internal static extern bool HeapFree(
+ [In] IntPtr hHeap,
+ [In] uint dwFlags,
+ [In] IntPtr lpMem);
+
+ internal static class SafeNetHandles
+ {
+ [DllImport(sspicli_LIB, ExactSpelling = true, SetLastError = true)]
+ internal static extern int FreeContextBuffer(
+ [In] IntPtr contextBuffer);
+
+ [DllImport(api_ms_win_core_handle_LIB, ExactSpelling = true, SetLastError = true)]
+ internal static extern bool CloseHandle(IntPtr handle);
+
+ [DllImport(api_ms_win_core_heap_obsolete_LIB, EntryPoint = "LocalAlloc", SetLastError = true)]
+ internal static extern SafeLocalFreeChannelBinding LocalAllocChannelBinding(int uFlags, UIntPtr sizetdwBytes);
+
+ [DllImport(api_ms_win_core_heap_obsolete_LIB, ExactSpelling = true, SetLastError = true)]
+ internal static extern IntPtr LocalFree(IntPtr handle);
+ }
+
+ // from tokenbinding.h
+ internal static class TokenBinding
+ {
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct TOKENBINDING_RESULT_DATA
+ {
+ public uint identifierSize;
+ public TOKENBINDING_IDENTIFIER* identifierData;
+ public TOKENBINDING_EXTENSION_FORMAT extensionFormat;
+ public uint extensionSize;
+ public IntPtr extensionData;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct TOKENBINDING_IDENTIFIER
+ {
+ // Note: If the layout of these fields changes, be sure to make the
+ // corresponding change to TokenBindingUtil.ExtractIdentifierBlob.
+
+ public TOKENBINDING_TYPE bindingType;
+ public TOKENBINDING_HASH_ALGORITHM hashAlgorithm;
+ public TOKENBINDING_SIGNATURE_ALGORITHM signatureAlgorithm;
+ }
+
+ internal enum TOKENBINDING_TYPE : byte
+ {
+ TOKENBINDING_TYPE_PROVIDED = 0,
+ TOKENBINDING_TYPE_REFERRED = 1,
+ }
+
+ internal enum TOKENBINDING_HASH_ALGORITHM : byte
+ {
+ TOKENBINDING_HASH_ALGORITHM_SHA256 = 4,
+ }
+
+ internal enum TOKENBINDING_SIGNATURE_ALGORITHM : byte
+ {
+ TOKENBINDING_SIGNATURE_ALGORITHM_RSA = 1,
+ TOKENBINDING_SIGNATURE_ALGORITHM_ECDSAP256 = 3,
+ }
+
+ internal enum TOKENBINDING_EXTENSION_FORMAT
+ {
+ TOKENBINDING_EXTENSION_FORMAT_UNDEFINED = 0,
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct TOKENBINDING_RESULT_LIST
+ {
+ public uint resultCount;
+ public TOKENBINDING_RESULT_DATA* resultData;
+ }
+ }
+
+ // DACL related stuff
+
+ [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated natively")]
+ [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
+ Justification = "Does not own the resource.")]
+ [StructLayout(LayoutKind.Sequential)]
+ internal class SECURITY_ATTRIBUTES
+ {
+ public int nLength = 12;
+ public SafeLocalMemHandle lpSecurityDescriptor = new SafeLocalMemHandle(IntPtr.Zero, false);
+ public bool bInheritHandle = false;
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HeaderCollection.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HeaderCollection.cs
new file mode 100644
index 0000000000..504c434667
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HeaderCollection.cs
@@ -0,0 +1,243 @@
+// 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;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ internal class HeaderCollection : IHeaderDictionary
+ {
+ private long? _contentLength;
+ private StringValues _contentLengthText;
+
+ public HeaderCollection()
+ : this(new Dictionary<string, StringValues>(4, StringComparer.OrdinalIgnoreCase))
+ {
+ }
+
+ public HeaderCollection(IDictionary<string, StringValues> store)
+ {
+ Store = store;
+ }
+
+ private IDictionary<string, StringValues> Store { get; set; }
+
+ // Readonly after the response has been started.
+ public bool IsReadOnly { get; internal set; }
+
+ public StringValues this[string key]
+ {
+ get
+ {
+ StringValues values;
+ return TryGetValue(key, out values) ? values : StringValues.Empty;
+ }
+ set
+ {
+ ThrowIfReadOnly();
+ if (StringValues.IsNullOrEmpty(value))
+ {
+ Remove(key);
+ }
+ else
+ {
+ ValidateHeaderCharacters(key);
+ ValidateHeaderCharacters(value);
+ Store[key] = value;
+ }
+ }
+ }
+
+ StringValues IDictionary<string, StringValues>.this[string key]
+ {
+ get { return Store[key]; }
+ set
+ {
+ ThrowIfReadOnly();
+ ValidateHeaderCharacters(key);
+ ValidateHeaderCharacters(value);
+ Store[key] = value;
+ }
+ }
+
+ public int Count
+ {
+ get { return Store.Count; }
+ }
+
+ public ICollection<string> Keys
+ {
+ get { return Store.Keys; }
+ }
+
+ public ICollection<StringValues> Values
+ {
+ get { return Store.Values; }
+ }
+
+ public long? ContentLength
+ {
+ get
+ {
+ long value;
+ var rawValue = this[HttpKnownHeaderNames.ContentLength];
+
+ if (_contentLengthText.Equals(rawValue))
+ {
+ return _contentLength;
+ }
+
+ if (rawValue.Count == 1 &&
+ !string.IsNullOrWhiteSpace(rawValue[0]) &&
+ HeaderUtilities.TryParseNonNegativeInt64(new StringSegment(rawValue[0]).Trim(), out value))
+ {
+ _contentLengthText = rawValue;
+ _contentLength = value;
+ return value;
+ }
+
+ return null;
+ }
+ set
+ {
+ ThrowIfReadOnly();
+
+ if (value.HasValue)
+ {
+ if (value.Value < 0)
+ {
+ throw new ArgumentOutOfRangeException("value", value.Value, "Cannot be negative.");
+ }
+ _contentLengthText = HeaderUtilities.FormatNonNegativeInt64(value.Value);
+ this[HttpKnownHeaderNames.ContentLength] = _contentLengthText;
+ _contentLength = value;
+ }
+ else
+ {
+ Remove(HttpKnownHeaderNames.ContentLength);
+ _contentLengthText = StringValues.Empty;
+ _contentLength = null;
+ }
+ }
+ }
+
+ public void Add(KeyValuePair<string, StringValues> item)
+ {
+ ThrowIfReadOnly();
+ ValidateHeaderCharacters(item.Key);
+ ValidateHeaderCharacters(item.Value);
+ Store.Add(item);
+ }
+
+ public void Add(string key, StringValues value)
+ {
+ ThrowIfReadOnly();
+ ValidateHeaderCharacters(key);
+ ValidateHeaderCharacters(value);
+ Store.Add(key, value);
+ }
+
+ public void Append(string key, string value)
+ {
+ ThrowIfReadOnly();
+ ValidateHeaderCharacters(key);
+ ValidateHeaderCharacters(value);
+ StringValues values;
+ Store.TryGetValue(key, out values);
+ Store[key] = StringValues.Concat(values, value);
+ }
+
+ public void Clear()
+ {
+ ThrowIfReadOnly();
+ Store.Clear();
+ }
+
+ public bool Contains(KeyValuePair<string, StringValues> item)
+ {
+ return Store.Contains(item);
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return Store.ContainsKey(key);
+ }
+
+ public void CopyTo(KeyValuePair<string, StringValues>[] array, int arrayIndex)
+ {
+ Store.CopyTo(array, arrayIndex);
+ }
+
+ public IEnumerator<KeyValuePair<string, StringValues>> GetEnumerator()
+ {
+ return Store.GetEnumerator();
+ }
+
+ public IEnumerable<string> GetValues(string key)
+ {
+ StringValues values;
+ if (Store.TryGetValue(key, out values))
+ {
+ return HeaderParser.SplitValues(values);
+ }
+ return HeaderParser.Empty;
+ }
+
+ public bool Remove(KeyValuePair<string, StringValues> item)
+ {
+ ThrowIfReadOnly();
+ return Store.Remove(item);
+ }
+
+ public bool Remove(string key)
+ {
+ ThrowIfReadOnly();
+ return Store.Remove(key);
+ }
+
+ public bool TryGetValue(string key, out StringValues value)
+ {
+ return Store.TryGetValue(key, out value);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ private void ThrowIfReadOnly()
+ {
+ if (IsReadOnly)
+ {
+ throw new InvalidOperationException("The response headers cannot be modified because the response has already started.");
+ }
+ }
+
+ public static void ValidateHeaderCharacters(StringValues headerValues)
+ {
+ foreach (var value in headerValues)
+ {
+ ValidateHeaderCharacters(value);
+ }
+ }
+
+ public static void ValidateHeaderCharacters(string headerCharacters)
+ {
+ if (headerCharacters != null)
+ {
+ foreach (var ch in headerCharacters)
+ {
+ if (ch < 0x20)
+ {
+ throw new InvalidOperationException(string.Format("Invalid control character in header: 0x{0:X2}", (byte)ch));
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HeaderEncoding.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HeaderEncoding.cs
new file mode 100644
index 0000000000..991571904b
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HeaderEncoding.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.Text;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ internal static class HeaderEncoding
+ {
+ // It should just be ASCII or ANSI, but they break badly with un-expected values. We use UTF-8 because it's the same for
+ // ASCII, and because some old client would send UTF8 Host headers and expect UTF8 Location responses
+ // (e.g. IE and HttpWebRequest on intranets).
+ private static Encoding Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false);
+
+ internal static unsafe string GetString(byte* pBytes, int byteCount)
+ {
+ // net451: return new string(pBytes, 0, byteCount, Encoding);
+
+ var charCount = Encoding.GetCharCount(pBytes, byteCount);
+ var chars = new char[charCount];
+ fixed (char* pChars = chars)
+ {
+ var count = Encoding.GetChars(pBytes, byteCount, pChars, charCount);
+ System.Diagnostics.Debug.Assert(count == charCount);
+ }
+ return new string(chars);
+ }
+
+ internal static byte[] GetBytes(string myString)
+ {
+ return Encoding.GetBytes(myString);
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HeaderParser.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HeaderParser.cs
new file mode 100644
index 0000000000..770a3ee3a7
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HeaderParser.cs
@@ -0,0 +1,58 @@
+// 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 Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ internal static class HeaderParser
+ {
+ internal static IEnumerable<string> Empty = new string[0];
+
+ // Split on commas, except in quotes
+ internal static IEnumerable<string> SplitValues(StringValues values)
+ {
+ foreach (var value in values)
+ {
+ int start = 0;
+ bool inQuotes = false;
+ int current = 0;
+ for ( ; current < value.Length; current++)
+ {
+ char ch = value[current];
+ if (inQuotes)
+ {
+ if (ch == '"')
+ {
+ inQuotes = false;
+ }
+ }
+ else if (ch == '"')
+ {
+ inQuotes = true;
+ }
+ else if (ch == ',')
+ {
+ var subValue = value.Substring(start, current - start);
+ if (!string.IsNullOrWhiteSpace(subValue))
+ {
+ yield return subValue.Trim();
+ start = current + 1;
+ }
+ }
+ }
+
+ if (start < current)
+ {
+ var subValue = value.Substring(start, current - start);
+ if (!string.IsNullOrWhiteSpace(subValue))
+ {
+ yield return subValue.Trim();
+ start = current + 1;
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HttpKnownHeaderNames.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HttpKnownHeaderNames.cs
new file mode 100644
index 0000000000..ac299f9785
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/HttpKnownHeaderNames.cs
@@ -0,0 +1,72 @@
+// 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.HttpSys.Internal
+{
+ internal static class HttpKnownHeaderNames
+ {
+ internal const string CacheControl = "Cache-Control";
+ internal const string Connection = "Connection";
+ internal const string Date = "Date";
+ internal const string KeepAlive = "Keep-Alive";
+ internal const string Pragma = "Pragma";
+ internal const string ProxyConnection = "Proxy-Connection";
+ internal const string Trailer = "Trailer";
+ internal const string TransferEncoding = "Transfer-Encoding";
+ internal const string Upgrade = "Upgrade";
+ internal const string Via = "Via";
+ internal const string Warning = "Warning";
+ internal const string ContentLength = "Content-Length";
+ internal const string ContentType = "Content-Type";
+ internal const string ContentDisposition = "Content-Disposition";
+ internal const string ContentEncoding = "Content-Encoding";
+ internal const string ContentLanguage = "Content-Language";
+ internal const string ContentLocation = "Content-Location";
+ internal const string ContentRange = "Content-Range";
+ internal const string Expires = "Expires";
+ internal const string LastModified = "Last-Modified";
+ internal const string Age = "Age";
+ internal const string Location = "Location";
+ internal const string ProxyAuthenticate = "Proxy-Authenticate";
+ internal const string RetryAfter = "Retry-After";
+ internal const string Server = "Server";
+ internal const string SetCookie = "Set-Cookie";
+ internal const string SetCookie2 = "Set-Cookie2";
+ internal const string Vary = "Vary";
+ internal const string WWWAuthenticate = "WWW-Authenticate";
+ internal const string Accept = "Accept";
+ internal const string AcceptCharset = "Accept-Charset";
+ internal const string AcceptEncoding = "Accept-Encoding";
+ internal const string AcceptLanguage = "Accept-Language";
+ internal const string Authorization = "Authorization";
+ internal const string Cookie = "Cookie";
+ internal const string Cookie2 = "Cookie2";
+ internal const string Expect = "Expect";
+ internal const string From = "From";
+ internal const string Host = "Host";
+ internal const string IfMatch = "If-Match";
+ internal const string IfModifiedSince = "If-Modified-Since";
+ internal const string IfNoneMatch = "If-None-Match";
+ internal const string IfRange = "If-Range";
+ internal const string IfUnmodifiedSince = "If-Unmodified-Since";
+ internal const string MaxForwards = "Max-Forwards";
+ internal const string ProxyAuthorization = "Proxy-Authorization";
+ internal const string Referer = "Referer";
+ internal const string Range = "Range";
+ internal const string UserAgent = "User-Agent";
+ internal const string ContentMD5 = "Content-MD5";
+ internal const string ETag = "ETag";
+ internal const string TE = "TE";
+ internal const string Allow = "Allow";
+ internal const string AcceptRanges = "Accept-Ranges";
+ internal const string P3P = "P3P";
+ internal const string XPoweredBy = "X-Powered-By";
+ internal const string XAspNetVersion = "X-AspNet-Version";
+ internal const string SecWebSocketKey = "Sec-WebSocket-Key";
+ internal const string SecWebSocketExtensions = "Sec-WebSocket-Extensions";
+ internal const string SecWebSocketAccept = "Sec-WebSocket-Accept";
+ internal const string Origin = "Origin";
+ internal const string SecWebSocketProtocol = "Sec-WebSocket-Protocol";
+ internal const string SecWebSocketVersion = "Sec-WebSocket-Version";
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/NativeRequestContext.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/NativeRequestContext.cs
new file mode 100644
index 0000000000..a0985c87ce
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/NativeRequestContext.cs
@@ -0,0 +1,455 @@
+// 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.Net.Sockets;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ internal unsafe class NativeRequestContext : IDisposable
+ {
+ private const int AlignmentPadding = 8;
+ private IntPtr _originalBufferAddress;
+ private HttpApiTypes.HTTP_REQUEST* _nativeRequest;
+ private byte[] _backingBuffer;
+ private int _bufferAlignment;
+ private SafeNativeOverlapped _nativeOverlapped;
+ private bool _permanentlyPinned;
+
+ // To be used by HttpSys
+ internal NativeRequestContext(SafeNativeOverlapped nativeOverlapped,
+ int bufferAlignment,
+ HttpApiTypes.HTTP_REQUEST* nativeRequest,
+ byte[] backingBuffer,
+ ulong requestId)
+ {
+ _nativeOverlapped = nativeOverlapped;
+ _bufferAlignment = bufferAlignment;
+ _nativeRequest = nativeRequest;
+ _backingBuffer = backingBuffer;
+ RequestId = requestId;
+ }
+
+ // To be used by IIS Integration.
+ internal NativeRequestContext(HttpApiTypes.HTTP_REQUEST* request)
+ {
+ _nativeRequest = request;
+ _bufferAlignment = 0;
+ _permanentlyPinned = true;
+ }
+
+
+ internal SafeNativeOverlapped NativeOverlapped => _nativeOverlapped;
+
+ internal HttpApiTypes.HTTP_REQUEST* NativeRequest
+ {
+ get
+ {
+ Debug.Assert(_nativeRequest != null || _backingBuffer == null, "native request accessed after ReleasePins().");
+ return _nativeRequest;
+ }
+ }
+
+ internal HttpApiTypes.HTTP_REQUEST_V2* NativeRequestV2
+ {
+ get
+ {
+ Debug.Assert(_nativeRequest != null || _backingBuffer == null, "native request accessed after ReleasePins().");
+ return (HttpApiTypes.HTTP_REQUEST_V2*)_nativeRequest;
+ }
+ }
+
+ internal ulong RequestId
+ {
+ get { return NativeRequest->RequestId; }
+ set { NativeRequest->RequestId = value; }
+ }
+
+ internal ulong ConnectionId => NativeRequest->ConnectionId;
+
+ internal HttpApiTypes.HTTP_VERB VerbId => NativeRequest->Verb;
+
+ internal ulong UrlContext => NativeRequest->UrlContext;
+
+ internal ushort UnknownHeaderCount => NativeRequest->Headers.UnknownHeaderCount;
+
+ internal SslStatus SslStatus
+ {
+ get
+ {
+ return NativeRequest->pSslInfo == null ? SslStatus.Insecure :
+ NativeRequest->pSslInfo->SslClientCertNegotiated == 0 ? SslStatus.NoClientCert :
+ SslStatus.ClientCert;
+ }
+ }
+
+ internal uint Size
+ {
+ get { return (uint)_backingBuffer.Length - AlignmentPadding; }
+ }
+
+ // ReleasePins() should be called exactly once. It must be called before Dispose() is called, which means it must be called
+ // before an object (Request) which closes the RequestContext on demand is returned to the application.
+ internal void ReleasePins()
+ {
+ Debug.Assert(_nativeRequest != null || _backingBuffer == null, "RequestContextBase::ReleasePins()|ReleasePins() called twice.");
+ _originalBufferAddress = (IntPtr)_nativeRequest;
+ _nativeRequest = null;
+ _nativeOverlapped?.Dispose();
+ _nativeOverlapped = null;
+ }
+
+ public virtual void Dispose()
+ {
+ Debug.Assert(_nativeRequest == null, "RequestContextBase::Dispose()|Dispose() called before ReleasePins().");
+ _nativeOverlapped?.Dispose();
+ }
+
+ // These methods require the HTTP_REQUEST to still be pinned in its original location.
+
+ internal string GetVerb()
+ {
+ var verb = NativeRequest->Verb;
+ if (verb > HttpApiTypes.HTTP_VERB.HttpVerbUnknown && verb < HttpApiTypes.HTTP_VERB.HttpVerbMaximum)
+ {
+ return HttpApiTypes.HttpVerbs[(int)verb];
+ }
+ else if (verb == HttpApiTypes.HTTP_VERB.HttpVerbUnknown && NativeRequest->pUnknownVerb != null)
+ {
+ return HeaderEncoding.GetString(NativeRequest->pUnknownVerb, NativeRequest->UnknownVerbLength);
+ }
+
+ return null;
+ }
+
+ internal string GetRawUrl()
+ {
+ if (NativeRequest->pRawUrl != null && NativeRequest->RawUrlLength > 0)
+ {
+ return Marshal.PtrToStringAnsi((IntPtr)NativeRequest->pRawUrl, NativeRequest->RawUrlLength);
+ }
+ return null;
+ }
+
+ internal byte[] GetRawUrlInBytes()
+ {
+ if (NativeRequest->pRawUrl != null && NativeRequest->RawUrlLength > 0)
+ {
+ var result = new byte[NativeRequest->RawUrlLength];
+ Marshal.Copy((IntPtr)NativeRequest->pRawUrl, result, 0, NativeRequest->RawUrlLength);
+
+ return result;
+ }
+
+ return null;
+ }
+
+ internal CookedUrl GetCookedUrl()
+ {
+ return new CookedUrl(NativeRequest->CookedUrl);
+ }
+
+ internal Version GetVersion()
+ {
+ var major = NativeRequest->Version.MajorVersion;
+ var minor = NativeRequest->Version.MinorVersion;
+ if (major == 1 && minor == 1)
+ {
+ return Constants.V1_1;
+ }
+ else if (major == 1 && minor == 0)
+ {
+ return Constants.V1_0;
+ }
+ return new Version(major, minor);
+ }
+
+ internal bool CheckAuthenticated()
+ {
+ var requestInfo = NativeRequestV2->pRequestInfo;
+ var infoCount = NativeRequestV2->RequestInfoCount;
+
+ for (int i = 0; i < infoCount; i++)
+ {
+ var info = &requestInfo[i];
+ if (info != null
+ && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth
+ && info->pInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ internal WindowsPrincipal GetUser()
+ {
+ var requestInfo = NativeRequestV2->pRequestInfo;
+ var infoCount = NativeRequestV2->RequestInfoCount;
+
+ for (int i = 0; i < infoCount; i++)
+ {
+ var info = &requestInfo[i];
+ if (info != null
+ && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth
+ && info->pInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess)
+ {
+ // Duplicates AccessToken
+ var identity = new WindowsIdentity(info->pInfo->AccessToken, GetAuthTypeFromRequest(info->pInfo->AuthType));
+
+ // Close the original
+ UnsafeNclNativeMethods.SafeNetHandles.CloseHandle(info->pInfo->AccessToken);
+
+ return new WindowsPrincipal(identity);
+ }
+ }
+
+ return new WindowsPrincipal(WindowsIdentity.GetAnonymous()); // Anonymous / !IsAuthenticated
+ }
+
+ private static string GetAuthTypeFromRequest(HttpApiTypes.HTTP_REQUEST_AUTH_TYPE input)
+ {
+ switch (input)
+ {
+ case HttpApiTypes.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeBasic:
+ return "Basic";
+ case HttpApiTypes.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeNTLM:
+ return "NTLM";
+ // case HttpApi.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeDigest:
+ // return "Digest";
+ case HttpApiTypes.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeNegotiate:
+ return "Negotiate";
+ case HttpApiTypes.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeKerberos:
+ return "Kerberos";
+ default:
+ throw new NotImplementedException(input.ToString());
+ }
+ }
+
+ // These methods are for accessing the request structure after it has been unpinned. They need to adjust addresses
+ // in case GC has moved the original object.
+
+ internal string GetKnownHeader(HttpSysRequestHeader header)
+ {
+ if (_permanentlyPinned)
+ {
+ return GetKnowHeaderHelper(header, 0, _nativeRequest);
+ }
+ else
+ {
+ fixed (byte* pMemoryBlob = _backingBuffer)
+ {
+ var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
+ long fixup = pMemoryBlob - (byte*)_originalBufferAddress;
+ return GetKnowHeaderHelper(header, fixup, request);
+ }
+ }
+ }
+
+ private string GetKnowHeaderHelper(HttpSysRequestHeader header, long fixup, HttpApiTypes.HTTP_REQUEST* request)
+ {
+ int headerIndex = (int)header;
+ string value = null;
+
+ HttpApiTypes.HTTP_KNOWN_HEADER* pKnownHeader = (&request->Headers.KnownHeaders) + headerIndex;
+ // For known headers, when header value is empty, RawValueLength will be 0 and
+ // pRawValue will point to empty string ("\0")
+ if (pKnownHeader->RawValueLength > 0)
+ {
+ value = HeaderEncoding.GetString(pKnownHeader->pRawValue + fixup, pKnownHeader->RawValueLength);
+ }
+
+ return value;
+ }
+
+ internal void GetUnknownHeaders(IDictionary<string, StringValues> unknownHeaders)
+ {
+ if (_permanentlyPinned)
+ {
+ GetUnknownHeadersHelper(unknownHeaders, 0, _nativeRequest);
+ }
+ else
+ {
+ // Return value.
+ fixed (byte* pMemoryBlob = _backingBuffer)
+ {
+ var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
+ long fixup = pMemoryBlob - (byte*)_originalBufferAddress;
+ GetUnknownHeadersHelper(unknownHeaders, fixup, request);
+ }
+ }
+ }
+
+ private void GetUnknownHeadersHelper(IDictionary<string, StringValues> unknownHeaders, long fixup, HttpApiTypes.HTTP_REQUEST* request)
+ {
+ int index;
+
+ // unknown headers
+ if (request->Headers.UnknownHeaderCount != 0)
+ {
+ var pUnknownHeader = (HttpApiTypes.HTTP_UNKNOWN_HEADER*)(fixup + (byte*)request->Headers.pUnknownHeaders);
+ for (index = 0; index < request->Headers.UnknownHeaderCount; index++)
+ {
+ // For unknown headers, when header value is empty, RawValueLength will be 0 and
+ // pRawValue will be null.
+ if (pUnknownHeader->pName != null && pUnknownHeader->NameLength > 0)
+ {
+ var headerName = HeaderEncoding.GetString(pUnknownHeader->pName + fixup, pUnknownHeader->NameLength);
+ string headerValue;
+ if (pUnknownHeader->pRawValue != null && pUnknownHeader->RawValueLength > 0)
+ {
+ headerValue = HeaderEncoding.GetString(pUnknownHeader->pRawValue + fixup, pUnknownHeader->RawValueLength);
+ }
+ else
+ {
+ headerValue = string.Empty;
+ }
+ // Note that Http.Sys currently collapses all headers of the same name to a single coma separated string,
+ // so we can just call Set.
+ unknownHeaders[headerName] = headerValue;
+ }
+ pUnknownHeader++;
+ }
+ }
+ }
+
+ internal SocketAddress GetRemoteEndPoint()
+ {
+ return GetEndPoint(localEndpoint: false);
+ }
+
+ internal SocketAddress GetLocalEndPoint()
+ {
+ return GetEndPoint(localEndpoint: true);
+ }
+
+ private SocketAddress GetEndPoint(bool localEndpoint)
+ {
+ if (_permanentlyPinned)
+ {
+ return GetEndPointHelper(localEndpoint, _nativeRequest, (byte *)0);
+ }
+ else
+ {
+ fixed (byte* pMemoryBlob = _backingBuffer)
+ {
+ var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
+ return GetEndPointHelper(localEndpoint, request, pMemoryBlob);
+ }
+ }
+ }
+
+ private SocketAddress GetEndPointHelper(bool localEndpoint, HttpApiTypes.HTTP_REQUEST* request, byte* pMemoryBlob)
+ {
+ var source = localEndpoint ? (byte*)request->Address.pLocalAddress : (byte*)request->Address.pRemoteAddress;
+
+ if (source == null)
+ {
+ return null;
+ }
+ var address = (IntPtr)(pMemoryBlob + _bufferAlignment - (byte*)_originalBufferAddress + source);
+ return CopyOutAddress(address);
+ }
+
+ private static SocketAddress CopyOutAddress(IntPtr address)
+ {
+ ushort addressFamily = *((ushort*)address);
+ if (addressFamily == (ushort)AddressFamily.InterNetwork)
+ {
+ var v4address = new SocketAddress(AddressFamily.InterNetwork, SocketAddress.IPv4AddressSize);
+ fixed (byte* pBuffer = v4address.Buffer)
+ {
+ for (int index = 2; index < SocketAddress.IPv4AddressSize; index++)
+ {
+ pBuffer[index] = ((byte*)address)[index];
+ }
+ }
+ return v4address;
+ }
+ if (addressFamily == (ushort)AddressFamily.InterNetworkV6)
+ {
+ var v6address = new SocketAddress(AddressFamily.InterNetworkV6, SocketAddress.IPv6AddressSize);
+ fixed (byte* pBuffer = v6address.Buffer)
+ {
+ for (int index = 2; index < SocketAddress.IPv6AddressSize; index++)
+ {
+ pBuffer[index] = ((byte*)address)[index];
+ }
+ }
+ return v6address;
+ }
+
+ return null;
+ }
+
+ internal uint GetChunks(ref int dataChunkIndex, ref uint dataChunkOffset, byte[] buffer, int offset, int size)
+ {
+ // Return value.
+ if (_permanentlyPinned)
+ {
+ return GetChunksHelper(ref dataChunkIndex, ref dataChunkOffset, buffer, offset, size, 0, _nativeRequest);
+ }
+ else
+ {
+ fixed (byte* pMemoryBlob = _backingBuffer)
+ {
+ var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
+ long fixup = pMemoryBlob - (byte*)_originalBufferAddress;
+ return GetChunksHelper(ref dataChunkIndex, ref dataChunkOffset, buffer, offset, size, fixup, request);
+ }
+ }
+ }
+
+ private uint GetChunksHelper(ref int dataChunkIndex, ref uint dataChunkOffset, byte[] buffer, int offset, int size, long fixup, HttpApiTypes.HTTP_REQUEST* request)
+ {
+ uint dataRead = 0;
+
+ if (request->EntityChunkCount > 0 && dataChunkIndex < request->EntityChunkCount && dataChunkIndex != -1)
+ {
+ var pDataChunk = (HttpApiTypes.HTTP_DATA_CHUNK*)(fixup + (byte*)&request->pEntityChunks[dataChunkIndex]);
+
+ fixed (byte* pReadBuffer = buffer)
+ {
+ byte* pTo = &pReadBuffer[offset];
+
+ while (dataChunkIndex < request->EntityChunkCount && dataRead < size)
+ {
+ if (dataChunkOffset >= pDataChunk->fromMemory.BufferLength)
+ {
+ dataChunkOffset = 0;
+ dataChunkIndex++;
+ pDataChunk++;
+ }
+ else
+ {
+ byte* pFrom = (byte*)pDataChunk->fromMemory.pBuffer + dataChunkOffset + fixup;
+
+ uint bytesToRead = pDataChunk->fromMemory.BufferLength - (uint)dataChunkOffset;
+ if (bytesToRead > (uint)size)
+ {
+ bytesToRead = (uint)size;
+ }
+ for (uint i = 0; i < bytesToRead; i++)
+ {
+ *(pTo++) = *(pFrom++);
+ }
+ dataRead += bytesToRead;
+ dataChunkOffset += bytesToRead;
+ }
+ }
+ }
+ }
+ // we're finished.
+ if (dataChunkIndex == request->EntityChunkCount)
+ {
+ dataChunkIndex = -1;
+ }
+ return dataRead;
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RawUrlHelper.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RawUrlHelper.cs
new file mode 100644
index 0000000000..ee0bf4a996
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RawUrlHelper.cs
@@ -0,0 +1,151 @@
+// 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.Text;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ internal static class RawUrlHelper
+ {
+ private static readonly byte[] _forwardSlashPath = Encoding.ASCII.GetBytes("/");
+
+ /// <summary>
+ /// Find the segment of the URI byte array which represents the path.
+ /// </summary>
+ public static ArraySegment<byte> GetPath(byte[] raw)
+ {
+ // performance
+ var pathStartIndex = 0;
+
+ // Performance improvement: accept two cases upfront
+ //
+ // 1) Since nearly all strings are relative Uris, just look if the string starts with '/'.
+ // If so, we have a relative Uri and the path starts at position 0.
+ // (http.sys already trimmed leading whitespaces)
+ //
+ // 2) The URL is simply '*'
+ if (raw[0] != '/' && !(raw.Length == 1 && raw[0] == '*'))
+ {
+ // We can't check against cookedUriScheme, since http.sys allows for request http://myserver/ to
+ // use a request line 'GET https://myserver/' (note http vs. https). Therefore check if the
+ // Uri starts with either http:// or https://.
+ var authorityStartIndex = FindHttpOrHttps(raw);
+ if (authorityStartIndex > 0)
+ {
+ // we have an absolute Uri. Find out where the authority ends and the path begins.
+ // Note that Uris like "http://server?query=value/1/2" are invalid according to RFC2616
+ // and http.sys behavior: If the Uri contains a query, there must be at least one '/'
+ // between the authority and the '?' character: It's safe to just look for the first
+ // '/' after the authority to determine the beginning of the path.
+ pathStartIndex = Find(raw, authorityStartIndex, '/');
+ if (pathStartIndex == -1)
+ {
+ // e.g. for request lines like: 'GET http://myserver' (no final '/')
+ // At this point we can return a path with a slash.
+ return new ArraySegment<byte>(_forwardSlashPath);
+ }
+ }
+ else
+ {
+ // RFC2616: Request-URI = "*" | absoluteURI | abs_path | authority
+ // 'authority' can only be used with CONNECT which is never received by HttpListener.
+ // I.e. if we don't have an absolute path (must start with '/') and we don't have
+ // an absolute Uri (must start with http:// or https://), then 'uriString' must be '*'.
+ throw new InvalidOperationException("Invalid URI format");
+ }
+ }
+
+ // Find end of path: The path is terminated by
+ // - the first '?' character
+ // - the first '#' character: This is never the case here, since http.sys won't accept
+ // Uris containing fragments. Also, RFC2616 doesn't allow fragments in request Uris.
+ // - end of Uri string
+ var scan = pathStartIndex + 1;
+ while (scan < raw.Length && raw[scan] != '?')
+ {
+ scan++;
+ }
+
+ return new ArraySegment<byte>(raw, pathStartIndex, scan - pathStartIndex);
+ }
+
+ /// <summary>
+ /// Compare the beginning portion of the raw URL byte array to https:// and http://
+ /// </summary>
+ /// <param name="raw">The byte array represents the raw URI</param>
+ /// <returns>Length of the matched bytes, 0 if it is not matched.</returns>
+ private static int FindHttpOrHttps(byte[] raw)
+ {
+ if (raw.Length < 7)
+ {
+ return 0;
+ }
+
+ if (raw[0] != 'h' && raw[0] != 'H')
+ {
+ return 0;
+ }
+
+ if (raw[1] != 't' && raw[1] != 'T')
+ {
+ return 0;
+ }
+
+ if (raw[2] != 't' && raw[2] != 'T')
+ {
+ return 0;
+ }
+
+ if (raw[3] != 'p' && raw[3] != 'P')
+ {
+ return 0;
+ }
+
+ if (raw[4] == ':')
+ {
+ if (raw[5] != '/' || raw[6] != '/')
+ {
+ return 0;
+ }
+ else
+ {
+ return 7;
+ }
+ }
+ else if (raw[4] == 's' || raw[4] == 'S')
+ {
+ if (raw.Length < 8)
+ {
+ return 0;
+ }
+
+ if (raw[5] != ':' || raw[6] != '/' || raw[7] != '/')
+ {
+ return 0;
+ }
+ else
+ {
+ return 8;
+ }
+ }
+ else
+ {
+ return 0;
+ }
+ }
+
+ private static int Find(byte[] raw, int begin, char target)
+ {
+ for (var idx = begin; idx < raw.Length; ++idx)
+ {
+ if (raw[idx] == target)
+ {
+ return idx;
+ }
+ }
+
+ return -1;
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RequestHeaders.Generated.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RequestHeaders.Generated.cs
new file mode 100644
index 0000000000..47759a41c5
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RequestHeaders.Generated.cs
@@ -0,0 +1,2542 @@
+// 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.
+// <auto-generated />
+
+using System;
+using System.CodeDom.Compiler;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ [GeneratedCode("TextTemplatingFileGenerator", "")]
+ internal partial class RequestHeaders
+ {
+ // Tracks if individual fields have been read from native or set directly.
+ // Once read or set, their presence in the collection is marked by if their StringValues is null or not.
+ private UInt32 _flag0, _flag1;
+
+ private StringValues _Accept;
+ private StringValues _AcceptCharset;
+ private StringValues _AcceptEncoding;
+ private StringValues _AcceptLanguage;
+ private StringValues _Allow;
+ private StringValues _Authorization;
+ private StringValues _CacheControl;
+ private StringValues _Connection;
+ private StringValues _ContentEncoding;
+ private StringValues _ContentLanguage;
+ private StringValues _ContentLength;
+ private StringValues _ContentLocation;
+ private StringValues _ContentMd5;
+ private StringValues _ContentRange;
+ private StringValues _ContentType;
+ private StringValues _Cookie;
+ private StringValues _Date;
+ private StringValues _Expect;
+ private StringValues _Expires;
+ private StringValues _From;
+ private StringValues _Host;
+ private StringValues _IfMatch;
+ private StringValues _IfModifiedSince;
+ private StringValues _IfNoneMatch;
+ private StringValues _IfRange;
+ private StringValues _IfUnmodifiedSince;
+ private StringValues _KeepAlive;
+ private StringValues _LastModified;
+ private StringValues _MaxForwards;
+ private StringValues _Pragma;
+ private StringValues _ProxyAuthorization;
+ private StringValues _Range;
+ private StringValues _Referer;
+ private StringValues _Te;
+ private StringValues _Trailer;
+ private StringValues _TransferEncoding;
+ private StringValues _Translate;
+ private StringValues _Upgrade;
+ private StringValues _UserAgent;
+ private StringValues _Via;
+ private StringValues _Warning;
+
+ internal StringValues Accept
+ {
+ get
+ {
+ if (!((_flag0 & 0x1u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Accept);
+ if (nativeValue != null)
+ {
+ _Accept = nativeValue;
+ }
+ _flag0 |= 0x1u;
+ }
+ return _Accept;
+ }
+ set
+ {
+ _flag0 |= 0x1u;
+ _Accept = value;
+ }
+ }
+
+ internal StringValues AcceptCharset
+ {
+ get
+ {
+ if (!((_flag0 & 0x2u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.AcceptCharset);
+ if (nativeValue != null)
+ {
+ _AcceptCharset = nativeValue;
+ }
+ _flag0 |= 0x2u;
+ }
+ return _AcceptCharset;
+ }
+ set
+ {
+ _flag0 |= 0x2u;
+ _AcceptCharset = value;
+ }
+ }
+
+ internal StringValues AcceptEncoding
+ {
+ get
+ {
+ if (!((_flag0 & 0x4u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.AcceptEncoding);
+ if (nativeValue != null)
+ {
+ _AcceptEncoding = nativeValue;
+ }
+ _flag0 |= 0x4u;
+ }
+ return _AcceptEncoding;
+ }
+ set
+ {
+ _flag0 |= 0x4u;
+ _AcceptEncoding = value;
+ }
+ }
+
+ internal StringValues AcceptLanguage
+ {
+ get
+ {
+ if (!((_flag0 & 0x8u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.AcceptLanguage);
+ if (nativeValue != null)
+ {
+ _AcceptLanguage = nativeValue;
+ }
+ _flag0 |= 0x8u;
+ }
+ return _AcceptLanguage;
+ }
+ set
+ {
+ _flag0 |= 0x8u;
+ _AcceptLanguage = value;
+ }
+ }
+
+ internal StringValues Allow
+ {
+ get
+ {
+ if (!((_flag0 & 0x10u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Allow);
+ if (nativeValue != null)
+ {
+ _Allow = nativeValue;
+ }
+ _flag0 |= 0x10u;
+ }
+ return _Allow;
+ }
+ set
+ {
+ _flag0 |= 0x10u;
+ _Allow = value;
+ }
+ }
+
+ internal StringValues Authorization
+ {
+ get
+ {
+ if (!((_flag0 & 0x20u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Authorization);
+ if (nativeValue != null)
+ {
+ _Authorization = nativeValue;
+ }
+ _flag0 |= 0x20u;
+ }
+ return _Authorization;
+ }
+ set
+ {
+ _flag0 |= 0x20u;
+ _Authorization = value;
+ }
+ }
+
+ internal StringValues CacheControl
+ {
+ get
+ {
+ if (!((_flag0 & 0x40u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.CacheControl);
+ if (nativeValue != null)
+ {
+ _CacheControl = nativeValue;
+ }
+ _flag0 |= 0x40u;
+ }
+ return _CacheControl;
+ }
+ set
+ {
+ _flag0 |= 0x40u;
+ _CacheControl = value;
+ }
+ }
+
+ internal StringValues Connection
+ {
+ get
+ {
+ if (!((_flag0 & 0x80u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Connection);
+ if (nativeValue != null)
+ {
+ _Connection = nativeValue;
+ }
+ _flag0 |= 0x80u;
+ }
+ return _Connection;
+ }
+ set
+ {
+ _flag0 |= 0x80u;
+ _Connection = value;
+ }
+ }
+
+ internal StringValues ContentEncoding
+ {
+ get
+ {
+ if (!((_flag0 & 0x100u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.ContentEncoding);
+ if (nativeValue != null)
+ {
+ _ContentEncoding = nativeValue;
+ }
+ _flag0 |= 0x100u;
+ }
+ return _ContentEncoding;
+ }
+ set
+ {
+ _flag0 |= 0x100u;
+ _ContentEncoding = value;
+ }
+ }
+
+ internal StringValues ContentLanguage
+ {
+ get
+ {
+ if (!((_flag0 & 0x200u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.ContentLanguage);
+ if (nativeValue != null)
+ {
+ _ContentLanguage = nativeValue;
+ }
+ _flag0 |= 0x200u;
+ }
+ return _ContentLanguage;
+ }
+ set
+ {
+ _flag0 |= 0x200u;
+ _ContentLanguage = value;
+ }
+ }
+
+ internal StringValues ContentLength
+ {
+ get
+ {
+ if (!((_flag0 & 0x400u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.ContentLength);
+ if (nativeValue != null)
+ {
+ _ContentLength = nativeValue;
+ }
+ _flag0 |= 0x400u;
+ }
+ return _ContentLength;
+ }
+ set
+ {
+ _flag0 |= 0x400u;
+ _ContentLength = value;
+ }
+ }
+
+ internal StringValues ContentLocation
+ {
+ get
+ {
+ if (!((_flag0 & 0x800u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.ContentLocation);
+ if (nativeValue != null)
+ {
+ _ContentLocation = nativeValue;
+ }
+ _flag0 |= 0x800u;
+ }
+ return _ContentLocation;
+ }
+ set
+ {
+ _flag0 |= 0x800u;
+ _ContentLocation = value;
+ }
+ }
+
+ internal StringValues ContentMd5
+ {
+ get
+ {
+ if (!((_flag0 & 0x1000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.ContentMd5);
+ if (nativeValue != null)
+ {
+ _ContentMd5 = nativeValue;
+ }
+ _flag0 |= 0x1000u;
+ }
+ return _ContentMd5;
+ }
+ set
+ {
+ _flag0 |= 0x1000u;
+ _ContentMd5 = value;
+ }
+ }
+
+ internal StringValues ContentRange
+ {
+ get
+ {
+ if (!((_flag0 & 0x2000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.ContentRange);
+ if (nativeValue != null)
+ {
+ _ContentRange = nativeValue;
+ }
+ _flag0 |= 0x2000u;
+ }
+ return _ContentRange;
+ }
+ set
+ {
+ _flag0 |= 0x2000u;
+ _ContentRange = value;
+ }
+ }
+
+ internal StringValues ContentType
+ {
+ get
+ {
+ if (!((_flag0 & 0x4000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.ContentType);
+ if (nativeValue != null)
+ {
+ _ContentType = nativeValue;
+ }
+ _flag0 |= 0x4000u;
+ }
+ return _ContentType;
+ }
+ set
+ {
+ _flag0 |= 0x4000u;
+ _ContentType = value;
+ }
+ }
+
+ internal StringValues Cookie
+ {
+ get
+ {
+ if (!((_flag0 & 0x8000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Cookie);
+ if (nativeValue != null)
+ {
+ _Cookie = nativeValue;
+ }
+ _flag0 |= 0x8000u;
+ }
+ return _Cookie;
+ }
+ set
+ {
+ _flag0 |= 0x8000u;
+ _Cookie = value;
+ }
+ }
+
+ internal StringValues Date
+ {
+ get
+ {
+ if (!((_flag0 & 0x10000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Date);
+ if (nativeValue != null)
+ {
+ _Date = nativeValue;
+ }
+ _flag0 |= 0x10000u;
+ }
+ return _Date;
+ }
+ set
+ {
+ _flag0 |= 0x10000u;
+ _Date = value;
+ }
+ }
+
+ internal StringValues Expect
+ {
+ get
+ {
+ if (!((_flag0 & 0x20000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Expect);
+ if (nativeValue != null)
+ {
+ _Expect = nativeValue;
+ }
+ _flag0 |= 0x20000u;
+ }
+ return _Expect;
+ }
+ set
+ {
+ _flag0 |= 0x20000u;
+ _Expect = value;
+ }
+ }
+
+ internal StringValues Expires
+ {
+ get
+ {
+ if (!((_flag0 & 0x40000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Expires);
+ if (nativeValue != null)
+ {
+ _Expires = nativeValue;
+ }
+ _flag0 |= 0x40000u;
+ }
+ return _Expires;
+ }
+ set
+ {
+ _flag0 |= 0x40000u;
+ _Expires = value;
+ }
+ }
+
+ internal StringValues From
+ {
+ get
+ {
+ if (!((_flag0 & 0x80000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.From);
+ if (nativeValue != null)
+ {
+ _From = nativeValue;
+ }
+ _flag0 |= 0x80000u;
+ }
+ return _From;
+ }
+ set
+ {
+ _flag0 |= 0x80000u;
+ _From = value;
+ }
+ }
+
+ internal StringValues Host
+ {
+ get
+ {
+ if (!((_flag0 & 0x100000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Host);
+ if (nativeValue != null)
+ {
+ _Host = nativeValue;
+ }
+ _flag0 |= 0x100000u;
+ }
+ return _Host;
+ }
+ set
+ {
+ _flag0 |= 0x100000u;
+ _Host = value;
+ }
+ }
+
+ internal StringValues IfMatch
+ {
+ get
+ {
+ if (!((_flag0 & 0x200000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.IfMatch);
+ if (nativeValue != null)
+ {
+ _IfMatch = nativeValue;
+ }
+ _flag0 |= 0x200000u;
+ }
+ return _IfMatch;
+ }
+ set
+ {
+ _flag0 |= 0x200000u;
+ _IfMatch = value;
+ }
+ }
+
+ internal StringValues IfModifiedSince
+ {
+ get
+ {
+ if (!((_flag0 & 0x400000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.IfModifiedSince);
+ if (nativeValue != null)
+ {
+ _IfModifiedSince = nativeValue;
+ }
+ _flag0 |= 0x400000u;
+ }
+ return _IfModifiedSince;
+ }
+ set
+ {
+ _flag0 |= 0x400000u;
+ _IfModifiedSince = value;
+ }
+ }
+
+ internal StringValues IfNoneMatch
+ {
+ get
+ {
+ if (!((_flag0 & 0x800000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.IfNoneMatch);
+ if (nativeValue != null)
+ {
+ _IfNoneMatch = nativeValue;
+ }
+ _flag0 |= 0x800000u;
+ }
+ return _IfNoneMatch;
+ }
+ set
+ {
+ _flag0 |= 0x800000u;
+ _IfNoneMatch = value;
+ }
+ }
+
+ internal StringValues IfRange
+ {
+ get
+ {
+ if (!((_flag0 & 0x1000000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.IfRange);
+ if (nativeValue != null)
+ {
+ _IfRange = nativeValue;
+ }
+ _flag0 |= 0x1000000u;
+ }
+ return _IfRange;
+ }
+ set
+ {
+ _flag0 |= 0x1000000u;
+ _IfRange = value;
+ }
+ }
+
+ internal StringValues IfUnmodifiedSince
+ {
+ get
+ {
+ if (!((_flag0 & 0x2000000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.IfUnmodifiedSince);
+ if (nativeValue != null)
+ {
+ _IfUnmodifiedSince = nativeValue;
+ }
+ _flag0 |= 0x2000000u;
+ }
+ return _IfUnmodifiedSince;
+ }
+ set
+ {
+ _flag0 |= 0x2000000u;
+ _IfUnmodifiedSince = value;
+ }
+ }
+
+ internal StringValues KeepAlive
+ {
+ get
+ {
+ if (!((_flag0 & 0x4000000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.KeepAlive);
+ if (nativeValue != null)
+ {
+ _KeepAlive = nativeValue;
+ }
+ _flag0 |= 0x4000000u;
+ }
+ return _KeepAlive;
+ }
+ set
+ {
+ _flag0 |= 0x4000000u;
+ _KeepAlive = value;
+ }
+ }
+
+ internal StringValues LastModified
+ {
+ get
+ {
+ if (!((_flag0 & 0x8000000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.LastModified);
+ if (nativeValue != null)
+ {
+ _LastModified = nativeValue;
+ }
+ _flag0 |= 0x8000000u;
+ }
+ return _LastModified;
+ }
+ set
+ {
+ _flag0 |= 0x8000000u;
+ _LastModified = value;
+ }
+ }
+
+ internal StringValues MaxForwards
+ {
+ get
+ {
+ if (!((_flag0 & 0x10000000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.MaxForwards);
+ if (nativeValue != null)
+ {
+ _MaxForwards = nativeValue;
+ }
+ _flag0 |= 0x10000000u;
+ }
+ return _MaxForwards;
+ }
+ set
+ {
+ _flag0 |= 0x10000000u;
+ _MaxForwards = value;
+ }
+ }
+
+ internal StringValues Pragma
+ {
+ get
+ {
+ if (!((_flag0 & 0x20000000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Pragma);
+ if (nativeValue != null)
+ {
+ _Pragma = nativeValue;
+ }
+ _flag0 |= 0x20000000u;
+ }
+ return _Pragma;
+ }
+ set
+ {
+ _flag0 |= 0x20000000u;
+ _Pragma = value;
+ }
+ }
+
+ internal StringValues ProxyAuthorization
+ {
+ get
+ {
+ if (!((_flag0 & 0x40000000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.ProxyAuthorization);
+ if (nativeValue != null)
+ {
+ _ProxyAuthorization = nativeValue;
+ }
+ _flag0 |= 0x40000000u;
+ }
+ return _ProxyAuthorization;
+ }
+ set
+ {
+ _flag0 |= 0x40000000u;
+ _ProxyAuthorization = value;
+ }
+ }
+
+ internal StringValues Range
+ {
+ get
+ {
+ if (!((_flag0 & 0x80000000u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Range);
+ if (nativeValue != null)
+ {
+ _Range = nativeValue;
+ }
+ _flag0 |= 0x80000000u;
+ }
+ return _Range;
+ }
+ set
+ {
+ _flag0 |= 0x80000000u;
+ _Range = value;
+ }
+ }
+
+ internal StringValues Referer
+ {
+ get
+ {
+ if (!((_flag1 & 0x1u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Referer);
+ if (nativeValue != null)
+ {
+ _Referer = nativeValue;
+ }
+ _flag1 |= 0x1u;
+ }
+ return _Referer;
+ }
+ set
+ {
+ _flag1 |= 0x1u;
+ _Referer = value;
+ }
+ }
+
+ internal StringValues Te
+ {
+ get
+ {
+ if (!((_flag1 & 0x2u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Te);
+ if (nativeValue != null)
+ {
+ _Te = nativeValue;
+ }
+ _flag1 |= 0x2u;
+ }
+ return _Te;
+ }
+ set
+ {
+ _flag1 |= 0x2u;
+ _Te = value;
+ }
+ }
+
+ internal StringValues Trailer
+ {
+ get
+ {
+ if (!((_flag1 & 0x4u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Trailer);
+ if (nativeValue != null)
+ {
+ _Trailer = nativeValue;
+ }
+ _flag1 |= 0x4u;
+ }
+ return _Trailer;
+ }
+ set
+ {
+ _flag1 |= 0x4u;
+ _Trailer = value;
+ }
+ }
+
+ internal StringValues TransferEncoding
+ {
+ get
+ {
+ if (!((_flag1 & 0x8u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.TransferEncoding);
+ if (nativeValue != null)
+ {
+ _TransferEncoding = nativeValue;
+ }
+ _flag1 |= 0x8u;
+ }
+ return _TransferEncoding;
+ }
+ set
+ {
+ _flag1 |= 0x8u;
+ _TransferEncoding = value;
+ }
+ }
+
+ internal StringValues Translate
+ {
+ get
+ {
+ if (!((_flag1 & 0x10u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Translate);
+ if (nativeValue != null)
+ {
+ _Translate = nativeValue;
+ }
+ _flag1 |= 0x10u;
+ }
+ return _Translate;
+ }
+ set
+ {
+ _flag1 |= 0x10u;
+ _Translate = value;
+ }
+ }
+
+ internal StringValues Upgrade
+ {
+ get
+ {
+ if (!((_flag1 & 0x20u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Upgrade);
+ if (nativeValue != null)
+ {
+ _Upgrade = nativeValue;
+ }
+ _flag1 |= 0x20u;
+ }
+ return _Upgrade;
+ }
+ set
+ {
+ _flag1 |= 0x20u;
+ _Upgrade = value;
+ }
+ }
+
+ internal StringValues UserAgent
+ {
+ get
+ {
+ if (!((_flag1 & 0x40u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.UserAgent);
+ if (nativeValue != null)
+ {
+ _UserAgent = nativeValue;
+ }
+ _flag1 |= 0x40u;
+ }
+ return _UserAgent;
+ }
+ set
+ {
+ _flag1 |= 0x40u;
+ _UserAgent = value;
+ }
+ }
+
+ internal StringValues Via
+ {
+ get
+ {
+ if (!((_flag1 & 0x80u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Via);
+ if (nativeValue != null)
+ {
+ _Via = nativeValue;
+ }
+ _flag1 |= 0x80u;
+ }
+ return _Via;
+ }
+ set
+ {
+ _flag1 |= 0x80u;
+ _Via = value;
+ }
+ }
+
+ internal StringValues Warning
+ {
+ get
+ {
+ if (!((_flag1 & 0x100u) != 0))
+ {
+ string nativeValue = GetKnownHeader(HttpSysRequestHeader.Warning);
+ if (nativeValue != null)
+ {
+ _Warning = nativeValue;
+ }
+ _flag1 |= 0x100u;
+ }
+ return _Warning;
+ }
+ set
+ {
+ _flag1 |= 0x100u;
+ _Warning = value;
+ }
+ }
+
+ private bool PropertiesContainsKey(string key)
+ {
+ switch (key.Length)
+ {
+ case 2:
+ if (string.Equals(key, "Te", StringComparison.OrdinalIgnoreCase))
+ {
+ return Te.Count > 0;
+ }
+ break;
+ case 3:
+ if (string.Equals(key, "Via", StringComparison.OrdinalIgnoreCase))
+ {
+ return Via.Count > 0;
+ }
+ break;
+ case 4:
+ if (string.Equals(key, "Date", StringComparison.OrdinalIgnoreCase))
+ {
+ return Date.Count > 0;
+ }
+ if (string.Equals(key, "From", StringComparison.OrdinalIgnoreCase))
+ {
+ return From.Count > 0;
+ }
+ if (string.Equals(key, "Host", StringComparison.OrdinalIgnoreCase))
+ {
+ return Host.Count > 0;
+ }
+ break;
+ case 5:
+ if (string.Equals(key, "Allow", StringComparison.OrdinalIgnoreCase))
+ {
+ return Allow.Count > 0;
+ }
+ if (string.Equals(key, "Range", StringComparison.OrdinalIgnoreCase))
+ {
+ return Range.Count > 0;
+ }
+ break;
+ case 6:
+ if (string.Equals(key, "Accept", StringComparison.OrdinalIgnoreCase))
+ {
+ return Accept.Count > 0;
+ }
+ if (string.Equals(key, "Cookie", StringComparison.OrdinalIgnoreCase))
+ {
+ return Cookie.Count > 0;
+ }
+ if (string.Equals(key, "Expect", StringComparison.OrdinalIgnoreCase))
+ {
+ return Expect.Count > 0;
+ }
+ if (string.Equals(key, "Pragma", StringComparison.OrdinalIgnoreCase))
+ {
+ return Pragma.Count > 0;
+ }
+ break;
+ case 7:
+ if (string.Equals(key, "Expires", StringComparison.OrdinalIgnoreCase))
+ {
+ return Expires.Count > 0;
+ }
+ if (string.Equals(key, "Referer", StringComparison.OrdinalIgnoreCase))
+ {
+ return Referer.Count > 0;
+ }
+ if (string.Equals(key, "Trailer", StringComparison.OrdinalIgnoreCase))
+ {
+ return Trailer.Count > 0;
+ }
+ if (string.Equals(key, "Upgrade", StringComparison.OrdinalIgnoreCase))
+ {
+ return Upgrade.Count > 0;
+ }
+ if (string.Equals(key, "Warning", StringComparison.OrdinalIgnoreCase))
+ {
+ return Warning.Count > 0;
+ }
+ break;
+ case 8:
+ if (string.Equals(key, "If-Match", StringComparison.OrdinalIgnoreCase))
+ {
+ return IfMatch.Count > 0;
+ }
+ if (string.Equals(key, "If-Range", StringComparison.OrdinalIgnoreCase))
+ {
+ return IfRange.Count > 0;
+ }
+ break;
+ case 9:
+ if (string.Equals(key, "Translate", StringComparison.OrdinalIgnoreCase))
+ {
+ return Translate.Count > 0;
+ }
+ break;
+ case 10:
+ if (string.Equals(key, "Connection", StringComparison.OrdinalIgnoreCase))
+ {
+ return Connection.Count > 0;
+ }
+ if (string.Equals(key, "Keep-Alive", StringComparison.OrdinalIgnoreCase))
+ {
+ return KeepAlive.Count > 0;
+ }
+ if (string.Equals(key, "User-Agent", StringComparison.OrdinalIgnoreCase))
+ {
+ return UserAgent.Count > 0;
+ }
+ break;
+ case 11:
+ if (string.Equals(key, "Content-Md5", StringComparison.OrdinalIgnoreCase))
+ {
+ return ContentMd5.Count > 0;
+ }
+ break;
+ case 12:
+ if (string.Equals(key, "Content-Type", StringComparison.OrdinalIgnoreCase))
+ {
+ return ContentType.Count > 0;
+ }
+ if (string.Equals(key, "Max-Forwards", StringComparison.OrdinalIgnoreCase))
+ {
+ return MaxForwards.Count > 0;
+ }
+ break;
+ case 13:
+ if (string.Equals(key, "Authorization", StringComparison.OrdinalIgnoreCase))
+ {
+ return Authorization.Count > 0;
+ }
+ if (string.Equals(key, "Cache-Control", StringComparison.OrdinalIgnoreCase))
+ {
+ return CacheControl.Count > 0;
+ }
+ if (string.Equals(key, "Content-Range", StringComparison.OrdinalIgnoreCase))
+ {
+ return ContentRange.Count > 0;
+ }
+ if (string.Equals(key, "If-None-Match", StringComparison.OrdinalIgnoreCase))
+ {
+ return IfNoneMatch.Count > 0;
+ }
+ if (string.Equals(key, "Last-Modified", StringComparison.OrdinalIgnoreCase))
+ {
+ return LastModified.Count > 0;
+ }
+ break;
+ case 14:
+ if (string.Equals(key, "Accept-Charset", StringComparison.OrdinalIgnoreCase))
+ {
+ return AcceptCharset.Count > 0;
+ }
+ if (string.Equals(key, "Content-Length", StringComparison.OrdinalIgnoreCase))
+ {
+ return ContentLength.Count > 0;
+ }
+ break;
+ case 15:
+ if (string.Equals(key, "Accept-Encoding", StringComparison.OrdinalIgnoreCase))
+ {
+ return AcceptEncoding.Count > 0;
+ }
+ if (string.Equals(key, "Accept-Language", StringComparison.OrdinalIgnoreCase))
+ {
+ return AcceptLanguage.Count > 0;
+ }
+ break;
+ case 16:
+ if (string.Equals(key, "Content-Encoding", StringComparison.OrdinalIgnoreCase))
+ {
+ return ContentEncoding.Count > 0;
+ }
+ if (string.Equals(key, "Content-Language", StringComparison.OrdinalIgnoreCase))
+ {
+ return ContentLanguage.Count > 0;
+ }
+ if (string.Equals(key, "Content-Location", StringComparison.OrdinalIgnoreCase))
+ {
+ return ContentLocation.Count > 0;
+ }
+ break;
+ case 17:
+ if (string.Equals(key, "If-Modified-Since", StringComparison.OrdinalIgnoreCase))
+ {
+ return IfModifiedSince.Count > 0;
+ }
+ if (string.Equals(key, "Transfer-Encoding", StringComparison.OrdinalIgnoreCase))
+ {
+ return TransferEncoding.Count > 0;
+ }
+ break;
+ case 19:
+ if (string.Equals(key, "If-Unmodified-Since", StringComparison.OrdinalIgnoreCase))
+ {
+ return IfUnmodifiedSince.Count > 0;
+ }
+ if (string.Equals(key, "Proxy-Authorization", StringComparison.OrdinalIgnoreCase))
+ {
+ return ProxyAuthorization.Count > 0;
+ }
+ break;
+ }
+ return false;
+ }
+
+ private bool PropertiesTryGetValue(string key, out StringValues value)
+ {
+ switch (key.Length)
+ {
+ case 2:
+ if (string.Equals(key, "Te", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Te;
+ return value.Count > 0;
+ }
+ break;
+ case 3:
+ if (string.Equals(key, "Via", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Via;
+ return value.Count > 0;
+ }
+ break;
+ case 4:
+ if (string.Equals(key, "Date", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Date;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "From", StringComparison.OrdinalIgnoreCase))
+ {
+ value = From;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Host", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Host;
+ return value.Count > 0;
+ }
+ break;
+ case 5:
+ if (string.Equals(key, "Allow", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Allow;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Range", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Range;
+ return value.Count > 0;
+ }
+ break;
+ case 6:
+ if (string.Equals(key, "Accept", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Accept;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Cookie", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Cookie;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Expect", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Expect;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Pragma", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Pragma;
+ return value.Count > 0;
+ }
+ break;
+ case 7:
+ if (string.Equals(key, "Expires", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Expires;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Referer", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Referer;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Trailer", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Trailer;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Upgrade", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Upgrade;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Warning", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Warning;
+ return value.Count > 0;
+ }
+ break;
+ case 8:
+ if (string.Equals(key, "If-Match", StringComparison.OrdinalIgnoreCase))
+ {
+ value = IfMatch;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "If-Range", StringComparison.OrdinalIgnoreCase))
+ {
+ value = IfRange;
+ return value.Count > 0;
+ }
+ break;
+ case 9:
+ if (string.Equals(key, "Translate", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Translate;
+ return value.Count > 0;
+ }
+ break;
+ case 10:
+ if (string.Equals(key, "Connection", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Connection;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Keep-Alive", StringComparison.OrdinalIgnoreCase))
+ {
+ value = KeepAlive;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "User-Agent", StringComparison.OrdinalIgnoreCase))
+ {
+ value = UserAgent;
+ return value.Count > 0;
+ }
+ break;
+ case 11:
+ if (string.Equals(key, "Content-Md5", StringComparison.OrdinalIgnoreCase))
+ {
+ value = ContentMd5;
+ return value.Count > 0;
+ }
+ break;
+ case 12:
+ if (string.Equals(key, "Content-Type", StringComparison.OrdinalIgnoreCase))
+ {
+ value = ContentType;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Max-Forwards", StringComparison.OrdinalIgnoreCase))
+ {
+ value = MaxForwards;
+ return value.Count > 0;
+ }
+ break;
+ case 13:
+ if (string.Equals(key, "Authorization", StringComparison.OrdinalIgnoreCase))
+ {
+ value = Authorization;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Cache-Control", StringComparison.OrdinalIgnoreCase))
+ {
+ value = CacheControl;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Content-Range", StringComparison.OrdinalIgnoreCase))
+ {
+ value = ContentRange;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "If-None-Match", StringComparison.OrdinalIgnoreCase))
+ {
+ value = IfNoneMatch;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Last-Modified", StringComparison.OrdinalIgnoreCase))
+ {
+ value = LastModified;
+ return value.Count > 0;
+ }
+ break;
+ case 14:
+ if (string.Equals(key, "Accept-Charset", StringComparison.OrdinalIgnoreCase))
+ {
+ value = AcceptCharset;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Content-Length", StringComparison.OrdinalIgnoreCase))
+ {
+ value = ContentLength;
+ return value.Count > 0;
+ }
+ break;
+ case 15:
+ if (string.Equals(key, "Accept-Encoding", StringComparison.OrdinalIgnoreCase))
+ {
+ value = AcceptEncoding;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Accept-Language", StringComparison.OrdinalIgnoreCase))
+ {
+ value = AcceptLanguage;
+ return value.Count > 0;
+ }
+ break;
+ case 16:
+ if (string.Equals(key, "Content-Encoding", StringComparison.OrdinalIgnoreCase))
+ {
+ value = ContentEncoding;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Content-Language", StringComparison.OrdinalIgnoreCase))
+ {
+ value = ContentLanguage;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Content-Location", StringComparison.OrdinalIgnoreCase))
+ {
+ value = ContentLocation;
+ return value.Count > 0;
+ }
+ break;
+ case 17:
+ if (string.Equals(key, "If-Modified-Since", StringComparison.OrdinalIgnoreCase))
+ {
+ value = IfModifiedSince;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Transfer-Encoding", StringComparison.OrdinalIgnoreCase))
+ {
+ value = TransferEncoding;
+ return value.Count > 0;
+ }
+ break;
+ case 19:
+ if (string.Equals(key, "If-Unmodified-Since", StringComparison.OrdinalIgnoreCase))
+ {
+ value = IfUnmodifiedSince;
+ return value.Count > 0;
+ }
+ if (string.Equals(key, "Proxy-Authorization", StringComparison.OrdinalIgnoreCase))
+ {
+ value = ProxyAuthorization;
+ return value.Count > 0;
+ }
+ break;
+ }
+ value = StringValues.Empty;
+ return false;
+ }
+
+ private bool PropertiesTrySetValue(string key, StringValues value)
+ {
+ switch (key.Length)
+ {
+ case 2:
+ if (string.Equals(key, "Te", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag1 |= 0x2u;
+ Te = value;
+ return true;
+ }
+ break;
+ case 3:
+ if (string.Equals(key, "Via", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag1 |= 0x80u;
+ Via = value;
+ return true;
+ }
+ break;
+ case 4:
+ if (string.Equals(key, "Date", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x10000u;
+ Date = value;
+ return true;
+ }
+ if (string.Equals(key, "From", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x80000u;
+ From = value;
+ return true;
+ }
+ if (string.Equals(key, "Host", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x100000u;
+ Host = value;
+ return true;
+ }
+ break;
+ case 5:
+ if (string.Equals(key, "Allow", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x10u;
+ Allow = value;
+ return true;
+ }
+ if (string.Equals(key, "Range", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x80000000u;
+ Range = value;
+ return true;
+ }
+ break;
+ case 6:
+ if (string.Equals(key, "Accept", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x1u;
+ Accept = value;
+ return true;
+ }
+ if (string.Equals(key, "Cookie", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x8000u;
+ Cookie = value;
+ return true;
+ }
+ if (string.Equals(key, "Expect", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x20000u;
+ Expect = value;
+ return true;
+ }
+ if (string.Equals(key, "Pragma", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x20000000u;
+ Pragma = value;
+ return true;
+ }
+ break;
+ case 7:
+ if (string.Equals(key, "Expires", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x40000u;
+ Expires = value;
+ return true;
+ }
+ if (string.Equals(key, "Referer", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag1 |= 0x1u;
+ Referer = value;
+ return true;
+ }
+ if (string.Equals(key, "Trailer", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag1 |= 0x4u;
+ Trailer = value;
+ return true;
+ }
+ if (string.Equals(key, "Upgrade", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag1 |= 0x20u;
+ Upgrade = value;
+ return true;
+ }
+ if (string.Equals(key, "Warning", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag1 |= 0x100u;
+ Warning = value;
+ return true;
+ }
+ break;
+ case 8:
+ if (string.Equals(key, "If-Match", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x200000u;
+ IfMatch = value;
+ return true;
+ }
+ if (string.Equals(key, "If-Range", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x1000000u;
+ IfRange = value;
+ return true;
+ }
+ break;
+ case 9:
+ if (string.Equals(key, "Translate", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag1 |= 0x10u;
+ Translate = value;
+ return true;
+ }
+ break;
+ case 10:
+ if (string.Equals(key, "Connection", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x80u;
+ Connection = value;
+ return true;
+ }
+ if (string.Equals(key, "Keep-Alive", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x4000000u;
+ KeepAlive = value;
+ return true;
+ }
+ if (string.Equals(key, "User-Agent", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag1 |= 0x40u;
+ UserAgent = value;
+ return true;
+ }
+ break;
+ case 11:
+ if (string.Equals(key, "Content-Md5", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x1000u;
+ ContentMd5 = value;
+ return true;
+ }
+ break;
+ case 12:
+ if (string.Equals(key, "Content-Type", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x4000u;
+ ContentType = value;
+ return true;
+ }
+ if (string.Equals(key, "Max-Forwards", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x10000000u;
+ MaxForwards = value;
+ return true;
+ }
+ break;
+ case 13:
+ if (string.Equals(key, "Authorization", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x20u;
+ Authorization = value;
+ return true;
+ }
+ if (string.Equals(key, "Cache-Control", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x40u;
+ CacheControl = value;
+ return true;
+ }
+ if (string.Equals(key, "Content-Range", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x2000u;
+ ContentRange = value;
+ return true;
+ }
+ if (string.Equals(key, "If-None-Match", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x800000u;
+ IfNoneMatch = value;
+ return true;
+ }
+ if (string.Equals(key, "Last-Modified", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x8000000u;
+ LastModified = value;
+ return true;
+ }
+ break;
+ case 14:
+ if (string.Equals(key, "Accept-Charset", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x2u;
+ AcceptCharset = value;
+ return true;
+ }
+ if (string.Equals(key, "Content-Length", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x400u;
+ ContentLength = value;
+ return true;
+ }
+ break;
+ case 15:
+ if (string.Equals(key, "Accept-Encoding", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x4u;
+ AcceptEncoding = value;
+ return true;
+ }
+ if (string.Equals(key, "Accept-Language", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x8u;
+ AcceptLanguage = value;
+ return true;
+ }
+ break;
+ case 16:
+ if (string.Equals(key, "Content-Encoding", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x100u;
+ ContentEncoding = value;
+ return true;
+ }
+ if (string.Equals(key, "Content-Language", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x200u;
+ ContentLanguage = value;
+ return true;
+ }
+ if (string.Equals(key, "Content-Location", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x800u;
+ ContentLocation = value;
+ return true;
+ }
+ break;
+ case 17:
+ if (string.Equals(key, "If-Modified-Since", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x400000u;
+ IfModifiedSince = value;
+ return true;
+ }
+ if (string.Equals(key, "Transfer-Encoding", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag1 |= 0x8u;
+ TransferEncoding = value;
+ return true;
+ }
+ break;
+ case 19:
+ if (string.Equals(key, "If-Unmodified-Since", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x2000000u;
+ IfUnmodifiedSince = value;
+ return true;
+ }
+ if (string.Equals(key, "Proxy-Authorization", StringComparison.OrdinalIgnoreCase))
+ {
+ _flag0 |= 0x40000000u;
+ ProxyAuthorization = value;
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ private bool PropertiesTryRemove(string key)
+ {
+ switch (key.Length)
+ {
+ case 2:
+ if (_Te.Count > 0
+ && string.Equals(key, "Te", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag1 & 0x2u) != 0);
+ Te = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 3:
+ if (_Via.Count > 0
+ && string.Equals(key, "Via", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag1 & 0x80u) != 0);
+ Via = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 4:
+ if (_Date.Count > 0
+ && string.Equals(key, "Date", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x10000u) != 0);
+ Date = StringValues.Empty;
+ return wasSet;
+ }
+ if (_From.Count > 0
+ && string.Equals(key, "From", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x80000u) != 0);
+ From = StringValues.Empty;
+ return wasSet;
+ }
+ if (_Host.Count > 0
+ && string.Equals(key, "Host", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x100000u) != 0);
+ Host = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 5:
+ if (_Allow.Count > 0
+ && string.Equals(key, "Allow", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x10u) != 0);
+ Allow = StringValues.Empty;
+ return wasSet;
+ }
+ if (_Range.Count > 0
+ && string.Equals(key, "Range", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x80000000u) != 0);
+ Range = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 6:
+ if (_Accept.Count > 0
+ && string.Equals(key, "Accept", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x1u) != 0);
+ Accept = StringValues.Empty;
+ return wasSet;
+ }
+ if (_Cookie.Count > 0
+ && string.Equals(key, "Cookie", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x8000u) != 0);
+ Cookie = StringValues.Empty;
+ return wasSet;
+ }
+ if (_Expect.Count > 0
+ && string.Equals(key, "Expect", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x20000u) != 0);
+ Expect = StringValues.Empty;
+ return wasSet;
+ }
+ if (_Pragma.Count > 0
+ && string.Equals(key, "Pragma", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x20000000u) != 0);
+ Pragma = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 7:
+ if (_Expires.Count > 0
+ && string.Equals(key, "Expires", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x40000u) != 0);
+ Expires = StringValues.Empty;
+ return wasSet;
+ }
+ if (_Referer.Count > 0
+ && string.Equals(key, "Referer", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag1 & 0x1u) != 0);
+ Referer = StringValues.Empty;
+ return wasSet;
+ }
+ if (_Trailer.Count > 0
+ && string.Equals(key, "Trailer", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag1 & 0x4u) != 0);
+ Trailer = StringValues.Empty;
+ return wasSet;
+ }
+ if (_Upgrade.Count > 0
+ && string.Equals(key, "Upgrade", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag1 & 0x20u) != 0);
+ Upgrade = StringValues.Empty;
+ return wasSet;
+ }
+ if (_Warning.Count > 0
+ && string.Equals(key, "Warning", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag1 & 0x100u) != 0);
+ Warning = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 8:
+ if (_IfMatch.Count > 0
+ && string.Equals(key, "If-Match", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x200000u) != 0);
+ IfMatch = StringValues.Empty;
+ return wasSet;
+ }
+ if (_IfRange.Count > 0
+ && string.Equals(key, "If-Range", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x1000000u) != 0);
+ IfRange = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 9:
+ if (_Translate.Count > 0
+ && string.Equals(key, "Translate", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag1 & 0x10u) != 0);
+ Translate = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 10:
+ if (_Connection.Count > 0
+ && string.Equals(key, "Connection", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x80u) != 0);
+ Connection = StringValues.Empty;
+ return wasSet;
+ }
+ if (_KeepAlive.Count > 0
+ && string.Equals(key, "Keep-Alive", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x4000000u) != 0);
+ KeepAlive = StringValues.Empty;
+ return wasSet;
+ }
+ if (_UserAgent.Count > 0
+ && string.Equals(key, "User-Agent", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag1 & 0x40u) != 0);
+ UserAgent = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 11:
+ if (_ContentMd5.Count > 0
+ && string.Equals(key, "Content-Md5", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x1000u) != 0);
+ ContentMd5 = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 12:
+ if (_ContentType.Count > 0
+ && string.Equals(key, "Content-Type", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x4000u) != 0);
+ ContentType = StringValues.Empty;
+ return wasSet;
+ }
+ if (_MaxForwards.Count > 0
+ && string.Equals(key, "Max-Forwards", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x10000000u) != 0);
+ MaxForwards = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 13:
+ if (_Authorization.Count > 0
+ && string.Equals(key, "Authorization", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x20u) != 0);
+ Authorization = StringValues.Empty;
+ return wasSet;
+ }
+ if (_CacheControl.Count > 0
+ && string.Equals(key, "Cache-Control", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x40u) != 0);
+ CacheControl = StringValues.Empty;
+ return wasSet;
+ }
+ if (_ContentRange.Count > 0
+ && string.Equals(key, "Content-Range", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x2000u) != 0);
+ ContentRange = StringValues.Empty;
+ return wasSet;
+ }
+ if (_IfNoneMatch.Count > 0
+ && string.Equals(key, "If-None-Match", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x800000u) != 0);
+ IfNoneMatch = StringValues.Empty;
+ return wasSet;
+ }
+ if (_LastModified.Count > 0
+ && string.Equals(key, "Last-Modified", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x8000000u) != 0);
+ LastModified = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 14:
+ if (_AcceptCharset.Count > 0
+ && string.Equals(key, "Accept-Charset", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x2u) != 0);
+ AcceptCharset = StringValues.Empty;
+ return wasSet;
+ }
+ if (_ContentLength.Count > 0
+ && string.Equals(key, "Content-Length", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x400u) != 0);
+ ContentLength = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 15:
+ if (_AcceptEncoding.Count > 0
+ && string.Equals(key, "Accept-Encoding", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x4u) != 0);
+ AcceptEncoding = StringValues.Empty;
+ return wasSet;
+ }
+ if (_AcceptLanguage.Count > 0
+ && string.Equals(key, "Accept-Language", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x8u) != 0);
+ AcceptLanguage = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 16:
+ if (_ContentEncoding.Count > 0
+ && string.Equals(key, "Content-Encoding", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x100u) != 0);
+ ContentEncoding = StringValues.Empty;
+ return wasSet;
+ }
+ if (_ContentLanguage.Count > 0
+ && string.Equals(key, "Content-Language", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x200u) != 0);
+ ContentLanguage = StringValues.Empty;
+ return wasSet;
+ }
+ if (_ContentLocation.Count > 0
+ && string.Equals(key, "Content-Location", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x800u) != 0);
+ ContentLocation = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 17:
+ if (_IfModifiedSince.Count > 0
+ && string.Equals(key, "If-Modified-Since", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x400000u) != 0);
+ IfModifiedSince = StringValues.Empty;
+ return wasSet;
+ }
+ if (_TransferEncoding.Count > 0
+ && string.Equals(key, "Transfer-Encoding", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag1 & 0x8u) != 0);
+ TransferEncoding = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ case 19:
+ if (_IfUnmodifiedSince.Count > 0
+ && string.Equals(key, "If-Unmodified-Since", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x2000000u) != 0);
+ IfUnmodifiedSince = StringValues.Empty;
+ return wasSet;
+ }
+ if (_ProxyAuthorization.Count > 0
+ && string.Equals(key, "Proxy-Authorization", StringComparison.Ordinal))
+ {
+ bool wasSet = ((_flag0 & 0x40000000u) != 0);
+ ProxyAuthorization = StringValues.Empty;
+ return wasSet;
+ }
+ break;
+ }
+ return false;
+ }
+
+ private IEnumerable<string> PropertiesKeys()
+ {
+ if (Accept.Count > 0)
+ {
+ yield return "Accept";
+ }
+ if (AcceptCharset.Count > 0)
+ {
+ yield return "Accept-Charset";
+ }
+ if (AcceptEncoding.Count > 0)
+ {
+ yield return "Accept-Encoding";
+ }
+ if (AcceptLanguage.Count > 0)
+ {
+ yield return "Accept-Language";
+ }
+ if (Allow.Count > 0)
+ {
+ yield return "Allow";
+ }
+ if (Authorization.Count > 0)
+ {
+ yield return "Authorization";
+ }
+ if (CacheControl.Count > 0)
+ {
+ yield return "Cache-Control";
+ }
+ if (Connection.Count > 0)
+ {
+ yield return "Connection";
+ }
+ if (ContentEncoding.Count > 0)
+ {
+ yield return "Content-Encoding";
+ }
+ if (ContentLanguage.Count > 0)
+ {
+ yield return "Content-Language";
+ }
+ if (ContentLength.Count > 0)
+ {
+ yield return "Content-Length";
+ }
+ if (ContentLocation.Count > 0)
+ {
+ yield return "Content-Location";
+ }
+ if (ContentMd5.Count > 0)
+ {
+ yield return "Content-Md5";
+ }
+ if (ContentRange.Count > 0)
+ {
+ yield return "Content-Range";
+ }
+ if (ContentType.Count > 0)
+ {
+ yield return "Content-Type";
+ }
+ if (Cookie.Count > 0)
+ {
+ yield return "Cookie";
+ }
+ if (Date.Count > 0)
+ {
+ yield return "Date";
+ }
+ if (Expect.Count > 0)
+ {
+ yield return "Expect";
+ }
+ if (Expires.Count > 0)
+ {
+ yield return "Expires";
+ }
+ if (From.Count > 0)
+ {
+ yield return "From";
+ }
+ if (Host.Count > 0)
+ {
+ yield return "Host";
+ }
+ if (IfMatch.Count > 0)
+ {
+ yield return "If-Match";
+ }
+ if (IfModifiedSince.Count > 0)
+ {
+ yield return "If-Modified-Since";
+ }
+ if (IfNoneMatch.Count > 0)
+ {
+ yield return "If-None-Match";
+ }
+ if (IfRange.Count > 0)
+ {
+ yield return "If-Range";
+ }
+ if (IfUnmodifiedSince.Count > 0)
+ {
+ yield return "If-Unmodified-Since";
+ }
+ if (KeepAlive.Count > 0)
+ {
+ yield return "Keep-Alive";
+ }
+ if (LastModified.Count > 0)
+ {
+ yield return "Last-Modified";
+ }
+ if (MaxForwards.Count > 0)
+ {
+ yield return "Max-Forwards";
+ }
+ if (Pragma.Count > 0)
+ {
+ yield return "Pragma";
+ }
+ if (ProxyAuthorization.Count > 0)
+ {
+ yield return "Proxy-Authorization";
+ }
+ if (Range.Count > 0)
+ {
+ yield return "Range";
+ }
+ if (Referer.Count > 0)
+ {
+ yield return "Referer";
+ }
+ if (Te.Count > 0)
+ {
+ yield return "Te";
+ }
+ if (Trailer.Count > 0)
+ {
+ yield return "Trailer";
+ }
+ if (TransferEncoding.Count > 0)
+ {
+ yield return "Transfer-Encoding";
+ }
+ if (Translate.Count > 0)
+ {
+ yield return "Translate";
+ }
+ if (Upgrade.Count > 0)
+ {
+ yield return "Upgrade";
+ }
+ if (UserAgent.Count > 0)
+ {
+ yield return "User-Agent";
+ }
+ if (Via.Count > 0)
+ {
+ yield return "Via";
+ }
+ if (Warning.Count > 0)
+ {
+ yield return "Warning";
+ }
+ }
+
+ private IEnumerable<StringValues> PropertiesValues()
+ {
+ if (Accept.Count > 0)
+ {
+ yield return Accept;
+ }
+ if (AcceptCharset.Count > 0)
+ {
+ yield return AcceptCharset;
+ }
+ if (AcceptEncoding.Count > 0)
+ {
+ yield return AcceptEncoding;
+ }
+ if (AcceptLanguage.Count > 0)
+ {
+ yield return AcceptLanguage;
+ }
+ if (Allow.Count > 0)
+ {
+ yield return Allow;
+ }
+ if (Authorization.Count > 0)
+ {
+ yield return Authorization;
+ }
+ if (CacheControl.Count > 0)
+ {
+ yield return CacheControl;
+ }
+ if (Connection.Count > 0)
+ {
+ yield return Connection;
+ }
+ if (ContentEncoding.Count > 0)
+ {
+ yield return ContentEncoding;
+ }
+ if (ContentLanguage.Count > 0)
+ {
+ yield return ContentLanguage;
+ }
+ if (ContentLength.Count > 0)
+ {
+ yield return ContentLength;
+ }
+ if (ContentLocation.Count > 0)
+ {
+ yield return ContentLocation;
+ }
+ if (ContentMd5.Count > 0)
+ {
+ yield return ContentMd5;
+ }
+ if (ContentRange.Count > 0)
+ {
+ yield return ContentRange;
+ }
+ if (ContentType.Count > 0)
+ {
+ yield return ContentType;
+ }
+ if (Cookie.Count > 0)
+ {
+ yield return Cookie;
+ }
+ if (Date.Count > 0)
+ {
+ yield return Date;
+ }
+ if (Expect.Count > 0)
+ {
+ yield return Expect;
+ }
+ if (Expires.Count > 0)
+ {
+ yield return Expires;
+ }
+ if (From.Count > 0)
+ {
+ yield return From;
+ }
+ if (Host.Count > 0)
+ {
+ yield return Host;
+ }
+ if (IfMatch.Count > 0)
+ {
+ yield return IfMatch;
+ }
+ if (IfModifiedSince.Count > 0)
+ {
+ yield return IfModifiedSince;
+ }
+ if (IfNoneMatch.Count > 0)
+ {
+ yield return IfNoneMatch;
+ }
+ if (IfRange.Count > 0)
+ {
+ yield return IfRange;
+ }
+ if (IfUnmodifiedSince.Count > 0)
+ {
+ yield return IfUnmodifiedSince;
+ }
+ if (KeepAlive.Count > 0)
+ {
+ yield return KeepAlive;
+ }
+ if (LastModified.Count > 0)
+ {
+ yield return LastModified;
+ }
+ if (MaxForwards.Count > 0)
+ {
+ yield return MaxForwards;
+ }
+ if (Pragma.Count > 0)
+ {
+ yield return Pragma;
+ }
+ if (ProxyAuthorization.Count > 0)
+ {
+ yield return ProxyAuthorization;
+ }
+ if (Range.Count > 0)
+ {
+ yield return Range;
+ }
+ if (Referer.Count > 0)
+ {
+ yield return Referer;
+ }
+ if (Te.Count > 0)
+ {
+ yield return Te;
+ }
+ if (Trailer.Count > 0)
+ {
+ yield return Trailer;
+ }
+ if (TransferEncoding.Count > 0)
+ {
+ yield return TransferEncoding;
+ }
+ if (Translate.Count > 0)
+ {
+ yield return Translate;
+ }
+ if (Upgrade.Count > 0)
+ {
+ yield return Upgrade;
+ }
+ if (UserAgent.Count > 0)
+ {
+ yield return UserAgent;
+ }
+ if (Via.Count > 0)
+ {
+ yield return Via;
+ }
+ if (Warning.Count > 0)
+ {
+ yield return Warning;
+ }
+ }
+
+ private IEnumerable<KeyValuePair<string, StringValues>> PropertiesEnumerable()
+ {
+ if (Accept.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Accept", Accept);
+ }
+ if (AcceptCharset.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Accept-Charset", AcceptCharset);
+ }
+ if (AcceptEncoding.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Accept-Encoding", AcceptEncoding);
+ }
+ if (AcceptLanguage.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Accept-Language", AcceptLanguage);
+ }
+ if (Allow.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Allow", Allow);
+ }
+ if (Authorization.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Authorization", Authorization);
+ }
+ if (CacheControl.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Cache-Control", CacheControl);
+ }
+ if (Connection.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Connection", Connection);
+ }
+ if (ContentEncoding.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Content-Encoding", ContentEncoding);
+ }
+ if (ContentLanguage.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Content-Language", ContentLanguage);
+ }
+ if (ContentLength.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Content-Length", ContentLength);
+ }
+ if (ContentLocation.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Content-Location", ContentLocation);
+ }
+ if (ContentMd5.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Content-Md5", ContentMd5);
+ }
+ if (ContentRange.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Content-Range", ContentRange);
+ }
+ if (ContentType.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Content-Type", ContentType);
+ }
+ if (Cookie.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Cookie", Cookie);
+ }
+ if (Date.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Date", Date);
+ }
+ if (Expect.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Expect", Expect);
+ }
+ if (Expires.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Expires", Expires);
+ }
+ if (From.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("From", From);
+ }
+ if (Host.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Host", Host);
+ }
+ if (IfMatch.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("If-Match", IfMatch);
+ }
+ if (IfModifiedSince.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("If-Modified-Since", IfModifiedSince);
+ }
+ if (IfNoneMatch.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("If-None-Match", IfNoneMatch);
+ }
+ if (IfRange.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("If-Range", IfRange);
+ }
+ if (IfUnmodifiedSince.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("If-Unmodified-Since", IfUnmodifiedSince);
+ }
+ if (KeepAlive.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Keep-Alive", KeepAlive);
+ }
+ if (LastModified.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Last-Modified", LastModified);
+ }
+ if (MaxForwards.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Max-Forwards", MaxForwards);
+ }
+ if (Pragma.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Pragma", Pragma);
+ }
+ if (ProxyAuthorization.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Proxy-Authorization", ProxyAuthorization);
+ }
+ if (Range.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Range", Range);
+ }
+ if (Referer.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Referer", Referer);
+ }
+ if (Te.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Te", Te);
+ }
+ if (Trailer.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Trailer", Trailer);
+ }
+ if (TransferEncoding.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Transfer-Encoding", TransferEncoding);
+ }
+ if (Translate.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Translate", Translate);
+ }
+ if (Upgrade.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Upgrade", Upgrade);
+ }
+ if (UserAgent.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("User-Agent", UserAgent);
+ }
+ if (Via.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Via", Via);
+ }
+ if (Warning.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("Warning", Warning);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RequestHeaders.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RequestHeaders.cs
new file mode 100644
index 0000000000..0f87c3565a
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RequestHeaders.cs
@@ -0,0 +1,265 @@
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ internal partial class RequestHeaders : IHeaderDictionary
+ {
+ private IDictionary<string, StringValues> _extra;
+ private NativeRequestContext _requestMemoryBlob;
+ private long? _contentLength;
+ private StringValues _contentLengthText;
+
+ internal RequestHeaders(NativeRequestContext requestMemoryBlob)
+ {
+ _requestMemoryBlob = requestMemoryBlob;
+ }
+
+ public bool IsReadOnly { get; internal set; }
+
+ private IDictionary<string, StringValues> Extra
+ {
+ get
+ {
+ if (_extra == null)
+ {
+ var newDict = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
+ GetUnknownHeaders(newDict);
+ Interlocked.CompareExchange(ref _extra, newDict, null);
+ }
+ return _extra;
+ }
+ }
+
+ StringValues IDictionary<string, StringValues>.this[string key]
+ {
+ get
+ {
+ StringValues value;
+ return PropertiesTryGetValue(key, out value) ? value : Extra[key];
+ }
+ set
+ {
+ ThrowIfReadOnly();
+ if (!PropertiesTrySetValue(key, value))
+ {
+ Extra[key] = value;
+ }
+ }
+ }
+
+ private string GetKnownHeader(HttpSysRequestHeader header)
+ {
+ return _requestMemoryBlob.GetKnownHeader(header);
+ }
+
+ private void GetUnknownHeaders(IDictionary<string, StringValues> extra)
+ {
+ _requestMemoryBlob.GetUnknownHeaders(extra);
+ }
+
+ void IDictionary<string, StringValues>.Add(string key, StringValues value)
+ {
+ if (!PropertiesTrySetValue(key, value))
+ {
+ Extra.Add(key, value);
+ }
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return PropertiesContainsKey(key) || Extra.ContainsKey(key);
+ }
+
+ public ICollection<string> Keys
+ {
+ get { return PropertiesKeys().Concat(Extra.Keys).ToArray(); }
+ }
+
+ ICollection<StringValues> IDictionary<string, StringValues>.Values
+ {
+ get { return PropertiesValues().Concat(Extra.Values).ToArray(); }
+ }
+
+ public int Count
+ {
+ get { return PropertiesKeys().Count() + Extra.Count; }
+ }
+
+ public bool Remove(string key)
+ {
+ // Although this is a mutating operation, Extra is used instead of StrongExtra,
+ // because if a real dictionary has not been allocated the default behavior of the
+ // nil dictionary is perfectly fine.
+ return PropertiesTryRemove(key) || Extra.Remove(key);
+ }
+
+ public bool TryGetValue(string key, out StringValues value)
+ {
+ return PropertiesTryGetValue(key, out value) || Extra.TryGetValue(key, out value);
+ }
+
+ void ICollection<KeyValuePair<string, StringValues>>.Add(KeyValuePair<string, StringValues> item)
+ {
+ ((IDictionary<string, object>)this).Add(item.Key, item.Value);
+ }
+
+ void ICollection<KeyValuePair<string, StringValues>>.Clear()
+ {
+ foreach (var key in PropertiesKeys())
+ {
+ PropertiesTryRemove(key);
+ }
+ Extra.Clear();
+ }
+
+ bool ICollection<KeyValuePair<string, StringValues>>.Contains(KeyValuePair<string, StringValues> item)
+ {
+ object value;
+ return ((IDictionary<string, object>)this).TryGetValue(item.Key, out value) && Object.Equals(value, item.Value);
+ }
+
+ void ICollection<KeyValuePair<string, StringValues>>.CopyTo(KeyValuePair<string, StringValues>[] array, int arrayIndex)
+ {
+ PropertiesEnumerable().Concat(Extra).ToArray().CopyTo(array, arrayIndex);
+ }
+
+ bool ICollection<KeyValuePair<string, StringValues>>.IsReadOnly
+ {
+ get { return false; }
+ }
+
+ long? IHeaderDictionary.ContentLength
+ {
+ get
+ {
+ long value;
+ var rawValue = this[HttpKnownHeaderNames.ContentLength];
+
+ if (_contentLengthText.Equals(rawValue))
+ {
+ return _contentLength;
+ }
+
+ if (rawValue.Count == 1 &&
+ !string.IsNullOrWhiteSpace(rawValue[0]) &&
+ HeaderUtilities.TryParseNonNegativeInt64(new StringSegment(rawValue[0]).Trim(), out value))
+ {
+ _contentLengthText = rawValue;
+ _contentLength = value;
+ return value;
+ }
+
+ return null;
+ }
+ set
+ {
+ ThrowIfReadOnly();
+
+ if (value.HasValue)
+ {
+ if (value.Value < 0)
+ {
+ throw new ArgumentOutOfRangeException("value", value.Value, "Cannot be negative.");
+ }
+ _contentLengthText = HeaderUtilities.FormatNonNegativeInt64(value.Value);
+ this[HttpKnownHeaderNames.ContentLength] = _contentLengthText;
+ _contentLength = value;
+ }
+ else
+ {
+ Remove(HttpKnownHeaderNames.ContentLength);
+ _contentLengthText = StringValues.Empty;
+ _contentLength = null;
+ }
+ }
+ }
+
+ public StringValues this[string key]
+ {
+ get
+ {
+ StringValues values;
+ return TryGetValue(key, out values) ? values : StringValues.Empty;
+ }
+ set
+ {
+ if (StringValues.IsNullOrEmpty(value))
+ {
+ Remove(key);
+ }
+ else
+ {
+ Extra[key] = value;
+ }
+ }
+ }
+
+ StringValues IHeaderDictionary.this[string key]
+ {
+ get
+ {
+ if (PropertiesTryGetValue(key, out var value))
+ {
+ return value;
+ }
+
+ if (Extra.TryGetValue(key, out value))
+ {
+ return value;
+ }
+ return StringValues.Empty;
+ }
+ set
+ {
+ if (!PropertiesTrySetValue(key, value))
+ {
+ Extra[key] = value;
+ }
+ }
+ }
+
+ bool ICollection<KeyValuePair<string, StringValues>>.Remove(KeyValuePair<string, StringValues> item)
+ {
+ return ((IDictionary<string, StringValues>)this).Contains(item) &&
+ ((IDictionary<string, StringValues>)this).Remove(item.Key);
+ }
+
+ IEnumerator<KeyValuePair<string, StringValues>> IEnumerable<KeyValuePair<string, StringValues>>.GetEnumerator()
+ {
+ return PropertiesEnumerable().Concat(Extra).GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IDictionary<string, StringValues>)this).GetEnumerator();
+ }
+
+ private void ThrowIfReadOnly()
+ {
+ if (IsReadOnly)
+ {
+ throw new InvalidOperationException("The response headers cannot be modified because the response has already started.");
+ }
+ }
+
+ public IEnumerable<string> GetValues(string key)
+ {
+ StringValues values;
+ if (TryGetValue(key, out values))
+ {
+ return HeaderParser.SplitValues(values);
+ }
+ return HeaderParser.Empty;
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RequestUriBuilder.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RequestUriBuilder.cs
new file mode 100644
index 0000000000..6308d6d8ea
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/RequestUriBuilder.cs
@@ -0,0 +1,345 @@
+// 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.Text;
+
+namespace Microsoft.AspNetCore.HttpSys.Internal
+{
+ // We don't use the cooked URL because http.sys unescapes all percent-encoded values. However,
+ // we also can't just use the raw Uri, since http.sys supports not only UTF-8, but also ANSI/DBCS and
+ // Unicode code points. System.Uri only supports UTF-8.
+ // The purpose of this class is to decode all UTF-8 percent encoded characters, with the
+ // exception of %2F ('/'), which is left encoded
+ internal static class RequestUriBuilder
+ {
+ private static readonly Encoding UTF8 = new UTF8Encoding(
+ encoderShouldEmitUTF8Identifier: false,
+ throwOnInvalidBytes: true);
+
+ public static string DecodeAndUnescapePath(byte[] rawUrlBytes)
+ {
+ if (rawUrlBytes == null)
+ {
+ throw new ArgumentNullException(nameof(rawUrlBytes));
+ }
+
+ if (rawUrlBytes.Length == 0)
+ {
+ throw new ArgumentException("Length of the URL cannot be zero.", nameof(rawUrlBytes));
+ }
+
+ var rawPath = RawUrlHelper.GetPath(rawUrlBytes);
+
+ var unescapedPath = Unescape(rawPath);
+
+ return UTF8.GetString(unescapedPath.Array, unescapedPath.Offset, unescapedPath.Count);
+ }
+
+ /// <summary>
+ /// Unescape a given path string in place. The given path string may contain escaped char.
+ /// </summary>
+ /// <param name="rawPath">The raw path string to be unescaped</param>
+ /// <returns>The unescaped path string</returns>
+ private static ArraySegment<byte> Unescape(ArraySegment<byte> rawPath)
+ {
+ // the slot to read the input
+ var reader = rawPath.Offset;
+
+ // the slot to write the unescaped byte
+ var writer = rawPath.Offset;
+
+ // the end of the path
+ var end = rawPath.Offset + rawPath.Count;
+
+ // the byte array
+ var buffer = rawPath.Array;
+
+ while (true)
+ {
+ if (reader == end)
+ {
+ break;
+ }
+
+ if (rawPath.Array[reader] == '%')
+ {
+ var decodeReader = reader;
+
+ // If decoding process succeeds, the writer iterator will be moved
+ // to the next write-ready location. On the other hand if the scanned
+ // percent-encodings cannot be interpreted as sequence of UTF-8 octets,
+ // these bytes should be copied to output as is.
+ // The decodeReader iterator is always moved to the first byte not yet
+ // be scanned after the process. A failed decoding means the chars
+ // between the reader and decodeReader can be copied to output untouched.
+ if (!DecodeCore(ref decodeReader, ref writer, end, buffer))
+ {
+ Copy(reader, decodeReader, ref writer, buffer);
+ }
+
+ reader = decodeReader;
+ }
+ else
+ {
+ buffer[writer++] = buffer[reader++];
+ }
+ }
+
+ return new ArraySegment<byte>(buffer, rawPath.Offset, writer - rawPath.Offset);
+ }
+
+ /// <summary>
+ /// Unescape the percent-encodings
+ /// </summary>
+ /// <param name="reader">The iterator point to the first % char</param>
+ /// <param name="writer">The place to write to</param>
+ /// <param name="end">The end of the buffer</param>
+ /// <param name="buffer">The byte array</param>
+ private static bool DecodeCore(ref int reader, ref int writer, int end, byte[] buffer)
+ {
+ // preserves the original head. if the percent-encodings cannot be interpreted as sequence of UTF-8 octets,
+ // bytes from this till the last scanned one will be copied to the memory pointed by writer.
+ var byte1 = UnescapePercentEncoding(ref reader, end, buffer);
+
+ if (!byte1.HasValue)
+ {
+ return false;
+ }
+
+ if (byte1 == 0)
+ {
+ throw new InvalidOperationException("The path contains null characters.");
+ }
+
+ if (byte1 <= 0x7F)
+ {
+ // first byte < U+007f, it is a single byte ASCII
+ buffer[writer++] = (byte)byte1;
+ return true;
+ }
+
+ int byte2 = 0, byte3 = 0, byte4 = 0;
+
+ // anticipate more bytes
+ var currentDecodeBits = 0;
+ var byteCount = 1;
+ var expectValueMin = 0;
+ if ((byte1 & 0xE0) == 0xC0)
+ {
+ // 110x xxxx, expect one more byte
+ currentDecodeBits = byte1.Value & 0x1F;
+ byteCount = 2;
+ expectValueMin = 0x80;
+ }
+ else if ((byte1 & 0xF0) == 0xE0)
+ {
+ // 1110 xxxx, expect two more bytes
+ currentDecodeBits = byte1.Value & 0x0F;
+ byteCount = 3;
+ expectValueMin = 0x800;
+ }
+ else if ((byte1 & 0xF8) == 0xF0)
+ {
+ // 1111 0xxx, expect three more bytes
+ currentDecodeBits = byte1.Value & 0x07;
+ byteCount = 4;
+ expectValueMin = 0x10000;
+ }
+ else
+ {
+ // invalid first byte
+ return false;
+ }
+
+ var remainingBytes = byteCount - 1;
+ while (remainingBytes > 0)
+ {
+ // read following three chars
+ if (reader == buffer.Length)
+ {
+ return false;
+ }
+
+ var nextItr = reader;
+ var nextByte = UnescapePercentEncoding(ref nextItr, end, buffer);
+ if (!nextByte.HasValue)
+ {
+ return false;
+ }
+
+ if ((nextByte & 0xC0) != 0x80)
+ {
+ // the follow up byte is not in form of 10xx xxxx
+ return false;
+ }
+
+ currentDecodeBits = (currentDecodeBits << 6) | (nextByte.Value & 0x3F);
+ remainingBytes--;
+
+ if (remainingBytes == 1 && currentDecodeBits >= 0x360 && currentDecodeBits <= 0x37F)
+ {
+ // this is going to end up in the range of 0xD800-0xDFFF UTF-16 surrogates that
+ // are not allowed in UTF-8;
+ return false;
+ }
+
+ if (remainingBytes == 2 && currentDecodeBits >= 0x110)
+ {
+ // this is going to be out of the upper Unicode bound 0x10FFFF.
+ return false;
+ }
+
+ reader = nextItr;
+ if (byteCount - remainingBytes == 2)
+ {
+ byte2 = nextByte.Value;
+ }
+ else if (byteCount - remainingBytes == 3)
+ {
+ byte3 = nextByte.Value;
+ }
+ else if (byteCount - remainingBytes == 4)
+ {
+ byte4 = nextByte.Value;
+ }
+ }
+
+ if (currentDecodeBits < expectValueMin)
+ {
+ // overlong encoding (e.g. using 2 bytes to encode something that only needed 1).
+ return false;
+ }
+
+ // all bytes are verified, write to the output
+ if (byteCount > 0)
+ {
+ buffer[writer++] = (byte)byte1;
+ }
+ if (byteCount > 1)
+ {
+ buffer[writer++] = (byte)byte2;
+ }
+ if (byteCount > 2)
+ {
+ buffer[writer++] = (byte)byte3;
+ }
+ if (byteCount > 3)
+ {
+ buffer[writer++] = (byte)byte4;
+ }
+
+ return true;
+ }
+
+ private static void Copy(int begin, int end, ref int writer, byte[] buffer)
+ {
+ while (begin != end)
+ {
+ buffer[writer++] = buffer[begin++];
+ }
+ }
+
+ /// <summary>
+ /// Read the percent-encoding and try unescape it.
+ ///
+ /// The operation first peek at the character the <paramref name="scan"/>
+ /// iterator points at. If it is % the <paramref name="scan"/> is then
+ /// moved on to scan the following to characters. If the two following
+ /// characters are hexadecimal literals they will be unescaped and the
+ /// value will be returned.
+ ///
+ /// If the first character is not % the <paramref name="scan"/> iterator
+ /// will be removed beyond the location of % and -1 will be returned.
+ ///
+ /// If the following two characters can't be successfully unescaped the
+ /// <paramref name="scan"/> iterator will be move behind the % and -1
+ /// will be returned.
+ /// </summary>
+ /// <param name="scan">The value to read</param>
+ /// <param name="end">The end of the buffer</param>
+ /// <param name="buffer">The byte array</param>
+ /// <returns>The unescaped byte if success. Otherwise return -1.</returns>
+ private static int? UnescapePercentEncoding(ref int scan, int end, byte[] buffer)
+ {
+ if (buffer[scan++] != '%')
+ {
+ return -1;
+ }
+
+ var probe = scan;
+
+ var value1 = ReadHex(ref probe, end, buffer);
+ if (!value1.HasValue)
+ {
+ return null;
+ }
+
+ var value2 = ReadHex(ref probe, end, buffer);
+ if (!value2.HasValue)
+ {
+ return null;
+ }
+
+ if (SkipUnescape(value1.Value, value2.Value))
+ {
+ return null;
+ }
+
+ scan = probe;
+ return (value1.Value << 4) + value2.Value;
+ }
+
+ /// <summary>
+ /// Read the next char and convert it into hexadecimal value.
+ ///
+ /// The <paramref name="scan"/> iterator will be moved to the next
+ /// byte no matter no matter whether the operation successes.
+ /// </summary>
+ /// <param name="scan">The value to read</param>
+ /// <param name="end">The end of the buffer</param>
+ /// <param name="buffer">The byte array</param>
+ /// <returns>The hexadecimal value if successes, otherwise -1.</returns>
+ private static int? ReadHex(ref int scan, int end, byte[] buffer)
+ {
+ if (scan == end)
+ {
+ return null;
+ }
+
+ var value = buffer[scan++];
+ var isHead = (((value >= '0') && (value <= '9')) ||
+ ((value >= 'A') && (value <= 'F')) ||
+ ((value >= 'a') && (value <= 'f')));
+
+ if (!isHead)
+ {
+ return null;
+ }
+
+ if (value <= '9')
+ {
+ return value - '0';
+ }
+ else if (value <= 'F')
+ {
+ return (value - 'A') + 10;
+ }
+ else // a - f
+ {
+ return (value - 'a') + 10;
+ }
+ }
+
+ private static bool SkipUnescape(int value1, int value2)
+ {
+ // skip %2F - '/'
+ if (value1 == 2 && value2 == 15)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/SslStatus.cs b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/SslStatus.cs
new file mode 100644
index 0000000000..5154a0e9da
--- /dev/null
+++ b/src/HttpSysServer/shared/Microsoft.AspNetCore.HttpSys.Sources/RequestProcessing/SslStatus.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.HttpSys.Internal
+{
+ internal enum SslStatus : byte
+ {
+ Insecure,
+ NoClientCert,
+ ClientCert
+ }
+}
diff --git a/src/HttpSysServer/src/Directory.Build.props b/src/HttpSysServer/src/Directory.Build.props
new file mode 100644
index 0000000000..4b89a431e7
--- /dev/null
+++ b/src/HttpSysServer/src/Directory.Build.props
@@ -0,0 +1,7 @@
+<Project>
+ <Import Project="..\Directory.Build.props" />
+
+ <ItemGroup>
+ <PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AsyncAcceptContext.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AsyncAcceptContext.cs
new file mode 100644
index 0000000000..4c0cd86756
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AsyncAcceptContext.cs
@@ -0,0 +1,260 @@
+// 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.CodeAnalysis;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.HttpSys.Internal;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal unsafe class AsyncAcceptContext : IAsyncResult, IDisposable
+ {
+ internal static readonly IOCompletionCallback IOCallback = new IOCompletionCallback(IOWaitCallback);
+
+ private TaskCompletionSource<RequestContext> _tcs;
+ private HttpSysListener _server;
+ private NativeRequestContext _nativeRequestContext;
+ private const int DefaultBufferSize = 4096;
+ private const int AlignmentPadding = 8;
+
+ internal AsyncAcceptContext(HttpSysListener server)
+ {
+ _server = server;
+ _tcs = new TaskCompletionSource<RequestContext>();
+ AllocateNativeRequest();
+ }
+
+ internal Task<RequestContext> Task
+ {
+ get
+ {
+ return _tcs.Task;
+ }
+ }
+
+ private TaskCompletionSource<RequestContext> Tcs
+ {
+ get
+ {
+ return _tcs;
+ }
+ }
+
+ internal HttpSysListener Server
+ {
+ get
+ {
+ return _server;
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Redirecting to callback")]
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Disposed by callback")]
+ private static void IOCompleted(AsyncAcceptContext asyncResult, uint errorCode, uint numBytes)
+ {
+ bool complete = false;
+ try
+ {
+ if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
+ errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA)
+ {
+ asyncResult.Tcs.TrySetException(new HttpSysException((int)errorCode));
+ complete = true;
+ }
+ else
+ {
+ HttpSysListener server = asyncResult.Server;
+ if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
+ {
+ // at this point we have received an unmanaged HTTP_REQUEST and memoryBlob
+ // points to it we need to hook up our authentication handling code here.
+ try
+ {
+ if (server.ValidateRequest(asyncResult._nativeRequestContext) && server.ValidateAuth(asyncResult._nativeRequestContext))
+ {
+ RequestContext requestContext = new RequestContext(server, asyncResult._nativeRequestContext);
+ asyncResult.Tcs.TrySetResult(requestContext);
+ complete = true;
+ }
+ }
+ catch (Exception)
+ {
+ server.SendError(asyncResult._nativeRequestContext.RequestId, StatusCodes.Status400BadRequest);
+ throw;
+ }
+ finally
+ {
+ // The request has been handed to the user, which means this code can't reuse the blob. Reset it here.
+ if (complete)
+ {
+ asyncResult._nativeRequestContext = null;
+ }
+ else
+ {
+ asyncResult.AllocateNativeRequest(size: asyncResult._nativeRequestContext.Size);
+ }
+ }
+ }
+ else
+ {
+ // (uint)backingBuffer.Length - AlignmentPadding
+ asyncResult.AllocateNativeRequest(numBytes, asyncResult._nativeRequestContext.RequestId);
+ }
+
+ // We need to issue a new request, either because auth failed, or because our buffer was too small the first time.
+ if (!complete)
+ {
+ uint statusCode = asyncResult.QueueBeginGetContext();
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
+ statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
+ {
+ // someother bad error, possible(?) return values are:
+ // ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
+ asyncResult.Tcs.TrySetException(new HttpSysException((int)statusCode));
+ complete = true;
+ }
+ }
+ if (!complete)
+ {
+ return;
+ }
+ }
+
+ if (complete)
+ {
+ asyncResult.Dispose();
+ }
+ }
+ catch (Exception exception)
+ {
+ // Logged by caller
+ asyncResult.Tcs.TrySetException(exception);
+ asyncResult.Dispose();
+ }
+ }
+
+ private static unsafe void IOWaitCallback(uint errorCode, uint numBytes, NativeOverlapped* nativeOverlapped)
+ {
+ // take the ListenerAsyncResult object from the state
+ var asyncResult = (AsyncAcceptContext)ThreadPoolBoundHandle.GetNativeOverlappedState(nativeOverlapped);
+ IOCompleted(asyncResult, errorCode, numBytes);
+ }
+
+ internal uint QueueBeginGetContext()
+ {
+ uint statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS;
+ bool retry;
+ do
+ {
+ retry = false;
+ uint bytesTransferred = 0;
+ statusCode = HttpApi.HttpReceiveHttpRequest(
+ Server.RequestQueue.Handle,
+ _nativeRequestContext.RequestId,
+ (uint)HttpApiTypes.HTTP_FLAGS.HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY,
+ _nativeRequestContext.NativeRequest,
+ _nativeRequestContext.Size,
+ &bytesTransferred,
+ _nativeRequestContext.NativeOverlapped);
+
+ if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_INVALID_PARAMETER && _nativeRequestContext.RequestId != 0)
+ {
+ // we might get this if somebody stole our RequestId,
+ // set RequestId to 0 and start all over again with the buffer we just allocated
+ // BUGBUG: how can someone steal our request ID? seems really bad and in need of fix.
+ _nativeRequestContext.RequestId = 0;
+ retry = true;
+ }
+ else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA)
+ {
+ // the buffer was not big enough to fit the headers, we need
+ // to read the RequestId returned, allocate a new buffer of the required size
+ // (uint)backingBuffer.Length - AlignmentPadding
+ AllocateNativeRequest(bytesTransferred);
+ retry = true;
+ }
+ else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS
+ && HttpSysListener.SkipIOCPCallbackOnSuccess)
+ {
+ // IO operation completed synchronously - callback won't be called to signal completion.
+ IOCompleted(this, statusCode, bytesTransferred);
+ }
+ }
+ while (retry);
+ return statusCode;
+ }
+
+ internal void AllocateNativeRequest(uint? size = null, ulong requestId = 0)
+ {
+ _nativeRequestContext?.ReleasePins();
+ _nativeRequestContext?.Dispose();
+ //Debug.Assert(size != 0, "unexpected size");
+
+ // We can't reuse overlapped objects
+ uint newSize = size.HasValue ? size.Value : DefaultBufferSize;
+ var backingBuffer = new byte[newSize + AlignmentPadding];
+
+ var boundHandle = Server.RequestQueue.BoundHandle;
+ var nativeOverlapped = new SafeNativeOverlapped(boundHandle,
+ boundHandle.AllocateNativeOverlapped(IOCallback, this, backingBuffer));
+
+ var requestAddress = Marshal.UnsafeAddrOfPinnedArrayElement(backingBuffer, 0);
+
+ // TODO:
+ // Apparently the HttpReceiveHttpRequest memory alignment requirements for non - ARM processors
+ // are different than for ARM processors. We have seen 4 - byte - aligned buffers allocated on
+ // virtual x64/x86 machines which were accepted by HttpReceiveHttpRequest without errors. In
+ // these cases the buffer alignment may cause reading values at invalid offset. Setting buffer
+ // alignment to 0 for now.
+ //
+ // _bufferAlignment = (int)(requestAddress.ToInt64() & 0x07);
+
+ var bufferAlignment = 0;
+
+ var nativeRequest = (HttpApiTypes.HTTP_REQUEST*)(requestAddress + bufferAlignment);
+ // nativeRequest
+ _nativeRequestContext = new NativeRequestContext(nativeOverlapped, bufferAlignment, nativeRequest, backingBuffer, requestId);
+ }
+
+ public object AsyncState
+ {
+ get { return _tcs.Task.AsyncState; }
+ }
+
+ public WaitHandle AsyncWaitHandle
+ {
+ get { return ((IAsyncResult)_tcs.Task).AsyncWaitHandle; }
+ }
+
+ public bool CompletedSynchronously
+ {
+ get { return ((IAsyncResult)_tcs.Task).CompletedSynchronously; }
+ }
+
+ public bool IsCompleted
+ {
+ get { return _tcs.Task.IsCompleted; }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if (_nativeRequestContext != null)
+ {
+ _nativeRequestContext.ReleasePins();
+ _nativeRequestContext.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AuthenticationHandler.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AuthenticationHandler.cs
new file mode 100644
index 0000000000..f8edd9ccd7
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AuthenticationHandler.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.
+
+using System;
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal class AuthenticationHandler : IAuthenticationHandler
+ {
+ private RequestContext _requestContext;
+ private AuthenticationScheme _scheme;
+
+ public Task<AuthenticateResult> AuthenticateAsync()
+ {
+ var identity = _requestContext.User?.Identity;
+ if (identity != null && identity.IsAuthenticated)
+ {
+ return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(_requestContext.User, properties: null, authenticationScheme: _scheme.Name)));
+ }
+ return Task.FromResult(AuthenticateResult.NoResult());
+ }
+
+ public Task ChallengeAsync(AuthenticationProperties properties)
+ {
+ _requestContext.Response.StatusCode = 401;
+ return Task.CompletedTask;
+ }
+
+ public Task ForbidAsync(AuthenticationProperties properties)
+ {
+ _requestContext.Response.StatusCode = 403;
+ return Task.CompletedTask;
+ }
+
+ public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
+ {
+ _scheme = scheme;
+ _requestContext = context.Features.Get<RequestContext>();
+
+ if (_requestContext == null)
+ {
+ throw new InvalidOperationException("No RequestContext found.");
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AuthenticationManager.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AuthenticationManager.cs
new file mode 100644
index 0000000000..b9594d1143
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AuthenticationManager.cs
@@ -0,0 +1,137 @@
+// 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.Linq;
+using System.Runtime.InteropServices;
+using System.Security.Claims;
+using System.Security.Principal;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ // See the native HTTP_SERVER_AUTHENTICATION_INFO structure documentation for additional information.
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa364638(v=vs.85).aspx
+
+ /// <summary>
+ /// Exposes the Http.Sys authentication configurations.
+ /// </summary>
+ public sealed class AuthenticationManager
+ {
+ private static readonly int AuthInfoSize =
+ Marshal.SizeOf<HttpApiTypes.HTTP_SERVER_AUTHENTICATION_INFO>();
+
+ private UrlGroup _urlGroup;
+ private AuthenticationSchemes _authSchemes;
+ private bool _allowAnonymous = true;
+
+ internal AuthenticationManager()
+ {
+ }
+
+ public AuthenticationSchemes Schemes
+ {
+ get { return _authSchemes; }
+ set
+ {
+ _authSchemes = value;
+ SetUrlGroupSecurity();
+ }
+ }
+
+ public bool AllowAnonymous
+ {
+ get { return _allowAnonymous; }
+ set { _allowAnonymous = value; }
+ }
+
+ internal void SetUrlGroupSecurity(UrlGroup urlGroup)
+ {
+ Debug.Assert(_urlGroup == null, "SetUrlGroupSecurity called more than once.");
+ _urlGroup = urlGroup;
+ SetUrlGroupSecurity();
+ }
+
+ private unsafe void SetUrlGroupSecurity()
+ {
+ if (_urlGroup == null)
+ {
+ // Not started yet.
+ return;
+ }
+
+ HttpApiTypes.HTTP_SERVER_AUTHENTICATION_INFO authInfo =
+ new HttpApiTypes.HTTP_SERVER_AUTHENTICATION_INFO();
+
+ authInfo.Flags = HttpApiTypes.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT;
+ var authSchemes = (HttpApiTypes.HTTP_AUTH_TYPES)_authSchemes;
+ if (authSchemes != HttpApiTypes.HTTP_AUTH_TYPES.NONE)
+ {
+ authInfo.AuthSchemes = authSchemes;
+
+ // TODO:
+ // NTLM auth sharing (on by default?) DisableNTLMCredentialCaching
+ // Kerberos auth sharing (off by default?) HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING
+ // Mutual Auth - ReceiveMutualAuth
+ // Digest domain and realm - HTTP_SERVER_AUTHENTICATION_DIGEST_PARAMS
+ // Basic realm - HTTP_SERVER_AUTHENTICATION_BASIC_PARAMS
+
+ IntPtr infoptr = new IntPtr(&authInfo);
+
+ _urlGroup.SetProperty(
+ HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerAuthenticationProperty,
+ infoptr, (uint)AuthInfoSize);
+ }
+ }
+
+ internal static IList<string> GenerateChallenges(AuthenticationSchemes authSchemes)
+ {
+ IList<string> challenges = new List<string>();
+
+ if (authSchemes == AuthenticationSchemes.None)
+ {
+ return challenges;
+ }
+
+ // Order by strength.
+ if ((authSchemes & AuthenticationSchemes.Kerberos) == AuthenticationSchemes.Kerberos)
+ {
+ challenges.Add("Kerberos");
+ }
+ if ((authSchemes & AuthenticationSchemes.Negotiate) == AuthenticationSchemes.Negotiate)
+ {
+ challenges.Add("Negotiate");
+ }
+ if ((authSchemes & AuthenticationSchemes.NTLM) == AuthenticationSchemes.NTLM)
+ {
+ challenges.Add("NTLM");
+ }
+ /*if ((_authSchemes & AuthenticationSchemes.Digest) == AuthenticationSchemes.Digest)
+ {
+ // TODO:
+ throw new NotImplementedException("Digest challenge generation has not been implemented.");
+ // challenges.Add("Digest");
+ }*/
+ if ((authSchemes & AuthenticationSchemes.Basic) == AuthenticationSchemes.Basic)
+ {
+ // TODO: Realm
+ challenges.Add("Basic");
+ }
+ return challenges;
+ }
+
+ internal void SetAuthenticationChallenge(RequestContext context)
+ {
+ IList<string> challenges = GenerateChallenges(context.Response.AuthenticationChallenges);
+
+ if (challenges.Count > 0)
+ {
+ context.Response.Headers[HttpKnownHeaderNames.WWWAuthenticate]
+ = StringValues.Concat(context.Response.Headers[HttpKnownHeaderNames.WWWAuthenticate], challenges.ToArray());
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AuthenticationSchemes.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AuthenticationSchemes.cs
new file mode 100644
index 0000000000..3d82bdadc0
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/AuthenticationSchemes.cs
@@ -0,0 +1,19 @@
+// 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.Server.HttpSys
+{
+ // REVIEW: this appears to be very similar to System.Net.AuthenticationSchemes
+ [Flags]
+ public enum AuthenticationSchemes
+ {
+ None = 0x0,
+ Basic = 0x1,
+ // Digest = 0x2, // TODO: Verify this is no longer supported by Http.Sys
+ NTLM = 0x4,
+ Negotiate = 0x8,
+ Kerberos = 0x10
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/FeatureContext.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/FeatureContext.cs
new file mode 100644
index 0000000000..9acef96deb
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/FeatureContext.cs
@@ -0,0 +1,594 @@
+// 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.Globalization;
+using System.IO;
+using System.Net;
+using System.Security.Claims;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Http.Features.Authentication;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal class FeatureContext :
+ IHttpRequestFeature,
+ IHttpConnectionFeature,
+ IHttpResponseFeature,
+ IHttpSendFileFeature,
+ ITlsConnectionFeature,
+ // ITlsTokenBindingFeature, TODO: https://github.com/aspnet/HttpSysServer/issues/231
+ IHttpBufferingFeature,
+ IHttpRequestLifetimeFeature,
+ IHttpAuthenticationFeature,
+ IHttpUpgradeFeature,
+ IHttpRequestIdentifierFeature,
+ IHttpMaxRequestBodySizeFeature,
+ IHttpBodyControlFeature
+ {
+ private RequestContext _requestContext;
+ private IFeatureCollection _features;
+ private bool _enableResponseCaching;
+
+ private Stream _requestBody;
+ private IHeaderDictionary _requestHeaders;
+ private string _scheme;
+ private string _httpMethod;
+ private string _httpProtocolVersion;
+ private string _query;
+ private string _pathBase;
+ private string _path;
+ private string _rawTarget;
+ private IPAddress _remoteIpAddress;
+ private IPAddress _localIpAddress;
+ private int _remotePort;
+ private int _localPort;
+ private string _connectionId;
+ private string _traceIdentitfier;
+ private X509Certificate2 _clientCert;
+ private ClaimsPrincipal _user;
+ private CancellationToken _disconnectToken;
+ private Stream _responseStream;
+ private IHeaderDictionary _responseHeaders;
+
+ private Fields _initializedFields;
+
+ private List<Tuple<Func<object, Task>, object>> _onStartingActions = new List<Tuple<Func<object, Task>, object>>();
+ private List<Tuple<Func<object, Task>, object>> _onCompletedActions = new List<Tuple<Func<object, Task>, object>>();
+ private bool _responseStarted;
+ private bool _completed;
+
+ internal FeatureContext(RequestContext requestContext)
+ {
+ _requestContext = requestContext;
+ _features = new FeatureCollection(new StandardFeatureCollection(this));
+ _enableResponseCaching = _requestContext.Server.Options.EnableResponseCaching;
+
+ // Pre-initialize any fields that are not lazy at the lower level.
+ _requestHeaders = Request.Headers;
+ _httpMethod = Request.Method;
+ _path = Request.Path;
+ _pathBase = Request.PathBase;
+ _query = Request.QueryString;
+ _rawTarget = Request.RawUrl;
+ _scheme = Request.Scheme;
+ _user = _requestContext.User;
+
+ _responseStream = new ResponseStream(requestContext.Response.Body, OnResponseStart);
+ _responseHeaders = Response.Headers;
+ }
+
+ internal IFeatureCollection Features => _features;
+
+ internal object RequestContext => _requestContext;
+
+ private Request Request => _requestContext.Request;
+
+ private Response Response => _requestContext.Response;
+
+ [Flags]
+ // Fields that may be lazy-initialized
+ private enum Fields
+ {
+ None = 0x0,
+ Protocol = 0x1,
+ RequestBody = 0x2,
+ RequestAborted = 0x4,
+ LocalIpAddress = 0x8,
+ RemoteIpAddress = 0x10,
+ LocalPort = 0x20,
+ RemotePort = 0x40,
+ ConnectionId = 0x80,
+ ClientCertificate = 0x100,
+ TraceIdentifier = 0x200,
+ }
+
+ private bool IsNotInitialized(Fields field)
+ {
+ return (_initializedFields & field) != field;
+ }
+
+ private void SetInitialized(Fields field)
+ {
+ _initializedFields |= field;
+ }
+
+ Stream IHttpRequestFeature.Body
+ {
+ get
+ {
+ if (IsNotInitialized(Fields.RequestBody))
+ {
+ _requestBody = Request.Body;
+ SetInitialized(Fields.RequestBody);
+ }
+ return _requestBody;
+ }
+ set
+ {
+ _requestBody = value;
+ SetInitialized(Fields.RequestBody);
+ }
+ }
+
+ IHeaderDictionary IHttpRequestFeature.Headers
+ {
+ get { return _requestHeaders; }
+ set { _requestHeaders = value; }
+ }
+
+ string IHttpRequestFeature.Method
+ {
+ get { return _httpMethod; }
+ set { _httpMethod = value; }
+ }
+
+ string IHttpRequestFeature.Path
+ {
+ get { return _path; }
+ set { _path = value; }
+ }
+
+ string IHttpRequestFeature.PathBase
+ {
+ get { return _pathBase; }
+ set { _pathBase = value; }
+ }
+
+ string IHttpRequestFeature.Protocol
+ {
+ get
+ {
+ if (IsNotInitialized(Fields.Protocol))
+ {
+ var protocol = Request.ProtocolVersion;
+ if (protocol.Major == 1 && protocol.Minor == 1)
+ {
+ _httpProtocolVersion = "HTTP/1.1";
+ }
+ else if (protocol.Major == 1 && protocol.Minor == 0)
+ {
+ _httpProtocolVersion = "HTTP/1.0";
+ }
+ else
+ {
+ _httpProtocolVersion = "HTTP/" + protocol.ToString(2);
+ }
+ SetInitialized(Fields.Protocol);
+ }
+ return _httpProtocolVersion;
+ }
+ set
+ {
+ _httpProtocolVersion = value;
+ SetInitialized(Fields.Protocol);
+ }
+ }
+
+ string IHttpRequestFeature.QueryString
+ {
+ get { return _query; }
+ set { _query = value; }
+ }
+
+ string IHttpRequestFeature.RawTarget
+ {
+ get { return _rawTarget; }
+ set { _rawTarget = value; }
+ }
+
+ string IHttpRequestFeature.Scheme
+ {
+ get { return _scheme; }
+ set { _scheme = value; }
+ }
+
+ IPAddress IHttpConnectionFeature.LocalIpAddress
+ {
+ get
+ {
+ if (IsNotInitialized(Fields.LocalIpAddress))
+ {
+ _localIpAddress = Request.LocalIpAddress;
+ SetInitialized(Fields.LocalIpAddress);
+ }
+ return _localIpAddress;
+ }
+ set
+ {
+ _localIpAddress = value;
+ SetInitialized(Fields.LocalIpAddress);
+ }
+ }
+
+ IPAddress IHttpConnectionFeature.RemoteIpAddress
+ {
+ get
+ {
+ if (IsNotInitialized(Fields.RemoteIpAddress))
+ {
+ _remoteIpAddress = Request.RemoteIpAddress;
+ SetInitialized(Fields.RemoteIpAddress);
+ }
+ return _remoteIpAddress;
+ }
+ set
+ {
+ _remoteIpAddress = value;
+ SetInitialized(Fields.RemoteIpAddress);
+ }
+ }
+
+ int IHttpConnectionFeature.LocalPort
+ {
+ get
+ {
+ if (IsNotInitialized(Fields.LocalPort))
+ {
+ _localPort = Request.LocalPort;
+ SetInitialized(Fields.LocalPort);
+ }
+ return _localPort;
+ }
+ set
+ {
+ _localPort = value;
+ SetInitialized(Fields.LocalPort);
+ }
+ }
+
+ int IHttpConnectionFeature.RemotePort
+ {
+ get
+ {
+ if (IsNotInitialized(Fields.RemotePort))
+ {
+ _remotePort = Request.RemotePort;
+ SetInitialized(Fields.RemotePort);
+ }
+ return _remotePort;
+ }
+ set
+ {
+ _remotePort = value;
+ SetInitialized(Fields.RemotePort);
+ }
+ }
+
+ string IHttpConnectionFeature.ConnectionId
+ {
+ get
+ {
+ if (IsNotInitialized(Fields.ConnectionId))
+ {
+ _connectionId = Request.ConnectionId.ToString(CultureInfo.InvariantCulture);
+ SetInitialized(Fields.ConnectionId);
+ }
+ return _connectionId;
+ }
+ set
+ {
+ _connectionId = value;
+ SetInitialized(Fields.ConnectionId);
+ }
+ }
+
+ X509Certificate2 ITlsConnectionFeature.ClientCertificate
+ {
+ get
+ {
+ if (IsNotInitialized(Fields.ClientCertificate))
+ {
+ _clientCert = Request.GetClientCertificateAsync().Result; // TODO: Sync;
+ SetInitialized(Fields.ClientCertificate);
+ }
+ return _clientCert;
+ }
+ set
+ {
+ _clientCert = value;
+ SetInitialized(Fields.ClientCertificate);
+ }
+ }
+
+ async Task<X509Certificate2> ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken)
+ {
+ if (IsNotInitialized(Fields.ClientCertificate))
+ {
+ _clientCert = await Request.GetClientCertificateAsync(cancellationToken);
+ SetInitialized(Fields.ClientCertificate);
+ }
+ return _clientCert;
+ }
+
+ internal ITlsConnectionFeature GetTlsConnectionFeature()
+ {
+ return Request.IsHttps ? this : null;
+ }
+ /* TODO: https://github.com/aspnet/HttpSysServer/issues/231
+ byte[] ITlsTokenBindingFeature.GetProvidedTokenBindingId() => Request.GetProvidedTokenBindingId();
+
+ byte[] ITlsTokenBindingFeature.GetReferredTokenBindingId() => Request.GetReferredTokenBindingId();
+
+ internal ITlsTokenBindingFeature GetTlsTokenBindingFeature()
+ {
+ return Request.IsHttps ? this : null;
+ }
+ */
+ void IHttpBufferingFeature.DisableRequestBuffering()
+ {
+ // There is no request buffering.
+ }
+
+ void IHttpBufferingFeature.DisableResponseBuffering()
+ {
+ // TODO: What about native buffering?
+ }
+
+ Stream IHttpResponseFeature.Body
+ {
+ get { return _responseStream; }
+ set { _responseStream = value; }
+ }
+
+ IHeaderDictionary IHttpResponseFeature.Headers
+ {
+ get { return _responseHeaders; }
+ set { _responseHeaders = value; }
+ }
+
+ bool IHttpResponseFeature.HasStarted => Response.HasStarted;
+
+ void IHttpResponseFeature.OnStarting(Func<object, Task> callback, object state)
+ {
+ if (callback == null)
+ {
+ throw new ArgumentNullException(nameof(callback));
+ }
+ if (_onStartingActions == null)
+ {
+ throw new InvalidOperationException("Cannot register new callbacks, the response has already started.");
+ }
+
+ _onStartingActions.Add(new Tuple<Func<object, Task>, object>(callback, state));
+ }
+
+ void IHttpResponseFeature.OnCompleted(Func<object, Task> callback, object state)
+ {
+ if (callback == null)
+ {
+ throw new ArgumentNullException(nameof(callback));
+ }
+ if (_onCompletedActions == null)
+ {
+ throw new InvalidOperationException("Cannot register new callbacks, the response has already completed.");
+ }
+
+ _onCompletedActions.Add(new Tuple<Func<object, Task>, object>(callback, state));
+ }
+
+ string IHttpResponseFeature.ReasonPhrase
+ {
+ get { return Response.ReasonPhrase; }
+ set { Response.ReasonPhrase = value; }
+ }
+
+ int IHttpResponseFeature.StatusCode
+ {
+ get { return Response.StatusCode; }
+ set { Response.StatusCode = value; }
+ }
+
+ async Task IHttpSendFileFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
+ {
+ await OnResponseStart();
+ await Response.SendFileAsync(path, offset, length, cancellation);
+ }
+
+ CancellationToken IHttpRequestLifetimeFeature.RequestAborted
+ {
+ get
+ {
+ if (IsNotInitialized(Fields.RequestAborted))
+ {
+ _disconnectToken = _requestContext.DisconnectToken;
+ SetInitialized(Fields.RequestAborted);
+ }
+ return _disconnectToken;
+ }
+ set
+ {
+ _disconnectToken = value;
+ SetInitialized(Fields.RequestAborted);
+ }
+ }
+
+ void IHttpRequestLifetimeFeature.Abort() => _requestContext.Abort();
+
+ bool IHttpUpgradeFeature.IsUpgradableRequest => _requestContext.IsUpgradableRequest;
+
+ async Task<Stream> IHttpUpgradeFeature.UpgradeAsync()
+ {
+ await OnResponseStart();
+ return await _requestContext.UpgradeAsync();
+ }
+
+ ClaimsPrincipal IHttpAuthenticationFeature.User
+ {
+ get { return _user; }
+ set { _user = value; }
+ }
+
+ IAuthenticationHandler IHttpAuthenticationFeature.Handler { get; set; }
+
+ string IHttpRequestIdentifierFeature.TraceIdentifier
+ {
+ get
+ {
+ if (IsNotInitialized(Fields.TraceIdentifier))
+ {
+ _traceIdentitfier = _requestContext.TraceIdentifier.ToString();
+ SetInitialized(Fields.TraceIdentifier);
+ }
+ return _traceIdentitfier;
+ }
+ set
+ {
+ _traceIdentitfier = value;
+ SetInitialized(Fields.TraceIdentifier);
+ }
+ }
+
+ bool IHttpBodyControlFeature.AllowSynchronousIO
+ {
+ get => _requestContext.AllowSynchronousIO;
+ set => _requestContext.AllowSynchronousIO = value;
+ }
+
+ bool IHttpMaxRequestBodySizeFeature.IsReadOnly => Request.HasRequestBodyStarted;
+
+ long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize
+ {
+ get => Request.MaxRequestBodySize;
+ set => Request.MaxRequestBodySize = value;
+ }
+
+ internal async Task OnResponseStart()
+ {
+ if (_responseStarted)
+ {
+ return;
+ }
+ _responseStarted = true;
+ await NotifiyOnStartingAsync();
+ ConsiderEnablingResponseCache();
+ }
+
+ private async Task NotifiyOnStartingAsync()
+ {
+ var actions = _onStartingActions;
+ _onStartingActions = null;
+ if (actions == null)
+ {
+ return;
+ }
+
+ actions.Reverse();
+ // Execute last to first. This mimics a stack unwind.
+ foreach (var actionPair in actions)
+ {
+ await actionPair.Item1(actionPair.Item2);
+ }
+ }
+
+ private void ConsiderEnablingResponseCache()
+ {
+ if (_enableResponseCaching)
+ {
+ // We don't have to worry too much about what Http.Sys supports, caching is a best-effort feature.
+ // If there's something about the request or response that prevents it from caching then the response
+ // will complete normally without caching.
+ _requestContext.Response.CacheTtl = GetCacheTtl(_requestContext);
+ }
+ }
+
+ private static TimeSpan? GetCacheTtl(RequestContext requestContext)
+ {
+ var response = requestContext.Response;
+ // Only consider kernel-mode caching if the Cache-Control response header is present.
+ var cacheControlHeader = response.Headers[HeaderNames.CacheControl];
+ if (string.IsNullOrEmpty(cacheControlHeader))
+ {
+ return null;
+ }
+
+ // Before we check the header value, check for the existence of other headers which would
+ // make us *not* want to cache the response.
+ if (response.Headers.ContainsKey(HeaderNames.SetCookie)
+ || response.Headers.ContainsKey(HeaderNames.Vary)
+ || response.Headers.ContainsKey(HeaderNames.Pragma))
+ {
+ return null;
+ }
+
+ // We require 'public' and 's-max-age' or 'max-age' or the Expires header.
+ CacheControlHeaderValue cacheControl;
+ if (CacheControlHeaderValue.TryParse(cacheControlHeader.ToString(), out cacheControl) && cacheControl.Public)
+ {
+ if (cacheControl.SharedMaxAge.HasValue)
+ {
+ return cacheControl.SharedMaxAge;
+ }
+ else if (cacheControl.MaxAge.HasValue)
+ {
+ return cacheControl.MaxAge;
+ }
+
+ DateTimeOffset expirationDate;
+ if (HeaderUtilities.TryParseDate(response.Headers[HeaderNames.Expires].ToString(), out expirationDate))
+ {
+ var expiresOffset = expirationDate - DateTimeOffset.UtcNow;
+ if (expiresOffset > TimeSpan.Zero)
+ {
+ return expiresOffset;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ internal Task OnCompleted()
+ {
+ if (_completed)
+ {
+ return Task.CompletedTask;
+ }
+ _completed = true;
+ return NotifyOnCompletedAsync();
+ }
+
+ private async Task NotifyOnCompletedAsync()
+ {
+ var actions = _onCompletedActions;
+ _onCompletedActions = null;
+ if (actions == null)
+ {
+ return;
+ }
+
+ actions.Reverse();
+ // Execute last to first. This mimics a stack unwind.
+ foreach (var actionPair in actions)
+ {
+ await actionPair.Item1(actionPair.Item2);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Helpers.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Helpers.cs
new file mode 100644
index 0000000000..5376203319
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Helpers.cs
@@ -0,0 +1,126 @@
+// 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.Runtime.CompilerServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal static class Helpers
+ {
+ internal static readonly byte[] ChunkTerminator = new byte[] { (byte)'0', (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };
+ internal static readonly byte[] CRLF = new byte[] { (byte)'\r', (byte)'\n' };
+
+ internal static ConfiguredTaskAwaitable SupressContext(this Task task)
+ {
+ return task.ConfigureAwait(continueOnCapturedContext: false);
+ }
+
+ internal static ConfiguredTaskAwaitable<T> SupressContext<T>(this Task<T> task)
+ {
+ return task.ConfigureAwait(continueOnCapturedContext: false);
+ }
+
+ internal static IAsyncResult ToIAsyncResult(this Task task, AsyncCallback callback, object state)
+ {
+ var tcs = new TaskCompletionSource<int>(state);
+ task.ContinueWith(t =>
+ {
+ if (t.IsFaulted)
+ {
+ tcs.TrySetException(t.Exception.InnerExceptions);
+ }
+ else if (t.IsCanceled)
+ {
+ tcs.TrySetCanceled();
+ }
+ else
+ {
+ tcs.TrySetResult(0);
+ }
+
+ if (callback != null)
+ {
+ callback(tcs.Task);
+ }
+ }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
+ return tcs.Task;
+ }
+
+ internal static ArraySegment<byte> GetChunkHeader(long size)
+ {
+ if (size < int.MaxValue)
+ {
+ return GetChunkHeader((int)size);
+ }
+
+ // Greater than 2gb, perf is no longer our concern
+ return new ArraySegment<byte>(Encoding.ASCII.GetBytes(size.ToString("X") + "\r\n"));
+ }
+
+ /// <summary>
+ /// A private utility routine to convert an integer to a chunk header,
+ /// which is an ASCII hex number followed by a CRLF.The header is returned
+ /// as a byte array.
+ /// Generates a right-aligned hex string and returns the start offset.
+ /// </summary>
+ /// <param name="size">Chunk size to be encoded</param>
+ /// <returns>A byte array with the header in int.</returns>
+ internal static ArraySegment<byte> GetChunkHeader(int size)
+ {
+ uint mask = 0xf0000000;
+ byte[] header = new byte[10];
+ int i;
+ int offset = -1;
+
+ // Loop through the size, looking at each nibble. If it's not 0
+ // convert it to hex. Save the index of the first non-zero
+ // byte.
+
+ for (i = 0; i < 8; i++, size <<= 4)
+ {
+ // offset == -1 means that we haven't found a non-zero nibble
+ // yet. If we haven't found one, and the current one is zero,
+ // don't do anything.
+
+ if (offset == -1)
+ {
+ if ((size & mask) == 0)
+ {
+ continue;
+ }
+ }
+
+ // Either we have a non-zero nibble or we're no longer skipping
+ // leading zeros. Convert this nibble to ASCII and save it.
+
+ uint temp = (uint)size >> 28;
+
+ if (temp < 10)
+ {
+ header[i] = (byte)(temp + '0');
+ }
+ else
+ {
+ header[i] = (byte)((temp - 10) + 'A');
+ }
+
+ // If we haven't found a non-zero nibble yet, we've found one
+ // now, so remember that.
+
+ if (offset == -1)
+ {
+ offset = i;
+ }
+ }
+
+ header[8] = (byte)'\r';
+ header[9] = (byte)'\n';
+
+ return new ArraySegment<byte>(header, offset, header.Length - offset);
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Http503VerbosityLevel .cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Http503VerbosityLevel .cs
new file mode 100644
index 0000000000..09da208c09
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Http503VerbosityLevel .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.
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ /// <summary>
+ /// Enum declaring the allowed values for the verbosity level when http.sys reject requests due to throttling.
+ /// </summary>
+ public enum Http503VerbosityLevel : long
+ {
+ /// <summary>
+ /// A 503 response is not sent; the connection is reset. This is the default HTTP Server API behavior.
+ /// </summary>
+ Basic = 0,
+
+ /// <summary>
+ /// The HTTP Server API sends a 503 response with a "Service Unavailable" reason phrase.
+ /// </summary>
+ Limited = 1,
+
+ /// <summary>
+ /// The HTTP Server API sends a 503 response with a detailed reason phrase.
+ /// </summary>
+ Full = 2
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysDefaults.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysDefaults.cs
new file mode 100644
index 0000000000..ea22e86d8e
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysDefaults.cs
@@ -0,0 +1,13 @@
+// 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.Server.HttpSys
+{
+ public static class HttpSysDefaults
+ {
+ /// <summary>
+ /// The name of the authentication scheme used.
+ /// </summary>
+ public static readonly string AuthenticationScheme = "Windows";
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysException.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysException.cs
new file mode 100644
index 0000000000..9e65fa3916
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysException.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 System;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ [SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable")]
+ public class HttpSysException : Win32Exception
+ {
+ internal HttpSysException()
+ : base(Marshal.GetLastWin32Error())
+ {
+ }
+
+ internal HttpSysException(int errorCode)
+ : base(errorCode)
+ {
+ }
+
+ internal HttpSysException(int errorCode, string message)
+ : base(errorCode, message)
+ {
+ }
+
+ // the base class returns the HResult with this property
+ // we need the Win32 Error Code, hence the override.
+ public override int ErrorCode
+ {
+ get
+ {
+ return NativeErrorCode;
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysListener.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysListener.cs
new file mode 100644
index 0000000000..a1ad7493d8
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysListener.cs
@@ -0,0 +1,435 @@
+// 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.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ /// <summary>
+ /// An HTTP server wrapping the Http.Sys APIs that accepts requests.
+ /// </summary>
+ internal class HttpSysListener : IDisposable
+ {
+ // Win8# 559317 fixed a bug in Http.sys's HttpReceiveClientCertificate method.
+ // Without this fix IOCP callbacks were not being called although ERROR_IO_PENDING was
+ // returned from HttpReceiveClientCertificate when using the
+ // FileCompletionNotificationModes.SkipCompletionPortOnSuccess flag.
+ // This bug was only hit when the buffer passed into HttpReceiveClientCertificate
+ // (1500 bytes initially) is too small for the certificate.
+ // Due to this bug in downlevel operating systems the FileCompletionNotificationModes.SkipCompletionPortOnSuccess
+ // flag is only used on Win8 and later.
+ internal static readonly bool SkipIOCPCallbackOnSuccess = ComNetOS.IsWin8orLater;
+
+ // Mitigate potential DOS attacks by limiting the number of unknown headers we accept. Numerous header names
+ // with hash collisions will cause the server to consume excess CPU. 1000 headers limits CPU time to under
+ // 0.5 seconds per request. Respond with a 400 Bad Request.
+ private const int UnknownHeaderLimit = 1000;
+
+ private volatile State _state; // m_State is set only within lock blocks, but often read outside locks.
+
+ private ServerSession _serverSession;
+ private UrlGroup _urlGroup;
+ private RequestQueue _requestQueue;
+ private DisconnectListener _disconnectListener;
+
+ private object _internalLock;
+
+ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ if (!HttpApi.Supported)
+ {
+ throw new PlatformNotSupportedException();
+ }
+
+ Debug.Assert(HttpApi.ApiVersion == HttpApiTypes.HTTP_API_VERSION.Version20, "Invalid Http api version");
+
+ Options = options;
+
+ Logger = LogHelper.CreateLogger(loggerFactory, typeof(HttpSysListener));
+
+ _state = State.Stopped;
+ _internalLock = new object();
+
+ // V2 initialization sequence:
+ // 1. Create server session
+ // 2. Create url group
+ // 3. Create request queue - Done in Start()
+ // 4. Add urls to url group - Done in Start()
+ // 5. Attach request queue to url group - Done in Start()
+
+ try
+ {
+ _serverSession = new ServerSession();
+
+ _urlGroup = new UrlGroup(_serverSession, Logger);
+
+ _requestQueue = new RequestQueue(_urlGroup, Logger);
+
+ _disconnectListener = new DisconnectListener(_requestQueue, Logger);
+ }
+ catch (Exception exception)
+ {
+ // If Url group or request queue creation failed, close server session before throwing.
+ _requestQueue?.Dispose();
+ _urlGroup?.Dispose();
+ _serverSession?.Dispose();
+ LogHelper.LogException(Logger, ".Ctor", exception);
+ throw;
+ }
+ }
+
+ internal enum State
+ {
+ Stopped,
+ Started,
+ Disposed,
+ }
+
+ internal ILogger Logger { get; private set; }
+
+ internal UrlGroup UrlGroup
+ {
+ get { return _urlGroup; }
+ }
+
+ internal RequestQueue RequestQueue
+ {
+ get { return _requestQueue; }
+ }
+
+ internal DisconnectListener DisconnectListener
+ {
+ get { return _disconnectListener; }
+ }
+
+ public HttpSysOptions Options { get; }
+
+ public bool IsListening
+ {
+ get { return _state == State.Started; }
+ }
+
+ /// <summary>
+ /// Start accepting incoming requests.
+ /// </summary>
+ public void Start()
+ {
+ CheckDisposed();
+
+ LogHelper.LogInfo(Logger, "Start");
+
+ // Make sure there are no race conditions between Start/Stop/Abort/Close/Dispose.
+ // Start needs to setup all resources. Abort/Stop must not interfere while Start is
+ // allocating those resources.
+ lock (_internalLock)
+ {
+ try
+ {
+ CheckDisposed();
+ if (_state == State.Started)
+ {
+ return;
+ }
+
+ Options.Apply(UrlGroup, RequestQueue);
+
+ _requestQueue.AttachToUrlGroup();
+
+ // All resources are set up correctly. Now add all prefixes.
+ try
+ {
+ Options.UrlPrefixes.RegisterAllPrefixes(UrlGroup);
+ }
+ catch (HttpSysException)
+ {
+ // If an error occurred while adding prefixes, free all resources allocated by previous steps.
+ _requestQueue.DetachFromUrlGroup();
+ throw;
+ }
+
+ _state = State.Started;
+ }
+ catch (Exception exception)
+ {
+ // Make sure the HttpListener instance can't be used if Start() failed.
+ _state = State.Disposed;
+ DisposeInternal();
+ LogHelper.LogException(Logger, "Start", exception);
+ throw;
+ }
+ }
+ }
+
+ private void Stop()
+ {
+ try
+ {
+ lock (_internalLock)
+ {
+ CheckDisposed();
+ if (_state == State.Stopped)
+ {
+ return;
+ }
+
+ Options.UrlPrefixes.UnregisterAllPrefixes();
+
+ _state = State.Stopped;
+
+ _requestQueue.DetachFromUrlGroup();
+ }
+ }
+ catch (Exception exception)
+ {
+ LogHelper.LogException(Logger, "Stop", exception);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Stop the server and clean up.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!disposing)
+ {
+ return;
+ }
+
+ lock (_internalLock)
+ {
+ try
+ {
+ if (_state == State.Disposed)
+ {
+ return;
+ }
+ LogHelper.LogInfo(Logger, "Dispose");
+
+ Stop();
+ DisposeInternal();
+ }
+ catch (Exception exception)
+ {
+ LogHelper.LogException(Logger, "Dispose", exception);
+ throw;
+ }
+ finally
+ {
+ _state = State.Disposed;
+ }
+ }
+ }
+
+ private void DisposeInternal()
+ {
+ // V2 stopping sequence:
+ // 1. Detach request queue from url group - Done in Stop()/Abort()
+ // 2. Remove urls from url group - Done in Stop()
+ // 3. Close request queue - Done in Stop()/Abort()
+ // 4. Close Url group.
+ // 5. Close server session.
+
+ _requestQueue.Dispose();
+
+ _urlGroup.Dispose();
+
+ Debug.Assert(_serverSession != null, "ServerSessionHandle is null in CloseV2Config");
+ Debug.Assert(!_serverSession.Id.IsInvalid, "ServerSessionHandle is invalid in CloseV2Config");
+
+ _serverSession.Dispose();
+ }
+
+ /// <summary>
+ /// Accept a request from the incoming request queue.
+ /// </summary>
+ public Task<RequestContext> AcceptAsync()
+ {
+ AsyncAcceptContext asyncResult = null;
+ try
+ {
+ CheckDisposed();
+ Debug.Assert(_state != State.Stopped, "Listener has been stopped.");
+ // prepare the ListenerAsyncResult object (this will have it's own
+ // event that the user can wait on for IO completion - which means we
+ // need to signal it when IO completes)
+ asyncResult = new AsyncAcceptContext(this);
+ uint statusCode = asyncResult.QueueBeginGetContext();
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
+ statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
+ {
+ // some other bad error, possible(?) return values are:
+ // ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
+ asyncResult.Dispose();
+ throw new HttpSysException((int)statusCode);
+ }
+ }
+ catch (Exception exception)
+ {
+ LogHelper.LogException(Logger, "GetContextAsync", exception);
+ throw;
+ }
+
+ return asyncResult.Task;
+ }
+
+ internal unsafe bool ValidateRequest(NativeRequestContext requestMemory)
+ {
+ // Block potential DOS attacks
+ if (requestMemory.UnknownHeaderCount > UnknownHeaderLimit)
+ {
+ SendError(requestMemory.RequestId, StatusCodes.Status400BadRequest, authChallenges: null);
+ return false;
+ }
+ return true;
+ }
+
+ internal unsafe bool ValidateAuth(NativeRequestContext requestMemory)
+ {
+ if (!Options.Authentication.AllowAnonymous && !requestMemory.CheckAuthenticated())
+ {
+ SendError(requestMemory.RequestId, StatusCodes.Status401Unauthorized,
+ AuthenticationManager.GenerateChallenges(Options.Authentication.Schemes));
+ return false;
+ }
+ return true;
+ }
+
+ internal unsafe void SendError(ulong requestId, int httpStatusCode, IList<string> authChallenges = null)
+ {
+ HttpApiTypes.HTTP_RESPONSE_V2 httpResponse = new HttpApiTypes.HTTP_RESPONSE_V2();
+ httpResponse.Response_V1.Version = new HttpApiTypes.HTTP_VERSION();
+ httpResponse.Response_V1.Version.MajorVersion = (ushort)1;
+ httpResponse.Response_V1.Version.MinorVersion = (ushort)1;
+
+ List<GCHandle> pinnedHeaders = null;
+ GCHandle gcHandle;
+ try
+ {
+ // Copied from the multi-value headers section of SerializeHeaders
+ if (authChallenges != null && authChallenges.Count > 0)
+ {
+ pinnedHeaders = new List<GCHandle>();
+
+ HttpApiTypes.HTTP_RESPONSE_INFO[] knownHeaderInfo = null;
+ knownHeaderInfo = new HttpApiTypes.HTTP_RESPONSE_INFO[1];
+ gcHandle = GCHandle.Alloc(knownHeaderInfo, GCHandleType.Pinned);
+ pinnedHeaders.Add(gcHandle);
+ httpResponse.pResponseInfo = (HttpApiTypes.HTTP_RESPONSE_INFO*)gcHandle.AddrOfPinnedObject();
+
+ knownHeaderInfo[httpResponse.ResponseInfoCount].Type = HttpApiTypes.HTTP_RESPONSE_INFO_TYPE.HttpResponseInfoTypeMultipleKnownHeaders;
+ knownHeaderInfo[httpResponse.ResponseInfoCount].Length =
+ (uint)Marshal.SizeOf<HttpApiTypes.HTTP_MULTIPLE_KNOWN_HEADERS>();
+
+ HttpApiTypes.HTTP_MULTIPLE_KNOWN_HEADERS header = new HttpApiTypes.HTTP_MULTIPLE_KNOWN_HEADERS();
+
+ header.HeaderId = HttpApiTypes.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderWwwAuthenticate;
+ header.Flags = HttpApiTypes.HTTP_RESPONSE_INFO_FLAGS.PreserveOrder; // The docs say this is for www-auth only.
+
+ HttpApiTypes.HTTP_KNOWN_HEADER[] nativeHeaderValues = new HttpApiTypes.HTTP_KNOWN_HEADER[authChallenges.Count];
+ gcHandle = GCHandle.Alloc(nativeHeaderValues, GCHandleType.Pinned);
+ pinnedHeaders.Add(gcHandle);
+ header.KnownHeaders = (HttpApiTypes.HTTP_KNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
+
+ for (int headerValueIndex = 0; headerValueIndex < authChallenges.Count; headerValueIndex++)
+ {
+ // Add Value
+ string headerValue = authChallenges[headerValueIndex];
+ byte[] bytes = HeaderEncoding.GetBytes(headerValue);
+ nativeHeaderValues[header.KnownHeaderCount].RawValueLength = (ushort)bytes.Length;
+ gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
+ pinnedHeaders.Add(gcHandle);
+ nativeHeaderValues[header.KnownHeaderCount].pRawValue = (byte*)gcHandle.AddrOfPinnedObject();
+ header.KnownHeaderCount++;
+ }
+
+ // This type is a struct, not an object, so pinning it causes a boxed copy to be created. We can't do that until after all the fields are set.
+ gcHandle = GCHandle.Alloc(header, GCHandleType.Pinned);
+ pinnedHeaders.Add(gcHandle);
+ knownHeaderInfo[0].pInfo = (HttpApiTypes.HTTP_MULTIPLE_KNOWN_HEADERS*)gcHandle.AddrOfPinnedObject();
+
+ httpResponse.ResponseInfoCount = 1;
+ }
+
+ httpResponse.Response_V1.StatusCode = (ushort)httpStatusCode;
+ string statusDescription = HttpReasonPhrase.Get(httpStatusCode);
+ uint dataWritten = 0;
+ uint statusCode;
+ byte[] byteReason = HeaderEncoding.GetBytes(statusDescription);
+ fixed (byte* pReason = byteReason)
+ {
+ httpResponse.Response_V1.pReason = (byte*)pReason;
+ httpResponse.Response_V1.ReasonLength = (ushort)byteReason.Length;
+
+ byte[] byteContentLength = new byte[] { (byte)'0' };
+ fixed (byte* pContentLength = byteContentLength)
+ {
+ (&httpResponse.Response_V1.Headers.KnownHeaders)[(int)HttpSysResponseHeader.ContentLength].pRawValue = (byte*)pContentLength;
+ (&httpResponse.Response_V1.Headers.KnownHeaders)[(int)HttpSysResponseHeader.ContentLength].RawValueLength = (ushort)byteContentLength.Length;
+ httpResponse.Response_V1.Headers.UnknownHeaderCount = 0;
+
+ statusCode =
+ HttpApi.HttpSendHttpResponse(
+ _requestQueue.Handle,
+ requestId,
+ 0,
+ &httpResponse,
+ null,
+ &dataWritten,
+ IntPtr.Zero,
+ 0,
+ SafeNativeOverlapped.Zero,
+ IntPtr.Zero);
+ }
+ }
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
+ {
+ // if we fail to send a 401 something's seriously wrong, abort the request
+ HttpApi.HttpCancelHttpRequest(_requestQueue.Handle, requestId, IntPtr.Zero);
+ }
+ }
+ finally
+ {
+ if (pinnedHeaders != null)
+ {
+ foreach (GCHandle handle in pinnedHeaders)
+ {
+ if (handle.IsAllocated)
+ {
+ handle.Free();
+ }
+ }
+ }
+ }
+ }
+
+ private void CheckDisposed()
+ {
+ if (_state == State.Disposed)
+ {
+ throw new ObjectDisposedException(this.GetType().FullName);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs
new file mode 100644
index 0000000000..93d6e2dd82
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs
@@ -0,0 +1,199 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class HttpSysOptions
+ {
+ private const Http503VerbosityLevel DefaultRejectionVerbosityLevel = Http503VerbosityLevel.Basic; // Http.sys default.
+ private const long DefaultRequestQueueLength = 1000; // Http.sys default.
+ internal static readonly int DefaultMaxAccepts = 5 * Environment.ProcessorCount;
+ // Matches the default maxAllowedContentLength in IIS (~28.6 MB)
+ // https://www.iis.net/configreference/system.webserver/security/requestfiltering/requestlimits#005
+ private const long DefaultMaxRequestBodySize = 30000000;
+
+ private Http503VerbosityLevel _rejectionVebosityLevel = DefaultRejectionVerbosityLevel;
+ // The native request queue
+ private long _requestQueueLength = DefaultRequestQueueLength;
+ private long? _maxConnections;
+ private RequestQueue _requestQueue;
+ private UrlGroup _urlGroup;
+ private long? _maxRequestBodySize = DefaultMaxRequestBodySize;
+
+ public HttpSysOptions()
+ {
+ }
+
+ /// <summary>
+ /// The maximum number of concurrent accepts.
+ /// </summary>
+ public int MaxAccepts { get; set; } = DefaultMaxAccepts;
+
+ /// <summary>
+ /// Attempts kernel mode caching for responses with eligible headers. The response may not include
+ /// Set-Cookie, Vary, or Pragma headers. It must include a Cache-Control header with Public and
+ /// either a Shared-Max-Age or Max-Age value, or an Expires header.
+ /// </summary>
+ public bool EnableResponseCaching { get; set; } = true;
+
+ /// <summary>
+ /// The url prefixes to register with Http.Sys. These may be modified at any time prior to disposing
+ /// the listener.
+ /// </summary>
+ public UrlPrefixCollection UrlPrefixes { get; } = new UrlPrefixCollection();
+
+ /// <summary>
+ /// Http.Sys authentication settings. These may be modified at any time prior to disposing
+ /// the listener.
+ /// </summary>
+ public AuthenticationManager Authentication { get; } = new AuthenticationManager();
+
+ /// <summary>
+ /// Exposes the Http.Sys timeout configurations. These may also be configured in the registry.
+ /// These may be modified at any time prior to disposing the listener.
+ /// </summary>
+ public TimeoutManager Timeouts { get; } = new TimeoutManager();
+
+ /// <summary>
+ /// Gets or Sets if response body writes that fail due to client disconnects should throw exceptions or
+ /// complete normally. The default is false.
+ /// </summary>
+ public bool ThrowWriteExceptions { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum number of concurrent connections to accept, -1 for infinite, or null to
+ /// use the machine wide setting from the registry. The default value is null.
+ /// </summary>
+ public long? MaxConnections
+ {
+ get => _maxConnections;
+ set
+ {
+ if (value.HasValue && value < -1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), value, "The value must be positive, or -1 for infiniate.");
+ }
+
+ if (value.HasValue && _urlGroup != null)
+ {
+ _urlGroup.SetMaxConnections(value.Value);
+ }
+
+ _maxConnections = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum number of requests that will be queued up in Http.Sys.
+ /// </summary>
+ public long RequestQueueLimit
+ {
+ get
+ {
+ return _requestQueueLength;
+ }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), value, "The value must be greater than zero.");
+ }
+
+ if (_requestQueue != null)
+ {
+ _requestQueue.SetLengthLimit(_requestQueueLength);
+ }
+ // Only store it if it succeeds or hasn't started yet
+ _requestQueueLength = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum allowed size of any request body in bytes.
+ /// When set to null, the maximum request body size is unlimited.
+ /// This limit has no effect on upgraded connections which are always unlimited.
+ /// This can be overridden per-request via <see cref="IHttpMaxRequestBodySizeFeature"/>.
+ /// </summary>
+ /// <remarks>
+ /// Defaults to 30,000,000 bytes, which is approximately 28.6MB.
+ /// </remarks>
+ public long? MaxRequestBodySize
+ {
+ get => _maxRequestBodySize;
+ set
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), value, "The value must be greater or equal to zero.");
+ }
+ _maxRequestBodySize = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value that controls whether synchronous IO is allowed for the HttpContext.Request.Body and HttpContext.Response.Body.
+ /// The default is `true`.
+ /// </summary>
+ public bool AllowSynchronousIO { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets a value that controls how http.sys reacts when rejecting requests due to throttling conditions - like when the request
+ /// queue limit is reached. The default in http.sys is "Basic" which means http.sys is just resetting the TCP connection. IIS uses Limited
+ /// as its default behavior which will result in sending back a 503 - Service Unavailable back to the client.
+ /// </summary>
+ public Http503VerbosityLevel Http503Verbosity
+ {
+ get
+ {
+ return _rejectionVebosityLevel;
+ }
+ set
+ {
+ if (value < Http503VerbosityLevel.Basic || value > Http503VerbosityLevel.Full)
+ {
+ string message = String.Format(
+ CultureInfo.InvariantCulture,
+ "The value must be one of the values defined in the '{0}' enum.",
+ typeof(Http503VerbosityLevel).Name);
+
+ throw new ArgumentOutOfRangeException(nameof(value), value, message);
+ }
+
+ if (_requestQueue != null)
+ {
+ _requestQueue.SetRejectionVerbosity(value);
+ }
+ // Only store it if it succeeds or hasn't started yet
+ _rejectionVebosityLevel = value;
+ }
+ }
+
+ internal void Apply(UrlGroup urlGroup, RequestQueue requestQueue)
+ {
+ _urlGroup = urlGroup;
+ _requestQueue = requestQueue;
+
+ if (_maxConnections.HasValue)
+ {
+ _urlGroup.SetMaxConnections(_maxConnections.Value);
+ }
+
+ if (_requestQueueLength != DefaultRequestQueueLength)
+ {
+ _requestQueue.SetLengthLimit(_requestQueueLength);
+ }
+
+ if (_rejectionVebosityLevel != DefaultRejectionVerbosityLevel)
+ {
+ _requestQueue.SetRejectionVerbosity(_rejectionVebosityLevel);
+ }
+
+ Authentication.SetUrlGroupSecurity(urlGroup);
+ Timeouts.SetUrlGroupTimeouts(urlGroup);
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/LogHelper.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/LogHelper.cs
new file mode 100644
index 0000000000..8c977074bf
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/LogHelper.cs
@@ -0,0 +1,106 @@
+// 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 Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal static class LogHelper
+ {
+ internal static ILogger CreateLogger(ILoggerFactory factory, Type type)
+ {
+ if (factory == null)
+ {
+ return null;
+ }
+
+ return factory.CreateLogger(type.FullName);
+ }
+
+ internal static void LogInfo(ILogger logger, string data)
+ {
+ if (logger == null)
+ {
+ Debug.WriteLine(data);
+ }
+ else
+ {
+ logger.LogInformation(data);
+ }
+ }
+
+ internal static void LogWarning(ILogger logger, string data)
+ {
+ if (logger == null)
+ {
+ Debug.WriteLine(data);
+ }
+ else
+ {
+ logger.LogWarning(data);
+ }
+ }
+
+ internal static void LogDebug(ILogger logger, string data)
+ {
+ if (logger == null)
+ {
+ Debug.WriteLine(data);
+ }
+ else
+ {
+ logger.LogDebug(data);
+ }
+ }
+
+ internal static void LogDebug(ILogger logger, string location, string data)
+ {
+ if (logger == null)
+ {
+ Debug.WriteLine(data);
+ }
+ else
+ {
+ logger.LogDebug(location + "; " + data);
+ }
+ }
+
+ internal static void LogDebug(ILogger logger, string location, Exception exception)
+ {
+ if (logger == null)
+ {
+ Console.WriteLine(location + Environment.NewLine + exception.ToString());
+ }
+ else
+ {
+ logger.LogDebug(0, exception, location);
+ }
+ }
+
+ internal static void LogException(ILogger logger, string location, Exception exception)
+ {
+ if (logger == null)
+ {
+ Debug.WriteLine(location + Environment.NewLine + exception.ToString());
+ }
+ else
+ {
+ logger.LogError(0, exception, location);
+ }
+ }
+
+ internal static void LogError(ILogger logger, string location, string message)
+ {
+ if (logger == null)
+ {
+ Debug.WriteLine(message);
+ }
+ else
+ {
+ logger.LogError(location + "; " + message);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/MessagePump.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/MessagePump.cs
new file mode 100644
index 0000000000..e70c52d89c
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/MessagePump.cs
@@ -0,0 +1,329 @@
+// 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.Contracts;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.AspNetCore.HttpSys.Internal;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal class MessagePump : IServer
+ {
+ private readonly ILogger _logger;
+ private readonly HttpSysOptions _options;
+
+ private IHttpApplication<object> _application;
+
+ private int _maxAccepts;
+ private int _acceptorCounts;
+ private Action<object> _processRequest;
+
+ private volatile int _stopping;
+ private int _outstandingRequests;
+ private readonly TaskCompletionSource<object> _shutdownSignal = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
+ private int _shutdownSignalCompleted;
+
+ private readonly ServerAddressesFeature _serverAddresses;
+
+ public MessagePump(IOptions<HttpSysOptions> options, ILoggerFactory loggerFactory, IAuthenticationSchemeProvider authentication)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+ _options = options.Value;
+ Listener = new HttpSysListener(_options, loggerFactory);
+ _logger = LogHelper.CreateLogger(loggerFactory, typeof(MessagePump));
+
+ if (_options.Authentication.Schemes != AuthenticationSchemes.None)
+ {
+ authentication.AddScheme(new AuthenticationScheme(HttpSysDefaults.AuthenticationScheme, displayName: null, handlerType: typeof(AuthenticationHandler)));
+ }
+
+ Features = new FeatureCollection();
+ _serverAddresses = new ServerAddressesFeature();
+ Features.Set<IServerAddressesFeature>(_serverAddresses);
+
+ _processRequest = new Action<object>(ProcessRequestAsync);
+ _maxAccepts = _options.MaxAccepts;
+ }
+
+ internal HttpSysListener Listener { get; }
+
+ public IFeatureCollection Features { get; }
+
+ private bool Stopping => _stopping == 1;
+
+ public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ var hostingUrlsPresent = _serverAddresses.Addresses.Count > 0;
+
+ if (_serverAddresses.PreferHostingUrls && hostingUrlsPresent)
+ {
+ if (_options.UrlPrefixes.Count > 0)
+ {
+ LogHelper.LogWarning(_logger, $"Overriding endpoints added to {nameof(HttpSysOptions.UrlPrefixes)} since {nameof(IServerAddressesFeature.PreferHostingUrls)} is set to true." +
+ $" Binding to address(es) '{string.Join(", ", _serverAddresses.Addresses)}' instead. ");
+
+ Listener.Options.UrlPrefixes.Clear();
+ }
+
+ foreach (var value in _serverAddresses.Addresses)
+ {
+ Listener.Options.UrlPrefixes.Add(value);
+ }
+ }
+ else if (_options.UrlPrefixes.Count > 0)
+ {
+ if (hostingUrlsPresent)
+ {
+ LogHelper.LogWarning(_logger, $"Overriding address(es) '{string.Join(", ", _serverAddresses.Addresses)}'. " +
+ $"Binding to endpoints added to {nameof(HttpSysOptions.UrlPrefixes)} instead.");
+
+ _serverAddresses.Addresses.Clear();
+ }
+
+ foreach (var prefix in _options.UrlPrefixes)
+ {
+ _serverAddresses.Addresses.Add(prefix.FullPrefix);
+ }
+ }
+ else if (hostingUrlsPresent)
+ {
+ foreach (var value in _serverAddresses.Addresses)
+ {
+ Listener.Options.UrlPrefixes.Add(value);
+ }
+ }
+ else
+ {
+ LogHelper.LogDebug(_logger, $"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default.");
+
+ _serverAddresses.Addresses.Add(Constants.DefaultServerAddress);
+ Listener.Options.UrlPrefixes.Add(Constants.DefaultServerAddress);
+ }
+
+ // Can't call Start twice
+ Contract.Assert(_application == null);
+
+ Contract.Assert(application != null);
+
+ _application = new ApplicationWrapper<TContext>(application);
+
+ Listener.Start();
+
+ ActivateRequestProcessingLimits();
+
+ return Task.CompletedTask;
+ }
+
+ private void ActivateRequestProcessingLimits()
+ {
+ for (int i = _acceptorCounts; i < _maxAccepts; i++)
+ {
+ ProcessRequestsWorker();
+ }
+ }
+
+ // The message pump.
+ // When we start listening for the next request on one thread, we may need to be sure that the
+ // completion continues on another thread as to not block the current request processing.
+ // The awaits will manage stack depth for us.
+ private async void ProcessRequestsWorker()
+ {
+ int workerIndex = Interlocked.Increment(ref _acceptorCounts);
+ while (!Stopping && workerIndex <= _maxAccepts)
+ {
+ // Receive a request
+ RequestContext requestContext;
+ try
+ {
+ requestContext = await Listener.AcceptAsync().SupressContext();
+ }
+ catch (Exception exception)
+ {
+ Contract.Assert(Stopping);
+ if (Stopping)
+ {
+ LogHelper.LogDebug(_logger, "ListenForNextRequestAsync-Stopping", exception);
+ }
+ else
+ {
+ LogHelper.LogException(_logger, "ListenForNextRequestAsync", exception);
+ }
+ continue;
+ }
+ try
+ {
+ Task ignored = Task.Factory.StartNew(_processRequest, requestContext);
+ }
+ catch (Exception ex)
+ {
+ // Request processing failed to be queued in threadpool
+ // Log the error message, release throttle and move on
+ LogHelper.LogException(_logger, "ProcessRequestAsync", ex);
+ }
+ }
+ Interlocked.Decrement(ref _acceptorCounts);
+ }
+
+ private async void ProcessRequestAsync(object requestContextObj)
+ {
+ var requestContext = requestContextObj as RequestContext;
+ try
+ {
+ if (Stopping)
+ {
+ SetFatalResponse(requestContext, 503);
+ return;
+ }
+
+ object context = null;
+ Interlocked.Increment(ref _outstandingRequests);
+ try
+ {
+ var featureContext = new FeatureContext(requestContext);
+ context = _application.CreateContext(featureContext.Features);
+ try
+ {
+ await _application.ProcessRequestAsync(context).SupressContext();
+ await featureContext.OnResponseStart();
+ }
+ finally
+ {
+ await featureContext.OnCompleted();
+ }
+ _application.DisposeContext(context, null);
+ requestContext.Dispose();
+ }
+ catch (Exception ex)
+ {
+ LogHelper.LogException(_logger, "ProcessRequestAsync", ex);
+ _application.DisposeContext(context, ex);
+ if (requestContext.Response.HasStarted)
+ {
+ requestContext.Abort();
+ }
+ else
+ {
+ // We haven't sent a response yet, try to send a 500 Internal Server Error
+ requestContext.Response.Headers.Clear();
+ SetFatalResponse(requestContext, 500);
+ }
+ }
+ finally
+ {
+ if (Interlocked.Decrement(ref _outstandingRequests) == 0 && Stopping)
+ {
+ LogHelper.LogInfo(_logger, "All requests drained.");
+ _shutdownSignal.TrySetResult(0);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.LogException(_logger, "ProcessRequestAsync", ex);
+ requestContext.Abort();
+ }
+ }
+
+ private static void SetFatalResponse(RequestContext context, int status)
+ {
+ context.Response.StatusCode = status;
+ context.Response.ContentLength = 0;
+ context.Dispose();
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ void RegisterCancelation()
+ {
+ cancellationToken.Register(() =>
+ {
+ if (Interlocked.Exchange(ref _shutdownSignalCompleted, 1) == 0)
+ {
+ LogHelper.LogInfo(_logger, "Canceled, terminating " + _outstandingRequests + " request(s).");
+ _shutdownSignal.TrySetResult(null);
+ }
+ });
+ }
+
+ if (Interlocked.Exchange(ref _stopping, 1) == 1)
+ {
+ RegisterCancelation();
+
+ return _shutdownSignal.Task;
+ }
+
+ try
+ {
+ // Wait for active requests to drain
+ if (_outstandingRequests > 0)
+ {
+ LogHelper.LogInfo(_logger, "Stopping, waiting for " + _outstandingRequests + " request(s) to drain.");
+ RegisterCancelation();
+ }
+ else
+ {
+ _shutdownSignal.TrySetResult(null);
+ }
+ }
+ catch (Exception ex)
+ {
+ _shutdownSignal.TrySetException(ex);
+ }
+
+ return _shutdownSignal.Task;
+ }
+
+ public void Dispose()
+ {
+ _stopping = 1;
+ _shutdownSignal.TrySetResult(null);
+
+ Listener.Dispose();
+ }
+
+ private class ApplicationWrapper<TContext> : IHttpApplication<object>
+ {
+ private readonly IHttpApplication<TContext> _application;
+
+ public ApplicationWrapper(IHttpApplication<TContext> application)
+ {
+ _application = application;
+ }
+
+ public object CreateContext(IFeatureCollection contextFeatures)
+ {
+ return _application.CreateContext(contextFeatures);
+ }
+
+ public void DisposeContext(object context, Exception exception)
+ {
+ _application.DisposeContext((TContext)context, exception);
+ }
+
+ public Task ProcessRequestAsync(object context)
+ {
+ return _application.ProcessRequestAsync((TContext)context);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Microsoft.AspNetCore.Server.HttpSys.csproj b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Microsoft.AspNetCore.Server.HttpSys.csproj
new file mode 100644
index 0000000000..9be8bad15e
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Microsoft.AspNetCore.Server.HttpSys.csproj
@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core HTTP server that uses the Windows HTTP Server API.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;weblistener;httpsys</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\..\shared\Microsoft.AspNetCore.HttpSys.Sources\**\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Authentication.Core" Version="$(MicrosoftAspNetCoreAuthenticationCorePackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(MicrosoftAspNetCoreHostingPackageVersion)" />
+ <PackageReference Include="Microsoft.Net.Http.Headers" Version="$(MicrosoftNetHttpHeadersPackageVersion)" />
+ <PackageReference Include="Microsoft.Win32.Registry" Version="$(MicrosoftWin32RegistryPackageVersion)" />
+ <PackageReference Include="System.Security.Principal.Windows" Version="$(SystemSecurityPrincipalWindowsPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/ComNetOS.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/ComNetOS.cs
new file mode 100644
index 0000000000..12d1381bae
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/ComNetOS.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;
+using Microsoft.Extensions.Internal;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal static class ComNetOS
+ {
+ // Windows is assumed based on HttpApi.Supported which is checked in the HttpSysListener constructor.
+ // Minimum support for Windows 7 is assumed.
+ internal static readonly bool IsWin8orLater;
+
+ static ComNetOS()
+ {
+ var win8Version = new Version(6, 2);
+
+ IsWin8orLater = (Environment.OSVersion.Version >= win8Version);
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/DisconnectListener.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/DisconnectListener.cs
new file mode 100644
index 0000000000..ecef12b989
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/DisconnectListener.cs
@@ -0,0 +1,157 @@
+// 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.Concurrent;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Threading;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal class DisconnectListener
+ {
+ private readonly ConcurrentDictionary<ulong, ConnectionCancellation> _connectionCancellationTokens
+ = new ConcurrentDictionary<ulong, ConnectionCancellation>();
+
+ private readonly RequestQueue _requestQueue;
+ private readonly ILogger _logger;
+
+ internal DisconnectListener(RequestQueue requestQueue, ILogger logger)
+ {
+ _requestQueue = requestQueue;
+ _logger = logger;
+ }
+
+ internal CancellationToken GetTokenForConnection(ulong connectionId)
+ {
+ try
+ {
+ // Create exactly one CancellationToken per connection.
+ return GetOrCreateDisconnectToken(connectionId);
+ }
+ catch (Win32Exception exception)
+ {
+ LogHelper.LogException(_logger, "GetConnectionToken", exception);
+ return CancellationToken.None;
+ }
+ }
+
+ private CancellationToken GetOrCreateDisconnectToken(ulong connectionId)
+ {
+ // Read case is performance sensitive
+ ConnectionCancellation cancellation;
+ if (!_connectionCancellationTokens.TryGetValue(connectionId, out cancellation))
+ {
+ cancellation = GetCreatedConnectionCancellation(connectionId);
+ }
+ return cancellation.GetCancellationToken(connectionId);
+ }
+
+ private ConnectionCancellation GetCreatedConnectionCancellation(ulong connectionId)
+ {
+ // Race condition on creation has no side effects
+ var cancellation = new ConnectionCancellation(this);
+ return _connectionCancellationTokens.GetOrAdd(connectionId, cancellation);
+ }
+
+ private unsafe CancellationToken CreateDisconnectToken(ulong connectionId)
+ {
+ LogHelper.LogDebug(_logger, "CreateDisconnectToken", "Registering connection for disconnect for connection ID: " + connectionId);
+
+ // Create a nativeOverlapped callback so we can register for disconnect callback
+ var cts = new CancellationTokenSource();
+ var returnToken = cts.Token;
+
+ SafeNativeOverlapped nativeOverlapped = null;
+ var boundHandle = _requestQueue.BoundHandle;
+ nativeOverlapped = new SafeNativeOverlapped(boundHandle, boundHandle.AllocateNativeOverlapped(
+ (errorCode, numBytes, overlappedPtr) =>
+ {
+ LogHelper.LogDebug(_logger, "CreateDisconnectToken", "http.sys disconnect callback fired for connection ID: " + connectionId);
+
+ // Free the overlapped
+ nativeOverlapped.Dispose();
+
+ // Pull the token out of the list and Cancel it.
+ ConnectionCancellation token;
+ _connectionCancellationTokens.TryRemove(connectionId, out token);
+ try
+ {
+ cts.Cancel();
+ }
+ catch (AggregateException exception)
+ {
+ LogHelper.LogException(_logger, "CreateDisconnectToken Callback", exception);
+ }
+ },
+ null, null));
+
+ uint statusCode;
+ try
+ {
+ statusCode = HttpApi.HttpWaitForDisconnectEx(requestQueueHandle: _requestQueue.Handle,
+ connectionId: connectionId, reserved: 0, overlapped: nativeOverlapped);
+ }
+ catch (Win32Exception exception)
+ {
+ statusCode = (uint)exception.NativeErrorCode;
+ LogHelper.LogException(_logger, "CreateDisconnectToken", exception);
+ }
+
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING &&
+ statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
+ {
+ // We got an unknown result, assume the connection has been closed.
+ nativeOverlapped.Dispose();
+ ConnectionCancellation ignored;
+ _connectionCancellationTokens.TryRemove(connectionId, out ignored);
+ LogHelper.LogDebug(_logger, "HttpWaitForDisconnectEx", new Win32Exception((int)statusCode));
+ cts.Cancel();
+ }
+
+ if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && HttpSysListener.SkipIOCPCallbackOnSuccess)
+ {
+ // IO operation completed synchronously - callback won't be called to signal completion
+ nativeOverlapped.Dispose();
+ ConnectionCancellation ignored;
+ _connectionCancellationTokens.TryRemove(connectionId, out ignored);
+ cts.Cancel();
+ }
+
+ return returnToken;
+ }
+
+ private class ConnectionCancellation
+ {
+ private readonly DisconnectListener _parent;
+ private volatile bool _initialized; // Must be volatile because initialization is synchronized
+ private CancellationToken _cancellationToken;
+
+ public ConnectionCancellation(DisconnectListener parent)
+ {
+ _parent = parent;
+ }
+
+ internal CancellationToken GetCancellationToken(ulong connectionId)
+ {
+ // Initialized case is performance sensitive
+ if (_initialized)
+ {
+ return _cancellationToken;
+ }
+ return InitializeCancellationToken(connectionId);
+ }
+
+ private CancellationToken InitializeCancellationToken(ulong connectionId)
+ {
+ object syncObject = this;
+#pragma warning disable 420 // Disable warning about volatile by reference since EnsureInitialized does volatile operations
+ return LazyInitializer.EnsureInitialized(ref _cancellationToken, ref _initialized, ref syncObject, () => _parent.CreateDisconnectToken(connectionId));
+#pragma warning restore 420
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpApi.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpApi.cs
new file mode 100644
index 0000000000..1dc97ef20d
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpApi.cs
@@ -0,0 +1,133 @@
+// 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.Runtime.InteropServices;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using static Microsoft.AspNetCore.HttpSys.Internal.HttpApiTypes;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal static unsafe class HttpApi
+ {
+ private const string HTTPAPI = "httpapi.dll";
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpInitialize(HTTPAPI_VERSION version, uint flags, void* pReserved);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpReceiveRequestEntityBody(SafeHandle requestQueueHandle, ulong requestId, uint flags, IntPtr pEntityBuffer, uint entityBufferLength, out uint bytesReturned, SafeNativeOverlapped pOverlapped);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpReceiveClientCertificate(SafeHandle requestQueueHandle, ulong connectionId, uint flags, HTTP_SSL_CLIENT_CERT_INFO* pSslClientCertInfo, uint sslClientCertInfoSize, uint* pBytesReceived, SafeNativeOverlapped pOverlapped);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpReceiveClientCertificate(SafeHandle requestQueueHandle, ulong connectionId, uint flags, byte* pSslClientCertInfo, uint sslClientCertInfoSize, uint* pBytesReceived, SafeNativeOverlapped pOverlapped);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpReceiveHttpRequest(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_REQUEST* pRequestBuffer, uint requestBufferLength, uint* pBytesReturned, SafeNativeOverlapped pOverlapped);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpSendHttpResponse(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_RESPONSE_V2* pHttpResponse, HTTP_CACHE_POLICY* pCachePolicy, uint* pBytesSent, IntPtr pReserved1, uint Reserved2, SafeNativeOverlapped pOverlapped, IntPtr pLogData);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpSendResponseEntityBody(SafeHandle requestQueueHandle, ulong requestId, uint flags, ushort entityChunkCount, HTTP_DATA_CHUNK* pEntityChunks, uint* pBytesSent, IntPtr pReserved1, uint Reserved2, SafeNativeOverlapped pOverlapped, IntPtr pLogData);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpCancelHttpRequest(SafeHandle requestQueueHandle, ulong requestId, IntPtr pOverlapped);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpWaitForDisconnectEx(SafeHandle requestQueueHandle, ulong connectionId, uint reserved, SafeNativeOverlapped overlapped);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpCreateServerSession(HTTPAPI_VERSION version, ulong* serverSessionId, uint reserved);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpCreateUrlGroup(ulong serverSessionId, ulong* urlGroupId, uint reserved);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern uint HttpAddUrlToUrlGroup(ulong urlGroupId, string pFullyQualifiedUrl, ulong context, uint pReserved);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpSetUrlGroupProperty(ulong urlGroupId, HTTP_SERVER_PROPERTY serverProperty, IntPtr pPropertyInfo, uint propertyInfoLength);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern uint HttpRemoveUrlFromUrlGroup(ulong urlGroupId, string pFullyQualifiedUrl, uint flags);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpCloseServerSession(ulong serverSessionId);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpCloseUrlGroup(ulong urlGroupId);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern uint HttpSetRequestQueueProperty(SafeHandle requestQueueHandle, HTTP_SERVER_PROPERTY serverProperty, IntPtr pPropertyInfo, uint propertyInfoLength, uint reserved, IntPtr pReserved);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern unsafe uint HttpCreateRequestQueue(HTTPAPI_VERSION version, string pName,
+ UnsafeNclNativeMethods.SECURITY_ATTRIBUTES pSecurityAttributes, uint flags, out HttpRequestQueueV2Handle pReqQueueHandle);
+
+ [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
+ internal static extern unsafe uint HttpCloseRequestQueue(IntPtr pReqQueueHandle);
+
+
+ private static HTTPAPI_VERSION version;
+
+ // This property is used by HttpListener to pass the version structure to the native layer in API
+ // calls.
+
+ internal static HTTPAPI_VERSION Version
+ {
+ get
+ {
+ return version;
+ }
+ }
+
+ // This property is used by HttpListener to get the Api version in use so that it uses appropriate
+ // Http APIs.
+
+ internal static HTTP_API_VERSION ApiVersion
+ {
+ get
+ {
+ if (version.HttpApiMajorVersion == 2 && version.HttpApiMinorVersion == 0)
+ {
+ return HTTP_API_VERSION.Version20;
+ }
+ else if (version.HttpApiMajorVersion == 1 && version.HttpApiMinorVersion == 0)
+ {
+ return HTTP_API_VERSION.Version10;
+ }
+ else
+ {
+ return HTTP_API_VERSION.Invalid;
+ }
+ }
+ }
+
+ static HttpApi()
+ {
+ InitHttpApi(2, 0);
+ }
+
+ private static void InitHttpApi(ushort majorVersion, ushort minorVersion)
+ {
+ version.HttpApiMajorVersion = majorVersion;
+ version.HttpApiMinorVersion = minorVersion;
+
+ var statusCode = HttpInitialize(version, (uint)HTTP_FLAGS.HTTP_INITIALIZE_SERVER, null);
+
+ supported = statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS;
+ }
+
+ private static volatile bool supported;
+ internal static bool Supported
+ {
+ get
+ {
+ return supported;
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpRequestQueueV2Handle.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpRequestQueueV2Handle.cs
new file mode 100644
index 0000000000..6fac23f67f
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpRequestQueueV2Handle.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.AspNetCore.HttpSys.Internal;
+using Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ // This class is a wrapper for Http.sys V2 request queue handle.
+ internal sealed class HttpRequestQueueV2Handle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ private HttpRequestQueueV2Handle()
+ : base(true)
+ {
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ return (HttpApi.HttpCloseRequestQueue(handle) ==
+ UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS);
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpServerSessionHandle.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpServerSessionHandle.cs
new file mode 100644
index 0000000000..903e4d9065
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpServerSessionHandle.cs
@@ -0,0 +1,46 @@
+// 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.Threading;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.Win32.SafeHandles;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal sealed class HttpServerSessionHandle : CriticalHandleZeroOrMinusOneIsInvalid
+ {
+ private int disposed;
+ private ulong serverSessionId;
+
+ internal HttpServerSessionHandle(ulong id)
+ : base()
+ {
+ serverSessionId = id;
+
+ // This class uses no real handle so we need to set a dummy handle. Otherwise, IsInvalid always remains
+ // true.
+
+ SetHandle(new IntPtr(1));
+ }
+
+ internal ulong DangerousGetServerSessionId()
+ {
+ return serverSessionId;
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ if (!IsInvalid)
+ {
+ if (Interlocked.Increment(ref disposed) == 1)
+ {
+ // Closing server session also closes all open url groups under that server session.
+ return (HttpApi.HttpCloseServerSession(serverSessionId) ==
+ UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS);
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpSysSettings.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpSysSettings.cs
new file mode 100644
index 0000000000..d9efa35c8f
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/HttpSysSettings.cs
@@ -0,0 +1,113 @@
+// 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.IO;
+using System.Security;
+using Microsoft.Win32;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal static class HttpSysSettings
+ {
+ private const string HttpSysParametersKey = @"System\CurrentControlSet\Services\HTTP\Parameters";
+ private const bool EnableNonUtf8Default = true;
+ private const bool FavorUtf8Default = true;
+ private const string EnableNonUtf8Name = "EnableNonUtf8";
+ private const string FavorUtf8Name = "FavorUtf8";
+
+ private static volatile bool enableNonUtf8 = EnableNonUtf8Default;
+ private static volatile bool favorUtf8 = FavorUtf8Default;
+
+ static HttpSysSettings()
+ {
+ ReadHttpSysRegistrySettings();
+ }
+
+ internal static bool EnableNonUtf8
+ {
+ get { return enableNonUtf8; }
+ }
+
+ internal static bool FavorUtf8
+ {
+ get { return favorUtf8; }
+ }
+
+ private static void ReadHttpSysRegistrySettings()
+ {
+ try
+ {
+ RegistryKey httpSysParameters = Registry.LocalMachine.OpenSubKey(HttpSysParametersKey);
+
+ if (httpSysParameters == null)
+ {
+ LogWarning("ReadHttpSysRegistrySettings", "The Http.Sys registry key is null.",
+ HttpSysParametersKey);
+ }
+ else
+ {
+ using (httpSysParameters)
+ {
+ enableNonUtf8 = ReadRegistryValue(httpSysParameters, EnableNonUtf8Name, EnableNonUtf8Default);
+ favorUtf8 = ReadRegistryValue(httpSysParameters, FavorUtf8Name, FavorUtf8Default);
+ }
+ }
+ }
+ catch (SecurityException e)
+ {
+ LogRegistryException("ReadHttpSysRegistrySettings", e);
+ }
+ catch (ObjectDisposedException e)
+ {
+ LogRegistryException("ReadHttpSysRegistrySettings", e);
+ }
+ }
+
+ private static bool ReadRegistryValue(RegistryKey key, string valueName, bool defaultValue)
+ {
+ Debug.Assert(key != null, "'key' must not be null");
+
+ try
+ {
+ if (key.GetValue(valueName) != null && key.GetValueKind(valueName) == RegistryValueKind.DWord)
+ {
+ // At this point we know the Registry value exists and it must be valid (any DWORD value
+ // can be converted to a bool).
+ return Convert.ToBoolean(key.GetValue(valueName), CultureInfo.InvariantCulture);
+ }
+ }
+ catch (UnauthorizedAccessException e)
+ {
+ LogRegistryException("ReadRegistryValue", e);
+ }
+ catch (IOException e)
+ {
+ LogRegistryException("ReadRegistryValue", e);
+ }
+ catch (SecurityException e)
+ {
+ LogRegistryException("ReadRegistryValue", e);
+ }
+ catch (ObjectDisposedException e)
+ {
+ LogRegistryException("ReadRegistryValue", e);
+ }
+
+ return defaultValue;
+ }
+
+ private static void LogRegistryException(string methodName, Exception e)
+ {
+ LogWarning(methodName, "Unable to access the Http.Sys registry value.", HttpSysParametersKey, e);
+ }
+
+ private static void LogWarning(string methodName, string message, params object[] args)
+ {
+ // TODO: log
+ // Logging.PrintWarning(Logging.HttpListener, typeof(HttpSysSettings), methodName, SR.GetString(message, args));
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/IntPtrHelper.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/IntPtrHelper.cs
new file mode 100644
index 0000000000..f2cce9fff4
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/IntPtrHelper.cs
@@ -0,0 +1,20 @@
+// 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.Server.HttpSys
+{
+ internal static class IntPtrHelper
+ {
+ internal static IntPtr Add(IntPtr a, int b)
+ {
+ return (IntPtr)((long)a + (long)b);
+ }
+
+ internal static long Subtract(IntPtr a, IntPtr b)
+ {
+ return ((long)a - (long)b);
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/RequestQueue.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/RequestQueue.cs
new file mode 100644
index 0000000000..1d3546f91b
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/RequestQueue.cs
@@ -0,0 +1,137 @@
+// 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.Runtime.InteropServices;
+using System.Threading;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal class RequestQueue
+ {
+ private static readonly int BindingInfoSize =
+ Marshal.SizeOf<HttpApiTypes.HTTP_BINDING_INFO>();
+
+ private readonly UrlGroup _urlGroup;
+ private readonly ILogger _logger;
+ private bool _disposed;
+
+ internal RequestQueue(UrlGroup urlGroup, ILogger logger)
+ {
+ _urlGroup = urlGroup;
+ _logger = logger;
+
+ HttpRequestQueueV2Handle requestQueueHandle = null;
+ var statusCode = HttpApi.HttpCreateRequestQueue(
+ HttpApi.Version, null, null, 0, out requestQueueHandle);
+
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
+ {
+ throw new HttpSysException((int)statusCode);
+ }
+
+ // Disabling callbacks when IO operation completes synchronously (returns ErrorCodes.ERROR_SUCCESS)
+ if (HttpSysListener.SkipIOCPCallbackOnSuccess &&
+ !UnsafeNclNativeMethods.SetFileCompletionNotificationModes(
+ requestQueueHandle,
+ UnsafeNclNativeMethods.FileCompletionNotificationModes.SkipCompletionPortOnSuccess |
+ UnsafeNclNativeMethods.FileCompletionNotificationModes.SkipSetEventOnHandle))
+ {
+ throw new HttpSysException(Marshal.GetLastWin32Error());
+ }
+
+ Handle = requestQueueHandle;
+ BoundHandle = ThreadPoolBoundHandle.BindHandle(Handle);
+ }
+
+ internal SafeHandle Handle { get; }
+ internal ThreadPoolBoundHandle BoundHandle { get; }
+
+ internal unsafe void AttachToUrlGroup()
+ {
+ CheckDisposed();
+ // Set the association between request queue and url group. After this, requests for registered urls will
+ // get delivered to this request queue.
+
+ var info = new HttpApiTypes.HTTP_BINDING_INFO();
+ info.Flags = HttpApiTypes.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT;
+ info.RequestQueueHandle = Handle.DangerousGetHandle();
+
+ var infoptr = new IntPtr(&info);
+
+ _urlGroup.SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerBindingProperty,
+ infoptr, (uint)BindingInfoSize);
+ }
+
+ internal unsafe void DetachFromUrlGroup()
+ {
+ CheckDisposed();
+ // Break the association between request queue and url group. After this, requests for registered urls
+ // will get 503s.
+ // Note that this method may be called multiple times (Stop() and then Abort()). This
+ // is fine since http.sys allows to set HttpServerBindingProperty multiple times for valid
+ // Url groups.
+
+ var info = new HttpApiTypes.HTTP_BINDING_INFO();
+ info.Flags = HttpApiTypes.HTTP_FLAGS.NONE;
+ info.RequestQueueHandle = IntPtr.Zero;
+
+ var infoptr = new IntPtr(&info);
+
+ _urlGroup.SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerBindingProperty,
+ infoptr, (uint)BindingInfoSize, throwOnError: false);
+ }
+
+ // The listener must be active for this to work.
+ internal unsafe void SetLengthLimit(long length)
+ {
+ CheckDisposed();
+
+ var result = HttpApi.HttpSetRequestQueueProperty(Handle,
+ HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerQueueLengthProperty,
+ new IntPtr((void*)&length), (uint)Marshal.SizeOf<long>(), 0, IntPtr.Zero);
+
+ if (result != 0)
+ {
+ throw new HttpSysException((int)result);
+ }
+ }
+
+ // The listener must be active for this to work.
+ internal unsafe void SetRejectionVerbosity(Http503VerbosityLevel verbosity)
+ {
+ CheckDisposed();
+
+ var result = HttpApi.HttpSetRequestQueueProperty(Handle,
+ HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServer503VerbosityProperty,
+ new IntPtr((void*)&verbosity), (uint)Marshal.SizeOf<long>(), 0, IntPtr.Zero);
+
+ if (result != 0)
+ {
+ throw new HttpSysException((int)result);
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ BoundHandle.Dispose();
+ Handle.Dispose();
+ }
+
+ private void CheckDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(this.GetType().FullName);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/ServerSession.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/ServerSession.cs
new file mode 100644
index 0000000000..1d2798bd8e
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/ServerSession.cs
@@ -0,0 +1,35 @@
+// 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 Microsoft.AspNetCore.HttpSys.Internal;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal class ServerSession : IDisposable
+ {
+ internal unsafe ServerSession()
+ {
+ ulong serverSessionId = 0;
+ var statusCode = HttpApi.HttpCreateServerSession(
+ HttpApi.Version, &serverSessionId, 0);
+
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
+ {
+ throw new HttpSysException((int)statusCode);
+ }
+
+ Debug.Assert(serverSessionId != 0, "Invalid id returned by HttpCreateServerSession");
+
+ Id = new HttpServerSessionHandle(serverSessionId);
+ }
+
+ public HttpServerSessionHandle Id { get; private set; }
+
+ public void Dispose()
+ {
+ Id.Dispose();
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/TokenBindingUtil.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/TokenBindingUtil.cs
new file mode 100644
index 0000000000..21d17656a8
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/TokenBindingUtil.cs
@@ -0,0 +1,82 @@
+// 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.Runtime.InteropServices;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using static Microsoft.AspNetCore.HttpSys.Internal.HttpApiTypes;
+using static Microsoft.AspNetCore.HttpSys.Internal.UnsafeNclNativeMethods.TokenBinding;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ /// <summary>
+ /// Contains helpers for dealing with TLS token binding.
+ /// </summary>
+ // TODO: https://github.com/aspnet/HttpSysServer/issues/231
+ internal unsafe static class TokenBindingUtil
+ {
+ private static byte[] ExtractIdentifierBlob(TOKENBINDING_RESULT_DATA* pTokenBindingResultData)
+ {
+ // Per http://tools.ietf.org/html/draft-ietf-tokbind-protocol-00, Sec. 4,
+ // the identifier is a tuple which contains (token binding type, hash algorithm
+ // signature algorithm, key data). We'll strip off the token binding type and
+ // return the remainder (starting with the hash algorithm) as an opaque blob.
+ byte[] retVal = new byte[checked(pTokenBindingResultData->identifierSize - 1)];
+ Marshal.Copy((IntPtr)(&pTokenBindingResultData->identifierData->hashAlgorithm), retVal, 0, retVal.Length);
+ return retVal;
+ }
+
+ /// <summary>
+ /// Returns the 'provided' token binding identifier, optionally also returning the
+ /// 'referred' token binding identifier. Returns null on failure.
+ /// </summary>
+ public static byte[] GetProvidedTokenIdFromBindingInfo(HTTP_REQUEST_TOKEN_BINDING_INFO* pTokenBindingInfo, out byte[] referredId)
+ {
+ byte[] providedId = null;
+ referredId = null;
+
+ HeapAllocHandle handle = null;
+ int status = UnsafeNclNativeMethods.TokenBindingVerifyMessage(
+ pTokenBindingInfo->TokenBinding,
+ pTokenBindingInfo->TokenBindingSize,
+ pTokenBindingInfo->KeyType,
+ pTokenBindingInfo->TlsUnique,
+ pTokenBindingInfo->TlsUniqueSize,
+ out handle);
+
+ // No match found or there was an error?
+ if (status != 0 || handle == null || handle.IsInvalid)
+ {
+ return null;
+ }
+
+ using (handle)
+ {
+ // Find the first 'provided' and 'referred' types.
+ TOKENBINDING_RESULT_LIST* pResultList = (TOKENBINDING_RESULT_LIST*)handle.DangerousGetHandle();
+ for (int i = 0; i < pResultList->resultCount; i++)
+ {
+ TOKENBINDING_RESULT_DATA* pThisResultData = &pResultList->resultData[i];
+ if (pThisResultData->identifierData->bindingType == TOKENBINDING_TYPE.TOKENBINDING_TYPE_PROVIDED)
+ {
+ if (providedId != null)
+ {
+ return null; // It is invalid to have more than one 'provided' identifier.
+ }
+ providedId = ExtractIdentifierBlob(pThisResultData);
+ }
+ else if (pThisResultData->identifierData->bindingType == TOKENBINDING_TYPE.TOKENBINDING_TYPE_REFERRED)
+ {
+ if (referredId != null)
+ {
+ return null; // It is invalid to have more than one 'referred' identifier.
+ }
+ referredId = ExtractIdentifierBlob(pThisResultData);
+ }
+ }
+ }
+
+ return providedId;
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/UrlGroup.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/UrlGroup.cs
new file mode 100644
index 0000000000..8f33c7b678
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/NativeInterop/UrlGroup.cs
@@ -0,0 +1,134 @@
+// 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.Runtime.InteropServices;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal class UrlGroup : IDisposable
+ {
+ private static readonly int QosInfoSize =
+ Marshal.SizeOf<HttpApiTypes.HTTP_QOS_SETTING_INFO>();
+
+ private ServerSession _serverSession;
+ private ILogger _logger;
+ private bool _disposed;
+
+ internal unsafe UrlGroup(ServerSession serverSession, ILogger logger)
+ {
+ _serverSession = serverSession;
+ _logger = logger;
+
+ ulong urlGroupId = 0;
+ var statusCode = HttpApi.HttpCreateUrlGroup(
+ _serverSession.Id.DangerousGetServerSessionId(), &urlGroupId, 0);
+
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
+ {
+ throw new HttpSysException((int)statusCode);
+ }
+
+ Debug.Assert(urlGroupId != 0, "Invalid id returned by HttpCreateUrlGroup");
+ Id = urlGroupId;
+ }
+
+ internal ulong Id { get; private set; }
+
+ internal unsafe void SetMaxConnections(long maxConnections)
+ {
+ var connectionLimit = new HttpApiTypes.HTTP_CONNECTION_LIMIT_INFO();
+ connectionLimit.Flags = HttpApiTypes.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT;
+ connectionLimit.MaxConnections = (uint)maxConnections;
+
+ var qosSettings = new HttpApiTypes.HTTP_QOS_SETTING_INFO();
+ qosSettings.QosType = HttpApiTypes.HTTP_QOS_SETTING_TYPE.HttpQosSettingTypeConnectionLimit;
+ qosSettings.QosSetting = new IntPtr(&connectionLimit);
+
+ SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerQosProperty, new IntPtr(&qosSettings), (uint)QosInfoSize);
+ }
+
+ internal void SetProperty(HttpApiTypes.HTTP_SERVER_PROPERTY property, IntPtr info, uint infosize, bool throwOnError = true)
+ {
+ Debug.Assert(info != IntPtr.Zero, "SetUrlGroupProperty called with invalid pointer");
+ CheckDisposed();
+
+ var statusCode = HttpApi.HttpSetUrlGroupProperty(Id, property, info, infosize);
+
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
+ {
+ var exception = new HttpSysException((int)statusCode);
+ LogHelper.LogException(_logger, "SetUrlGroupProperty", exception);
+ if (throwOnError)
+ {
+ throw exception;
+ }
+ }
+ }
+
+ internal void RegisterPrefix(string uriPrefix, int contextId)
+ {
+ LogHelper.LogInfo(_logger, "Listening on prefix: " + uriPrefix);
+ CheckDisposed();
+
+ var statusCode = HttpApi.HttpAddUrlToUrlGroup(Id, uriPrefix, (ulong)contextId, 0);
+
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
+ {
+ if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_ALREADY_EXISTS)
+ {
+ throw new HttpSysException((int)statusCode, string.Format(Resources.Exception_PrefixAlreadyRegistered, uriPrefix));
+ }
+ else
+ {
+ throw new HttpSysException((int)statusCode);
+ }
+ }
+ }
+
+ internal bool UnregisterPrefix(string uriPrefix)
+ {
+ LogHelper.LogInfo(_logger, "Stop listening on prefix: " + uriPrefix);
+ CheckDisposed();
+
+ var statusCode = HttpApi.HttpRemoveUrlFromUrlGroup(Id, uriPrefix, 0);
+
+ if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_NOT_FOUND)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ Debug.Assert(Id != 0, "HttpCloseUrlGroup called with invalid url group id");
+
+ uint statusCode = HttpApi.HttpCloseUrlGroup(Id);
+
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
+ {
+ LogHelper.LogError(_logger, "CleanupV2Config", "Result: " + statusCode);
+ }
+ Id = 0;
+ }
+
+ private void CheckDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(this.GetType().FullName);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Properties/AssemblyInfo.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..3bc10f0e3c
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// 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("Microsoft.AspNetCore.Server.HttpSys.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Properties/Resources.Designer.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..3360ccd879
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Properties/Resources.Designer.cs
@@ -0,0 +1,206 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Server.HttpSys.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// The destination array is too small.
+ /// </summary>
+ internal static string Exception_ArrayTooSmall
+ {
+ get { return GetString("Exception_ArrayTooSmall"); }
+ }
+
+ /// <summary>
+ /// The destination array is too small.
+ /// </summary>
+ internal static string FormatException_ArrayTooSmall()
+ {
+ return GetString("Exception_ArrayTooSmall");
+ }
+
+ /// <summary>
+ /// End has already been called.
+ /// </summary>
+ internal static string Exception_EndCalledMultipleTimes
+ {
+ get { return GetString("Exception_EndCalledMultipleTimes"); }
+ }
+
+ /// <summary>
+ /// End has already been called.
+ /// </summary>
+ internal static string FormatException_EndCalledMultipleTimes()
+ {
+ return GetString("Exception_EndCalledMultipleTimes");
+ }
+
+ /// <summary>
+ /// The status code '{0}' is not supported.
+ /// </summary>
+ internal static string Exception_InvalidStatusCode
+ {
+ get { return GetString("Exception_InvalidStatusCode"); }
+ }
+
+ /// <summary>
+ /// The status code '{0}' is not supported.
+ /// </summary>
+ internal static string FormatException_InvalidStatusCode(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Exception_InvalidStatusCode"), p0);
+ }
+
+ /// <summary>
+ /// The stream is not seekable.
+ /// </summary>
+ internal static string Exception_NoSeek
+ {
+ get { return GetString("Exception_NoSeek"); }
+ }
+
+ /// <summary>
+ /// The stream is not seekable.
+ /// </summary>
+ internal static string FormatException_NoSeek()
+ {
+ return GetString("Exception_NoSeek");
+ }
+
+ /// <summary>
+ /// The prefix '{0}' is already registered.
+ /// </summary>
+ internal static string Exception_PrefixAlreadyRegistered
+ {
+ get { return GetString("Exception_PrefixAlreadyRegistered"); }
+ }
+
+ /// <summary>
+ /// The prefix '{0}' is already registered.
+ /// </summary>
+ internal static string FormatException_PrefixAlreadyRegistered(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Exception_PrefixAlreadyRegistered"), p0);
+ }
+
+ /// <summary>
+ /// This stream only supports read operations.
+ /// </summary>
+ internal static string Exception_ReadOnlyStream
+ {
+ get { return GetString("Exception_ReadOnlyStream"); }
+ }
+
+ /// <summary>
+ /// This stream only supports read operations.
+ /// </summary>
+ internal static string FormatException_ReadOnlyStream()
+ {
+ return GetString("Exception_ReadOnlyStream");
+ }
+
+ /// <summary>
+ /// More data written than specified in the Content-Length header.
+ /// </summary>
+ internal static string Exception_TooMuchWritten
+ {
+ get { return GetString("Exception_TooMuchWritten"); }
+ }
+
+ /// <summary>
+ /// More data written than specified in the Content-Length header.
+ /// </summary>
+ internal static string FormatException_TooMuchWritten()
+ {
+ return GetString("Exception_TooMuchWritten");
+ }
+
+ /// <summary>
+ /// Only the http and https schemes are supported.
+ /// </summary>
+ internal static string Exception_UnsupportedScheme
+ {
+ get { return GetString("Exception_UnsupportedScheme"); }
+ }
+
+ /// <summary>
+ /// Only the http and https schemes are supported.
+ /// </summary>
+ internal static string FormatException_UnsupportedScheme()
+ {
+ return GetString("Exception_UnsupportedScheme");
+ }
+
+ /// <summary>
+ /// This stream only supports write operations.
+ /// </summary>
+ internal static string Exception_WriteOnlyStream
+ {
+ get { return GetString("Exception_WriteOnlyStream"); }
+ }
+
+ /// <summary>
+ /// This stream only supports write operations.
+ /// </summary>
+ internal static string FormatException_WriteOnlyStream()
+ {
+ return GetString("Exception_WriteOnlyStream");
+ }
+
+ /// <summary>
+ /// The given IAsyncResult does not match this opperation.
+ /// </summary>
+ internal static string Exception_WrongIAsyncResult
+ {
+ get { return GetString("Exception_WrongIAsyncResult"); }
+ }
+
+ /// <summary>
+ /// The given IAsyncResult does not match this opperation.
+ /// </summary>
+ internal static string FormatException_WrongIAsyncResult()
+ {
+ return GetString("Exception_WrongIAsyncResult");
+ }
+
+ /// <summary>
+ /// An exception occured while running an action registered with {0}.
+ /// </summary>
+ internal static string Warning_ExceptionInOnResponseCompletedAction
+ {
+ get { return GetString("Warning_ExceptionInOnResponseCompletedAction"); }
+ }
+
+ /// <summary>
+ /// An exception occured while running an action registered with {0}.
+ /// </summary>
+ internal static string FormatWarning_ExceptionInOnResponseCompletedAction(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Warning_ExceptionInOnResponseCompletedAction"), p0);
+ }
+
+ 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/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/BoundaryType.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/BoundaryType.cs
new file mode 100644
index 0000000000..6e2565b433
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/BoundaryType.cs
@@ -0,0 +1,15 @@
+// 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.Server.HttpSys
+{
+ internal enum BoundaryType
+ {
+ None = 0,
+ Chunked = 1, // Transfer-Encoding: chunked
+ ContentLength = 2, // Content-Length: XXX
+ Close = 3, // Connection: close
+ PassThrough = 4, // The application is handling the boundary themselves (e.g. chunking themselves).
+ Invalid = 5,
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/ClientCertLoader.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/ClientCertLoader.cs
new file mode 100644
index 0000000000..74d7ed902f
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/ClientCertLoader.cs
@@ -0,0 +1,433 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Runtime.InteropServices;
+using System.Security;
+using System.Security.Authentication.ExtendedProtection;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ // This class is used to load the client certificate on-demand. Because client certs are optional, all
+ // failures are handled internally and reported via ClientCertException or ClientCertError.
+ internal unsafe sealed class ClientCertLoader : IAsyncResult, IDisposable
+ {
+ private const uint CertBoblSize = 1500;
+ private static readonly IOCompletionCallback IOCallback = new IOCompletionCallback(WaitCallback);
+ private static readonly int RequestChannelBindStatusSize =
+ Marshal.SizeOf<HttpApiTypes.HTTP_REQUEST_CHANNEL_BIND_STATUS>();
+
+ private SafeNativeOverlapped _overlapped;
+ private byte[] _backingBuffer;
+ private HttpApiTypes.HTTP_SSL_CLIENT_CERT_INFO* _memoryBlob;
+ private uint _size;
+ private TaskCompletionSource<object> _tcs;
+ private RequestContext _requestContext;
+
+ private int _clientCertError;
+ private X509Certificate2 _clientCert;
+ private Exception _clientCertException;
+ private CancellationTokenRegistration _cancellationRegistration;
+
+ internal ClientCertLoader(RequestContext requestContext, CancellationToken cancellationToken)
+ {
+ _requestContext = requestContext;
+ _tcs = new TaskCompletionSource<object>();
+ // we will use this overlapped structure to issue async IO to ul
+ // the event handle will be put in by the BeginHttpApi2.ERROR_SUCCESS() method
+ Reset(CertBoblSize);
+
+ if (cancellationToken.CanBeCanceled)
+ {
+ _cancellationRegistration = RequestContext.RegisterForCancellation(cancellationToken);
+ }
+ }
+
+ internal SafeHandle RequestQueueHandle => _requestContext.Server.RequestQueue.Handle;
+
+ internal X509Certificate2 ClientCert
+ {
+ get
+ {
+ Contract.Assert(Task.IsCompleted);
+ return _clientCert;
+ }
+ }
+
+ internal int ClientCertError
+ {
+ get
+ {
+ Contract.Assert(Task.IsCompleted);
+ return _clientCertError;
+ }
+ }
+
+ internal Exception ClientCertException
+ {
+ get
+ {
+ Contract.Assert(Task.IsCompleted);
+ return _clientCertException;
+ }
+ }
+
+ private RequestContext RequestContext
+ {
+ get
+ {
+ return _requestContext;
+ }
+ }
+
+ private Task Task
+ {
+ get
+ {
+ return _tcs.Task;
+ }
+ }
+
+ private SafeNativeOverlapped NativeOverlapped
+ {
+ get
+ {
+ return _overlapped;
+ }
+ }
+
+ private HttpApiTypes.HTTP_SSL_CLIENT_CERT_INFO* RequestBlob
+ {
+ get
+ {
+ return _memoryBlob;
+ }
+ }
+
+ private void Reset(uint size)
+ {
+ if (size == _size)
+ {
+ return;
+ }
+ if (_size != 0)
+ {
+ _overlapped.Dispose();
+ }
+ _size = size;
+ if (size == 0)
+ {
+ _overlapped = null;
+ _memoryBlob = null;
+ _backingBuffer = null;
+ return;
+ }
+ _backingBuffer = new byte[checked((int)size)];
+ var boundHandle = RequestContext.Server.RequestQueue.BoundHandle;
+ _overlapped = new SafeNativeOverlapped(boundHandle,
+ boundHandle.AllocateNativeOverlapped(IOCallback, this, _backingBuffer));
+ _memoryBlob = (HttpApiTypes.HTTP_SSL_CLIENT_CERT_INFO*)Marshal.UnsafeAddrOfPinnedArrayElement(_backingBuffer, 0);
+ }
+
+ // When you use netsh to configure HTTP.SYS with clientcertnegotiation = enable
+ // which means negotiate client certificates, when the client makes the
+ // initial SSL connection, the server (HTTP.SYS) requests the client certificate.
+ //
+ // Some apps may not want to negotiate the client cert at the beginning,
+ // perhaps serving the default.htm. In this case the HTTP.SYS is configured
+ // with clientcertnegotiation = disabled, which means that the client certificate is
+ // optional so initially when SSL is established HTTP.SYS won't ask for client
+ // certificate. This works fine for the default.htm in the case above,
+ // however, if the app wants to demand a client certificate at a later time
+ // perhaps showing "YOUR ORDERS" page, then the server wants to negotiate
+ // Client certs. This will in turn makes HTTP.SYS to do the
+ // SEC_I_RENOGOTIATE through which the client cert demand is made
+ //
+ // NOTE: When calling HttpReceiveClientCertificate you can get
+ // ERROR_NOT_FOUND - which means the client did not provide the cert
+ // If this is important, the server should respond with 403 forbidden
+ // HTTP.SYS will not do this for you automatically
+ internal Task LoadClientCertificateAsync()
+ {
+ uint size = CertBoblSize;
+ bool retry;
+ do
+ {
+ retry = false;
+ uint bytesReceived = 0;
+
+ uint statusCode =
+ HttpApi.HttpReceiveClientCertificate(
+ RequestQueueHandle,
+ RequestContext.Request.UConnectionId,
+ (uint)HttpApiTypes.HTTP_FLAGS.NONE,
+ RequestBlob,
+ size,
+ &bytesReceived,
+ NativeOverlapped);
+
+ if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA)
+ {
+ HttpApiTypes.HTTP_SSL_CLIENT_CERT_INFO* pClientCertInfo = RequestBlob;
+ size = bytesReceived + pClientCertInfo->CertEncodedSize;
+ Reset(size);
+ retry = true;
+ }
+ else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_NOT_FOUND)
+ {
+ // The client did not send a cert.
+ Complete(0, null);
+ }
+ else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
+ HttpSysListener.SkipIOCPCallbackOnSuccess)
+ {
+ IOCompleted(statusCode, bytesReceived);
+ }
+ else if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
+ statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
+ {
+ // Some other bad error, possible(?) return values are:
+ // ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
+ // Also ERROR_BAD_DATA if we got it twice or it reported smaller size buffer required.
+ Fail(new HttpSysException((int)statusCode));
+ }
+ }
+ while (retry);
+
+ return Task;
+ }
+
+ private void Complete(int certErrors, X509Certificate2 cert)
+ {
+ // May be null
+ _clientCert = cert;
+ _clientCertError = certErrors;
+ Dispose();
+ _tcs.TrySetResult(null);
+ }
+
+ private void Fail(Exception ex)
+ {
+ // TODO: Log
+ _clientCertException = ex;
+ Dispose();
+ _tcs.TrySetResult(null);
+ }
+
+ private unsafe void IOCompleted(uint errorCode, uint numBytes)
+ {
+ IOCompleted(this, errorCode, numBytes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Redirected to callback")]
+ private static unsafe void IOCompleted(ClientCertLoader asyncResult, uint errorCode, uint numBytes)
+ {
+ RequestContext requestContext = asyncResult.RequestContext;
+ try
+ {
+ if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA)
+ {
+ // There is a bug that has existed in http.sys since w2k3. Bytesreceived will only
+ // return the size of the initial cert structure. To get the full size,
+ // we need to add the certificate encoding size as well.
+
+ HttpApiTypes.HTTP_SSL_CLIENT_CERT_INFO* pClientCertInfo = asyncResult.RequestBlob;
+ asyncResult.Reset(numBytes + pClientCertInfo->CertEncodedSize);
+
+ uint bytesReceived = 0;
+ errorCode =
+ HttpApi.HttpReceiveClientCertificate(
+ requestContext.Server.RequestQueue.Handle,
+ requestContext.Request.UConnectionId,
+ (uint)HttpApiTypes.HTTP_FLAGS.NONE,
+ asyncResult._memoryBlob,
+ asyncResult._size,
+ &bytesReceived,
+ asyncResult._overlapped);
+
+ if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING ||
+ (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && !HttpSysListener.SkipIOCPCallbackOnSuccess))
+ {
+ return;
+ }
+ }
+
+ if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_NOT_FOUND)
+ {
+ // The client did not send a cert.
+ asyncResult.Complete(0, null);
+ }
+ else if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
+ {
+ asyncResult.Fail(new HttpSysException((int)errorCode));
+ }
+ else
+ {
+ HttpApiTypes.HTTP_SSL_CLIENT_CERT_INFO* pClientCertInfo = asyncResult._memoryBlob;
+ if (pClientCertInfo == null)
+ {
+ asyncResult.Complete(0, null);
+ }
+ else
+ {
+ if (pClientCertInfo->pCertEncoded != null)
+ {
+ try
+ {
+ byte[] certEncoded = new byte[pClientCertInfo->CertEncodedSize];
+ Marshal.Copy((IntPtr)pClientCertInfo->pCertEncoded, certEncoded, 0, certEncoded.Length);
+ asyncResult.Complete((int)pClientCertInfo->CertFlags, new X509Certificate2(certEncoded));
+ }
+ catch (CryptographicException exception)
+ {
+ // TODO: Log
+ asyncResult.Fail(exception);
+ }
+ catch (SecurityException exception)
+ {
+ // TODO: Log
+ asyncResult.Fail(exception);
+ }
+ }
+ }
+ }
+ }
+ catch (Exception exception)
+ {
+ asyncResult.Fail(exception);
+ }
+ }
+
+ private static unsafe void WaitCallback(uint errorCode, uint numBytes, NativeOverlapped* nativeOverlapped)
+ {
+ var asyncResult = (ClientCertLoader)ThreadPoolBoundHandle.GetNativeOverlappedState(nativeOverlapped);
+ IOCompleted(asyncResult, errorCode, numBytes);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _cancellationRegistration.Dispose();
+ if (_overlapped != null)
+ {
+ _memoryBlob = null;
+ _overlapped.Dispose();
+ }
+ }
+ }
+
+ public object AsyncState
+ {
+ get { return _tcs.Task.AsyncState; }
+ }
+
+ public WaitHandle AsyncWaitHandle
+ {
+ get { return ((IAsyncResult)_tcs.Task).AsyncWaitHandle; }
+ }
+
+ public bool CompletedSynchronously
+ {
+ get { return ((IAsyncResult)_tcs.Task).CompletedSynchronously; }
+ }
+
+ public bool IsCompleted
+ {
+ get { return _tcs.Task.IsCompleted; }
+ }
+
+ internal static unsafe ChannelBinding GetChannelBindingFromTls(RequestQueue requestQueue, ulong connectionId, ILogger logger)
+ {
+ // +128 since a CBT is usually <128 thus we need to call HRCC just once. If the CBT
+ // is >128 we will get ERROR_MORE_DATA and call again
+ int size = RequestChannelBindStatusSize + 128;
+
+ Debug.Assert(size >= 0);
+
+ byte[] blob = null;
+ SafeLocalFreeChannelBinding token = null;
+
+ uint bytesReceived = 0; ;
+ uint statusCode;
+
+ do
+ {
+ blob = new byte[size];
+ fixed (byte* blobPtr = blob)
+ {
+ // Http.sys team: ServiceName will always be null if
+ // HTTP_RECEIVE_SECURE_CHANNEL_TOKEN flag is set.
+ statusCode = HttpApi.HttpReceiveClientCertificate(
+ requestQueue.Handle,
+ connectionId,
+ (uint)HttpApiTypes.HTTP_FLAGS.HTTP_RECEIVE_SECURE_CHANNEL_TOKEN,
+ blobPtr,
+ (uint)size,
+ &bytesReceived,
+ SafeNativeOverlapped.Zero);
+
+ if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
+ {
+ int tokenOffset = GetTokenOffsetFromBlob((IntPtr)blobPtr);
+ int tokenSize = GetTokenSizeFromBlob((IntPtr)blobPtr);
+ Debug.Assert(tokenSize < Int32.MaxValue);
+
+ token = SafeLocalFreeChannelBinding.LocalAlloc(tokenSize);
+
+ Marshal.Copy(blob, tokenOffset, token.DangerousGetHandle(), tokenSize);
+ }
+ else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA)
+ {
+ int tokenSize = GetTokenSizeFromBlob((IntPtr)blobPtr);
+ Debug.Assert(tokenSize < Int32.MaxValue);
+
+ size = RequestChannelBindStatusSize + tokenSize;
+ }
+ else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_INVALID_PARAMETER)
+ {
+ LogHelper.LogError(logger, "GetChannelBindingFromTls", "Channel binding is not supported.");
+ return null; // old schannel library which doesn't support CBT
+ }
+ else
+ {
+ // It's up to the consumer to fail if the missing ChannelBinding matters to them.
+ LogHelper.LogException(logger, "GetChannelBindingFromTls", new HttpSysException((int)statusCode));
+ break;
+ }
+ }
+ }
+ while (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS);
+
+ return token;
+ }
+
+ private static int GetTokenOffsetFromBlob(IntPtr blob)
+ {
+ Debug.Assert(blob != IntPtr.Zero);
+ IntPtr tokenPointer = Marshal.ReadIntPtr(blob, (int)Marshal.OffsetOf<HttpApiTypes.HTTP_REQUEST_CHANNEL_BIND_STATUS>("ChannelToken"));
+ Debug.Assert(tokenPointer != IntPtr.Zero);
+ return (int)IntPtrHelper.Subtract(tokenPointer, blob);
+ }
+
+ private static int GetTokenSizeFromBlob(IntPtr blob)
+ {
+ Debug.Assert(blob != IntPtr.Zero);
+ return Marshal.ReadInt32(blob, (int)Marshal.OffsetOf<HttpApiTypes.HTTP_REQUEST_CHANNEL_BIND_STATUS>("ChannelTokenSize"));
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/HttpReasonPhrase.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/HttpReasonPhrase.cs
new file mode 100644
index 0000000000..e8781b689d
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/HttpReasonPhrase.cs
@@ -0,0 +1,101 @@
+// 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.Server.HttpSys
+{
+ internal static class HttpReasonPhrase
+ {
+ private static readonly string[][] HttpReasonPhrases = new string[][]
+ {
+ null,
+
+ new string[]
+ {
+ /* 100 */ "Continue",
+ /* 101 */ "Switching Protocols",
+ /* 102 */ "Processing"
+ },
+
+ new string[]
+ {
+ /* 200 */ "OK",
+ /* 201 */ "Created",
+ /* 202 */ "Accepted",
+ /* 203 */ "Non-Authoritative Information",
+ /* 204 */ "No Content",
+ /* 205 */ "Reset Content",
+ /* 206 */ "Partial Content",
+ /* 207 */ "Multi-Status"
+ },
+
+ new string[]
+ {
+ /* 300 */ "Multiple Choices",
+ /* 301 */ "Moved Permanently",
+ /* 302 */ "Found",
+ /* 303 */ "See Other",
+ /* 304 */ "Not Modified",
+ /* 305 */ "Use Proxy",
+ /* 306 */ null,
+ /* 307 */ "Temporary Redirect"
+ },
+
+ new string[]
+ {
+ /* 400 */ "Bad Request",
+ /* 401 */ "Unauthorized",
+ /* 402 */ "Payment Required",
+ /* 403 */ "Forbidden",
+ /* 404 */ "Not Found",
+ /* 405 */ "Method Not Allowed",
+ /* 406 */ "Not Acceptable",
+ /* 407 */ "Proxy Authentication Required",
+ /* 408 */ "Request Timeout",
+ /* 409 */ "Conflict",
+ /* 410 */ "Gone",
+ /* 411 */ "Length Required",
+ /* 412 */ "Precondition Failed",
+ /* 413 */ "Request Entity Too Large",
+ /* 414 */ "Request-Uri Too Long",
+ /* 415 */ "Unsupported Media Type",
+ /* 416 */ "Requested Range Not Satisfiable",
+ /* 417 */ "Expectation Failed",
+ /* 418 */ null,
+ /* 419 */ null,
+ /* 420 */ null,
+ /* 421 */ null,
+ /* 422 */ "Unprocessable Entity",
+ /* 423 */ "Locked",
+ /* 424 */ "Failed Dependency",
+ /* 425 */ null,
+ /* 426 */ "Upgrade Required", // RFC 2817
+ },
+
+ new string[]
+ {
+ /* 500 */ "Internal Server Error",
+ /* 501 */ "Not Implemented",
+ /* 502 */ "Bad Gateway",
+ /* 503 */ "Service Unavailable",
+ /* 504 */ "Gateway Timeout",
+ /* 505 */ "Http Version Not Supported",
+ /* 506 */ null,
+ /* 507 */ "Insufficient Storage"
+ }
+ };
+
+ internal static string Get(int code)
+ {
+ if (code >= 100 && code < 600)
+ {
+ int i = code / 100;
+ int j = code % 100;
+ if (j < HttpReasonPhrases[i].Length)
+ {
+ return HttpReasonPhrases[i][j];
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/OpaqueStream.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/OpaqueStream.cs
new file mode 100644
index 0000000000..0bd9bba848
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/OpaqueStream.cs
@@ -0,0 +1,165 @@
+// 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 System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ // A duplex wrapper around RequestStream and ResponseStream.
+ // TODO: Consider merging RequestStream and ResponseStream instead.
+ internal class OpaqueStream : Stream
+ {
+ private readonly Stream _requestStream;
+ private readonly Stream _responseStream;
+
+ internal OpaqueStream(Stream requestStream, Stream responseStream)
+ {
+ _requestStream = requestStream;
+ _responseStream = responseStream;
+ }
+
+#region Properties
+
+ public override bool CanRead
+ {
+ get { return _requestStream.CanRead; }
+ }
+
+ public override bool CanSeek
+ {
+ get { return false; }
+ }
+
+ public override bool CanTimeout
+ {
+ get { return _requestStream.CanTimeout || _responseStream.CanTimeout; }
+ }
+
+ public override bool CanWrite
+ {
+ get { return _responseStream.CanWrite; }
+ }
+
+ public override long Length
+ {
+ get { throw new NotSupportedException(Resources.Exception_NoSeek); }
+ }
+
+ public override long Position
+ {
+ get { throw new NotSupportedException(Resources.Exception_NoSeek); }
+ set { throw new NotSupportedException(Resources.Exception_NoSeek); }
+ }
+
+ public override int ReadTimeout
+ {
+ get { return _requestStream.ReadTimeout; }
+ set { _requestStream.ReadTimeout = value; }
+ }
+
+ public override int WriteTimeout
+ {
+ get { return _responseStream.WriteTimeout; }
+ set { _responseStream.WriteTimeout = value; }
+ }
+
+#endregion Properties
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException(Resources.Exception_NoSeek);
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException(Resources.Exception_NoSeek);
+ }
+
+#region Read
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ return _requestStream.Read(buffer, offset, count);
+ }
+
+ public override int ReadByte()
+ {
+ return _requestStream.ReadByte();
+ }
+
+ public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ return _requestStream.BeginRead(buffer, offset, count, callback, state);
+ }
+
+ public override int EndRead(IAsyncResult asyncResult)
+ {
+ return _requestStream.EndRead(asyncResult);
+ }
+
+ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ return _requestStream.ReadAsync(buffer, offset, count, cancellationToken);
+ }
+
+ public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
+ {
+ return _requestStream.CopyToAsync(destination, bufferSize, cancellationToken);
+ }
+
+#endregion Read
+
+#region Write
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ _responseStream.Write(buffer, offset, count);
+ }
+
+ public override void WriteByte(byte value)
+ {
+ _responseStream.WriteByte(value);
+ }
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ return _responseStream.BeginWrite(buffer, offset, count, callback, state);
+ }
+
+ public override void EndWrite(IAsyncResult asyncResult)
+ {
+ _responseStream.EndWrite(asyncResult);
+ }
+
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ return _responseStream.WriteAsync(buffer, offset, count, cancellationToken);
+ }
+
+ public override void Flush()
+ {
+ _responseStream.Flush();
+ }
+
+ public override Task FlushAsync(CancellationToken cancellationToken)
+ {
+ return _responseStream.FlushAsync(cancellationToken);
+ }
+
+#endregion Write
+
+ protected override void Dispose(bool disposing)
+ {
+ // TODO: Suppress dispose?
+ if (disposing)
+ {
+ _requestStream.Dispose();
+ _responseStream.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/Request.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/Request.cs
new file mode 100644
index 0000000000..3dedf347a9
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/Request.cs
@@ -0,0 +1,344 @@
+// 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.Globalization;
+using System.IO;
+using System.Net;
+using System.Security.Cryptography.X509Certificates;
+using System.Security.Principal;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.HttpSys.Internal;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal sealed class Request
+ {
+ private NativeRequestContext _nativeRequestContext;
+
+ private X509Certificate2 _clientCert;
+ // TODO: https://github.com/aspnet/HttpSysServer/issues/231
+ // private byte[] _providedTokenBindingId;
+ // private byte[] _referredTokenBindingId;
+
+ private BoundaryType _contentBoundaryType;
+
+ private long? _contentLength;
+ private RequestStream _nativeStream;
+
+ private AspNetCore.HttpSys.Internal.SocketAddress _localEndPoint;
+ private AspNetCore.HttpSys.Internal.SocketAddress _remoteEndPoint;
+
+ private bool _isDisposed = false;
+
+ internal Request(RequestContext requestContext, NativeRequestContext nativeRequestContext)
+ {
+ // TODO: Verbose log
+ RequestContext = requestContext;
+ _nativeRequestContext = nativeRequestContext;
+ _contentBoundaryType = BoundaryType.None;
+
+ RequestId = nativeRequestContext.RequestId;
+ UConnectionId = nativeRequestContext.ConnectionId;
+ SslStatus = nativeRequestContext.SslStatus;
+
+ KnownMethod = nativeRequestContext.VerbId;
+ Method = _nativeRequestContext.GetVerb();
+
+ RawUrl = nativeRequestContext.GetRawUrl();
+
+ var cookedUrl = nativeRequestContext.GetCookedUrl();
+ QueryString = cookedUrl.GetQueryString() ?? string.Empty;
+
+ var prefix = requestContext.Server.Options.UrlPrefixes.GetPrefix((int)nativeRequestContext.UrlContext);
+
+ var rawUrlInBytes = _nativeRequestContext.GetRawUrlInBytes();
+ var originalPath = RequestUriBuilder.DecodeAndUnescapePath(rawUrlInBytes);
+
+ // 'OPTIONS * HTTP/1.1'
+ if (KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbOPTIONS && string.Equals(RawUrl, "*", StringComparison.Ordinal))
+ {
+ PathBase = string.Empty;
+ Path = string.Empty;
+ }
+ // These paths are both unescaped already.
+ else if (originalPath.Length == prefix.Path.Length - 1)
+ {
+ // They matched exactly except for the trailing slash.
+ PathBase = originalPath;
+ Path = string.Empty;
+ }
+ else
+ {
+ // url: /base/path, prefix: /base/, base: /base, path: /path
+ // url: /, prefix: /, base: , path: /
+ PathBase = originalPath.Substring(0, prefix.Path.Length - 1);
+ Path = originalPath.Substring(prefix.Path.Length - 1);
+ }
+
+ ProtocolVersion = _nativeRequestContext.GetVersion();
+
+ Headers = new RequestHeaders(_nativeRequestContext);
+
+ User = _nativeRequestContext.GetUser();
+
+ // GetTlsTokenBindingInfo(); TODO: https://github.com/aspnet/HttpSysServer/issues/231
+
+ // Finished directly accessing the HTTP_REQUEST structure.
+ _nativeRequestContext.ReleasePins();
+ // TODO: Verbose log parameters
+ }
+
+ internal ulong UConnectionId { get; }
+
+ // No ulongs in public APIs...
+ public long ConnectionId => (long)UConnectionId;
+
+ internal ulong RequestId { get; }
+
+ private SslStatus SslStatus { get; }
+
+ private RequestContext RequestContext { get; }
+
+ // With the leading ?, if any
+ public string QueryString { get; }
+
+ public long? ContentLength
+ {
+ get
+ {
+ if (_contentBoundaryType == BoundaryType.None)
+ {
+ string transferEncoding = Headers[HttpKnownHeaderNames.TransferEncoding];
+ if (string.Equals("chunked", transferEncoding?.Trim(), StringComparison.OrdinalIgnoreCase))
+ {
+ _contentBoundaryType = BoundaryType.Chunked;
+ }
+ else
+ {
+ string length = Headers[HttpKnownHeaderNames.ContentLength];
+ long value;
+ if (length != null && long.TryParse(length.Trim(), NumberStyles.None,
+ CultureInfo.InvariantCulture.NumberFormat, out value))
+ {
+ _contentBoundaryType = BoundaryType.ContentLength;
+ _contentLength = value;
+ }
+ else
+ {
+ _contentBoundaryType = BoundaryType.Invalid;
+ }
+ }
+ }
+
+ return _contentLength;
+ }
+ }
+
+ public RequestHeaders Headers { get; }
+
+ internal HttpApiTypes.HTTP_VERB KnownMethod { get; }
+
+ internal bool IsHeadMethod => KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbHEAD;
+
+ public string Method { get; }
+
+ public Stream Body => EnsureRequestStream() ?? Stream.Null;
+
+ private RequestStream EnsureRequestStream()
+ {
+ if (_nativeStream == null && HasEntityBody)
+ {
+ _nativeStream = new RequestStream(RequestContext);
+ }
+ return _nativeStream;
+ }
+
+ public bool HasRequestBodyStarted => _nativeStream?.HasStarted ?? false;
+
+ public long? MaxRequestBodySize
+ {
+ get => EnsureRequestStream()?.MaxSize;
+ set
+ {
+ EnsureRequestStream();
+ if (_nativeStream != null)
+ {
+ _nativeStream.MaxSize = value;
+ }
+ }
+ }
+
+ public string PathBase { get; }
+
+ public string Path { get; }
+
+ public bool IsHttps => SslStatus != SslStatus.Insecure;
+
+ public string RawUrl { get; }
+
+ public Version ProtocolVersion { get; }
+
+ public bool HasEntityBody
+ {
+ get
+ {
+ // accessing the ContentLength property delay creates _contentBoundaryType
+ return (ContentLength.HasValue && ContentLength.Value > 0 && _contentBoundaryType == BoundaryType.ContentLength)
+ || _contentBoundaryType == BoundaryType.Chunked;
+ }
+ }
+
+ private AspNetCore.HttpSys.Internal.SocketAddress RemoteEndPoint
+ {
+ get
+ {
+ if (_remoteEndPoint == null)
+ {
+ _remoteEndPoint = _nativeRequestContext.GetRemoteEndPoint();
+ }
+
+ return _remoteEndPoint;
+ }
+ }
+
+ private AspNetCore.HttpSys.Internal.SocketAddress LocalEndPoint
+ {
+ get
+ {
+ if (_localEndPoint == null)
+ {
+ _localEndPoint = _nativeRequestContext.GetLocalEndPoint();
+ }
+
+ return _localEndPoint;
+ }
+ }
+
+ // TODO: Lazy cache?
+ public IPAddress RemoteIpAddress => RemoteEndPoint.GetIPAddress();
+
+ public IPAddress LocalIpAddress => LocalEndPoint.GetIPAddress();
+
+ public int RemotePort => RemoteEndPoint.GetPort();
+
+ public int LocalPort => LocalEndPoint.GetPort();
+
+ public string Scheme => IsHttps ? Constants.HttpsScheme : Constants.HttpScheme;
+
+ // HTTP.Sys allows you to upgrade anything to opaque unless content-length > 0 or chunked are specified.
+ internal bool IsUpgradable => !HasEntityBody && ComNetOS.IsWin8orLater;
+
+ internal WindowsPrincipal User { get; }
+
+ // Populates the client certificate. The result may be null if there is no client cert.
+ // TODO: Does it make sense for this to be invoked multiple times (e.g. renegotiate)? Client and server code appear to
+ // enable this, but it's unclear what Http.Sys would do.
+ public async Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (SslStatus == SslStatus.Insecure)
+ {
+ // Non-SSL
+ return null;
+ }
+ // TODO: Verbose log
+ if (_clientCert != null)
+ {
+ return _clientCert;
+ }
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var certLoader = new ClientCertLoader(RequestContext, cancellationToken);
+ try
+ {
+ await certLoader.LoadClientCertificateAsync().SupressContext();
+ // Populate the environment.
+ if (certLoader.ClientCert != null)
+ {
+ _clientCert = certLoader.ClientCert;
+ }
+ // TODO: Expose errors and exceptions?
+ }
+ catch (Exception)
+ {
+ if (certLoader != null)
+ {
+ certLoader.Dispose();
+ }
+ throw;
+ }
+ return _clientCert;
+ }
+ /* TODO: https://github.com/aspnet/WebListener/issues/231
+ private byte[] GetProvidedTokenBindingId()
+ {
+ return _providedTokenBindingId;
+ }
+
+ private byte[] GetReferredTokenBindingId()
+ {
+ return _referredTokenBindingId;
+ }
+ */
+ // Only call from the constructor so we can directly access the native request blob.
+ // This requires Windows 10 and the following reg key:
+ // Set Key: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\HTTP\Parameters to Value: EnableSslTokenBinding = 1 [DWORD]
+ // Then for IE to work you need to set these:
+ // Key: HKLM\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_TOKEN_BINDING
+ // Value: "iexplore.exe"=dword:0x00000001
+ // Key: HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_TOKEN_BINDING
+ // Value: "iexplore.exe"=dword:00000001
+ // TODO: https://github.com/aspnet/WebListener/issues/231
+ // TODO: https://github.com/aspnet/WebListener/issues/204 Move to NativeRequestContext
+ /*
+ private unsafe void GetTlsTokenBindingInfo()
+ {
+ var nativeRequest = (HttpApi.HTTP_REQUEST_V2*)_nativeRequestContext.RequestBlob;
+ for (int i = 0; i < nativeRequest->RequestInfoCount; i++)
+ {
+ var pThisInfo = &nativeRequest->pRequestInfo[i];
+ if (pThisInfo->InfoType == HttpApi.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeSslTokenBinding)
+ {
+ var pTokenBindingInfo = (HttpApi.HTTP_REQUEST_TOKEN_BINDING_INFO*)pThisInfo->pInfo;
+ _providedTokenBindingId = TokenBindingUtil.GetProvidedTokenIdFromBindingInfo(pTokenBindingInfo, out _referredTokenBindingId);
+ }
+ }
+ }
+ */
+ internal uint GetChunks(ref int dataChunkIndex, ref uint dataChunkOffset, byte[] buffer, int offset, int size)
+ {
+ return _nativeRequestContext.GetChunks(ref dataChunkIndex, ref dataChunkOffset, buffer, offset, size);
+ }
+
+ // should only be called from RequestContext
+ internal void Dispose()
+ {
+ // TODO: Verbose log
+ _isDisposed = true;
+ _nativeRequestContext.Dispose();
+ (User?.Identity as WindowsIdentity)?.Dispose();
+ if (_nativeStream != null)
+ {
+ _nativeStream.Dispose();
+ }
+ }
+
+ private void CheckDisposed()
+ {
+ if (_isDisposed)
+ {
+ throw new ObjectDisposedException(this.GetType().FullName);
+ }
+ }
+
+ internal void SwitchToOpaqueMode()
+ {
+ if (_nativeStream == null)
+ {
+ _nativeStream = new RequestStream(RequestContext);
+ }
+ _nativeStream.SwitchToOpaqueMode();
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestContext.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestContext.cs
new file mode 100644
index 0000000000..89405cf538
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestContext.cs
@@ -0,0 +1,222 @@
+// 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.IO;
+using System.Security.Authentication.ExtendedProtection;
+using System.Security.Claims;
+using System.Security.Principal;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal sealed class RequestContext : IDisposable
+ {
+ private static readonly Action<object> AbortDelegate = Abort;
+
+ private NativeRequestContext _memoryBlob;
+ private CancellationTokenSource _requestAbortSource;
+ private CancellationToken? _disconnectToken;
+ private bool _disposed;
+
+ internal RequestContext(HttpSysListener server, NativeRequestContext memoryBlob)
+ {
+ // TODO: Verbose log
+ Server = server;
+ _memoryBlob = memoryBlob;
+ Request = new Request(this, _memoryBlob);
+ Response = new Response(this);
+ AllowSynchronousIO = server.Options.AllowSynchronousIO;
+ }
+
+ internal HttpSysListener Server { get; }
+
+ internal ILogger Logger => Server.Logger;
+
+ public Request Request { get; }
+
+ public Response Response { get; }
+
+ public WindowsPrincipal User => Request.User;
+
+ public CancellationToken DisconnectToken
+ {
+ get
+ {
+ // Create a new token per request, but link it to a single connection token.
+ // We need to be able to dispose of the registrations each request to prevent leaks.
+ if (!_disconnectToken.HasValue)
+ {
+ if (_disposed || Response.BodyIsFinished)
+ {
+ // We cannot register for disconnect notifications after the response has finished sending.
+ _disconnectToken = CancellationToken.None;
+ }
+ else
+ {
+ var connectionDisconnectToken = Server.DisconnectListener.GetTokenForConnection(Request.UConnectionId);
+
+ if (connectionDisconnectToken.CanBeCanceled)
+ {
+ _requestAbortSource = CancellationTokenSource.CreateLinkedTokenSource(connectionDisconnectToken);
+ _disconnectToken = _requestAbortSource.Token;
+ }
+ else
+ {
+ _disconnectToken = CancellationToken.None;
+ }
+ }
+ }
+ return _disconnectToken.Value;
+ }
+ }
+
+ public unsafe Guid TraceIdentifier
+ {
+ get
+ {
+ // This is the base GUID used by HTTP.SYS for generating the activity ID.
+ // HTTP.SYS overwrites the first 8 bytes of the base GUID with RequestId to generate ETW activity ID.
+ var guid = new Guid(0xffcb4c93, 0xa57f, 0x453c, 0xb6, 0x3f, 0x84, 0x71, 0xc, 0x79, 0x67, 0xbb);
+ *((ulong*)&guid) = Request.RequestId;
+ return guid;
+ }
+ }
+
+ public bool IsUpgradableRequest => Request.IsUpgradable;
+
+ internal bool AllowSynchronousIO { get; set; }
+
+ public Task<Stream> UpgradeAsync()
+ {
+ if (!IsUpgradableRequest)
+ {
+ throw new InvalidOperationException("This request cannot be upgraded, it is incompatible.");
+ }
+ if (Response.HasStarted)
+ {
+ throw new InvalidOperationException("This request cannot be upgraded, the response has already started.");
+ }
+
+ // Set the status code and reason phrase
+ Response.StatusCode = StatusCodes.Status101SwitchingProtocols;
+ Response.ReasonPhrase = HttpReasonPhrase.Get(StatusCodes.Status101SwitchingProtocols);
+
+ Response.SendOpaqueUpgrade(); // TODO: Async
+ Request.SwitchToOpaqueMode();
+ Response.SwitchToOpaqueMode();
+ var opaqueStream = new OpaqueStream(Request.Body, Response.Body);
+ return Task.FromResult<Stream>(opaqueStream);
+ }
+
+ // TODO: Public when needed
+ internal bool TryGetChannelBinding(ref ChannelBinding value)
+ {
+ if (!Request.IsHttps)
+ {
+ LogHelper.LogDebug(Logger, "TryGetChannelBinding", "Channel binding requires HTTPS.");
+ return false;
+ }
+
+ value = ClientCertLoader.GetChannelBindingFromTls(Server.RequestQueue, Request.UConnectionId, Logger);
+
+ Debug.Assert(value != null, "GetChannelBindingFromTls returned null even though OS supposedly supports Extended Protection");
+ LogHelper.LogInfo(Logger, "Channel binding retrieved.");
+ return value != null;
+ }
+
+ /// <summary>
+ /// Flushes and completes the response.
+ /// </summary>
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+ _disposed = true;
+
+ // TODO: Verbose log
+ try
+ {
+ _requestAbortSource?.Dispose();
+ Response.Dispose();
+ }
+ catch
+ {
+ Abort();
+ }
+ finally
+ {
+ Request.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Forcibly terminate and dispose the request, closing the connection if necessary.
+ /// </summary>
+ public void Abort()
+ {
+ // May be called from Dispose() code path, don't check _disposed.
+ // TODO: Verbose log
+ _disposed = true;
+ if (_requestAbortSource != null)
+ {
+ try
+ {
+ _requestAbortSource.Cancel();
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ catch (Exception ex)
+ {
+ LogHelper.LogDebug(Logger, "Abort", ex);
+ }
+ _requestAbortSource.Dispose();
+ }
+ ForceCancelRequest();
+ Request.Dispose();
+ // Only Abort, Response.Dispose() tries a graceful flush
+ Response.Abort();
+ }
+
+ private static void Abort(object state)
+ {
+ var context = (RequestContext)state;
+ context.Abort();
+ }
+
+ internal CancellationTokenRegistration RegisterForCancellation(CancellationToken cancellationToken)
+ {
+ return cancellationToken.Register(AbortDelegate, this);
+ }
+
+ // The request is being aborted, but large writes may be in progress. Cancel them.
+ internal void ForceCancelRequest()
+ {
+ try
+ {
+ var statusCode = HttpApi.HttpCancelHttpRequest(Server.RequestQueue.Handle,
+ Request.RequestId, IntPtr.Zero);
+
+ // Either the connection has already dropped, or the last write is in progress.
+ // The requestId becomes invalid as soon as the last Content-Length write starts.
+ // The only way to cancel now is with CancelIoEx.
+ if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_CONNECTION_INVALID)
+ {
+ Response.CancelLastWrite();
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ // RequestQueueHandle may have been closed
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestHeaders.Generated.tt b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestHeaders.Generated.tt
new file mode 100644
index 0000000000..1b4b3a177a
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestHeaders.Generated.tt
@@ -0,0 +1,216 @@
+<#@ template language="C#" #>
+<#@ assembly name="System.Core.dll" #>
+<#@ import namespace="System.Linq" #>
+<#
+var props = new[]
+{
+ new { Key = "Accept", Name = "Accept", ID = "HttpSysRequestHeader.Accept" },
+ new { Key = "Accept-Charset", Name = "AcceptCharset", ID = "HttpSysRequestHeader.AcceptCharset" },
+ new { Key = "Accept-Encoding", Name = "AcceptEncoding", ID = "HttpSysRequestHeader.AcceptEncoding" },
+ new { Key = "Accept-Language", Name = "AcceptLanguage", ID = "HttpSysRequestHeader.AcceptLanguage" },
+ new { Key = "Allow", Name = "Allow", ID = "HttpSysRequestHeader.Allow" },
+ new { Key = "Authorization", Name = "Authorization", ID = "HttpSysRequestHeader.Authorization" },
+ new { Key = "Cache-Control", Name = "CacheControl", ID = "HttpSysRequestHeader.CacheControl" },
+ new { Key = "Connection", Name = "Connection", ID = "HttpSysRequestHeader.Connection" },
+ new { Key = "Content-Encoding", Name = "ContentEncoding", ID = "HttpSysRequestHeader.ContentEncoding" },
+ new { Key = "Content-Language", Name = "ContentLanguage", ID = "HttpSysRequestHeader.ContentLanguage" },
+ new { Key = "Content-Length", Name = "ContentLength", ID = "HttpSysRequestHeader.ContentLength" },
+ new { Key = "Content-Location", Name = "ContentLocation", ID = "HttpSysRequestHeader.ContentLocation" },
+ new { Key = "Content-Md5", Name = "ContentMd5", ID = "HttpSysRequestHeader.ContentMd5" },
+ new { Key = "Content-Range", Name = "ContentRange", ID = "HttpSysRequestHeader.ContentRange" },
+ new { Key = "Content-Type", Name = "ContentType", ID = "HttpSysRequestHeader.ContentType" },
+ new { Key = "Cookie", Name = "Cookie", ID = "HttpSysRequestHeader.Cookie" },
+ new { Key = "Date", Name = "Date", ID = "HttpSysRequestHeader.Date" },
+ new { Key = "Expect", Name = "Expect", ID = "HttpSysRequestHeader.Expect" },
+ new { Key = "Expires", Name = "Expires", ID = "HttpSysRequestHeader.Expires" },
+ new { Key = "From", Name = "From", ID = "HttpSysRequestHeader.From" },
+ new { Key = "Host", Name = "Host", ID = "HttpSysRequestHeader.Host" },
+ new { Key = "If-Match", Name = "IfMatch", ID = "HttpSysRequestHeader.IfMatch" },
+ new { Key = "If-Modified-Since", Name = "IfModifiedSince", ID = "HttpSysRequestHeader.IfModifiedSince" },
+ new { Key = "If-None-Match", Name = "IfNoneMatch", ID = "HttpSysRequestHeader.IfNoneMatch" },
+ new { Key = "If-Range", Name = "IfRange", ID = "HttpSysRequestHeader.IfRange" },
+ new { Key = "If-Unmodified-Since", Name = "IfUnmodifiedSince", ID = "HttpSysRequestHeader.IfUnmodifiedSince" },
+ new { Key = "Keep-Alive", Name = "KeepAlive", ID = "HttpSysRequestHeader.KeepAlive" },
+ new { Key = "Last-Modified", Name = "LastModified", ID = "HttpSysRequestHeader.LastModified" },
+ new { Key = "Max-Forwards", Name = "MaxForwards", ID = "HttpSysRequestHeader.MaxForwards" },
+ new { Key = "Pragma", Name = "Pragma", ID = "HttpSysRequestHeader.Pragma" },
+ new { Key = "Proxy-Authorization", Name = "ProxyAuthorization", ID = "HttpSysRequestHeader.ProxyAuthorization" },
+ new { Key = "Range", Name = "Range", ID = "HttpSysRequestHeader.Range" },
+ new { Key = "Referer", Name = "Referer", ID = "HttpSysRequestHeader.Referer" },
+ new { Key = "Te", Name = "Te", ID = "HttpSysRequestHeader.Te" },
+ new { Key = "Trailer", Name = "Trailer", ID = "HttpSysRequestHeader.Trailer" },
+ new { Key = "Transfer-Encoding", Name = "TransferEncoding", ID = "HttpSysRequestHeader.TransferEncoding" },
+ new { Key = "Translate", Name = "Translate", ID = "HttpSysRequestHeader.Translate" },
+ new { Key = "Upgrade", Name = "Upgrade", ID = "HttpSysRequestHeader.Upgrade" },
+ new { Key = "User-Agent", Name = "UserAgent", ID = "HttpSysRequestHeader.UserAgent" },
+ new { Key = "Via", Name = "Via", ID = "HttpSysRequestHeader.Via" },
+ new { Key = "Warning", Name = "Warning", ID = "HttpSysRequestHeader.Warning" },
+}.Select((prop, Index)=>new {prop.Key, prop.Name, prop.ID, Index});
+
+var lengths = props.GroupBy(prop=>prop.Key.Length).OrderBy(prop=>prop.Key);
+
+
+Func<int,string> IsRead = Index => "((_flag" + (Index / 32) + " & 0x" + (1<<(Index % 32)).ToString("x") + "u) != 0)";
+Func<int,string> MarkRead = Index => "_flag" + (Index / 32) + " |= 0x" + (1<<(Index % 32)).ToString("x") + "u";
+Func<int,string> Clear = Index => "_flag" + (Index / 32) + " &= ~0x" + (1<<(Index % 32)).ToString("x") + "u";
+#>
+// 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.
+// <auto-generated />
+
+using System;
+using System.CodeDom.Compiler;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ [GeneratedCode("TextTemplatingFileGenerator", "")]
+ internal partial class RequestHeaders
+ {
+ // Tracks if individual fields have been read from native or set directly.
+ // Once read or set, their presence in the collection is marked by if their StringValues is null or not.
+ private UInt32 _flag0, _flag1;
+
+<# foreach(var prop in props) { #>
+ private StringValues _<#=prop.Name#>;
+<# } #>
+
+<# foreach(var prop in props) { #>
+ internal StringValues <#=prop.Name#>
+ {
+ get
+ {
+ if (!<#=IsRead(prop.Index)#>)
+ {
+ string nativeValue = GetKnownHeader(<#=prop.ID#>);
+ if (nativeValue != null)
+ {
+ _<#=prop.Name#> = nativeValue;
+ }
+ <#=MarkRead(prop.Index)#>;
+ }
+ return _<#=prop.Name#>;
+ }
+ set
+ {
+ <#=MarkRead(prop.Index)#>;
+ _<#=prop.Name#> = value;
+ }
+ }
+
+<# } #>
+ private bool PropertiesContainsKey(string key)
+ {
+ switch (key.Length)
+ {
+<# foreach(var length in lengths) { #>
+ case <#=length.Key#>:
+<# foreach(var prop in length) { #>
+ if (string.Equals(key, "<#=prop.Key#>", StringComparison.OrdinalIgnoreCase))
+ {
+ return <#=prop.Name#>.Count > 0;
+ }
+<# } #>
+ break;
+<# } #>
+ }
+ return false;
+ }
+
+ private bool PropertiesTryGetValue(string key, out StringValues value)
+ {
+ switch (key.Length)
+ {
+<# foreach(var length in lengths) { #>
+ case <#=length.Key#>:
+<# foreach(var prop in length) { #>
+ if (string.Equals(key, "<#=prop.Key#>", StringComparison.OrdinalIgnoreCase))
+ {
+ value = <#=prop.Name#>;
+ return value.Count > 0;
+ }
+<# } #>
+ break;
+<# } #>
+ }
+ value = StringValues.Empty;
+ return false;
+ }
+
+ private bool PropertiesTrySetValue(string key, StringValues value)
+ {
+ switch (key.Length)
+ {
+<# foreach(var length in lengths) { #>
+ case <#=length.Key#>:
+<# foreach(var prop in length) { #>
+ if (string.Equals(key, "<#=prop.Key#>", StringComparison.OrdinalIgnoreCase))
+ {
+ <#=MarkRead(prop.Index)#>;
+ <#=prop.Name#> = value;
+ return true;
+ }
+<# } #>
+ break;
+<# } #>
+ }
+ return false;
+ }
+
+ private bool PropertiesTryRemove(string key)
+ {
+ switch (key.Length)
+ {
+<# foreach(var length in lengths) { #>
+ case <#=length.Key#>:
+<# foreach(var prop in length) { #>
+ if (_<#=prop.Name#>.Count > 0
+ && string.Equals(key, "<#=prop.Key#>", StringComparison.Ordinal))
+ {
+ bool wasSet = <#=IsRead(prop.Index)#>;
+ <#=prop.Name#> = StringValues.Empty;
+ return wasSet;
+ }
+<# } #>
+ break;
+<# } #>
+ }
+ return false;
+ }
+
+ private IEnumerable<string> PropertiesKeys()
+ {
+<# foreach(var prop in props) { #>
+ if (<#=prop.Name#>.Count > 0)
+ {
+ yield return "<#=prop.Key#>";
+ }
+<# } #>
+ }
+
+ private IEnumerable<StringValues> PropertiesValues()
+ {
+<# foreach(var prop in props) { #>
+ if (<#=prop.Name#>.Count > 0)
+ {
+ yield return <#=prop.Name#>;
+ }
+<# } #>
+ }
+
+ private IEnumerable<KeyValuePair<string, StringValues>> PropertiesEnumerable()
+ {
+<# foreach(var prop in props) { #>
+ if (<#=prop.Name#>.Count > 0)
+ {
+ yield return new KeyValuePair<string, StringValues>("<#=prop.Key#>", <#=prop.Name#>);
+ }
+<# } #>
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStream.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStream.cs
new file mode 100644
index 0000000000..f0bf45d68f
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStream.cs
@@ -0,0 +1,472 @@
+// 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.Globalization;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal class RequestStream : Stream
+ {
+ private const int MaxReadSize = 0x20000; // http.sys recommends we limit reads to 128k
+
+ private RequestContext _requestContext;
+ private uint _dataChunkOffset;
+ private int _dataChunkIndex;
+ private long? _maxSize;
+ private long _totalRead;
+ private bool _closed;
+
+ internal RequestStream(RequestContext httpContext)
+ {
+ _requestContext = httpContext;
+ _maxSize = _requestContext.Server.Options.MaxRequestBodySize;
+ }
+
+ internal RequestContext RequestContext
+ {
+ get { return _requestContext; }
+ }
+
+ private SafeHandle RequestQueueHandle => RequestContext.Server.RequestQueue.Handle;
+
+ private ulong RequestId => RequestContext.Request.RequestId;
+
+ private ILogger Logger => RequestContext.Server.Logger;
+
+ public bool HasStarted { get; private set; }
+
+ public long? MaxSize
+ {
+ get => _maxSize;
+ set
+ {
+ if (HasStarted)
+ {
+ throw new InvalidOperationException("The maximum request size cannot be changed after the request body has started reading.");
+ }
+ if (value.HasValue && value < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), value, "The value must be greater or equal to zero.");
+ }
+ _maxSize = value;
+ }
+ }
+
+ public override bool CanSeek => false;
+
+ public override bool CanWrite => false;
+
+ public override bool CanRead => true;
+
+ public override long Length => throw new NotSupportedException(Resources.Exception_NoSeek);
+
+ public override long Position
+ {
+ get => throw new NotSupportedException(Resources.Exception_NoSeek);
+ set => throw new NotSupportedException(Resources.Exception_NoSeek);
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ => throw new NotSupportedException(Resources.Exception_NoSeek);
+
+ public override void SetLength(long value) => throw new NotSupportedException(Resources.Exception_NoSeek);
+
+ public override void Flush() => throw new InvalidOperationException(Resources.Exception_ReadOnlyStream);
+
+ public override Task FlushAsync(CancellationToken cancellationToken)
+ => throw new InvalidOperationException(Resources.Exception_ReadOnlyStream);
+
+ internal void SwitchToOpaqueMode()
+ {
+ HasStarted = true;
+ _maxSize = null;
+ }
+
+ internal void Abort()
+ {
+ _closed = true;
+ _requestContext.Abort();
+ }
+
+ private void ValidateReadBuffer(byte[] buffer, int offset, int size)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+ if (offset < 0 || offset > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException("offset", offset, string.Empty);
+ }
+ if (size <= 0 || size > buffer.Length - offset)
+ {
+ throw new ArgumentOutOfRangeException("size", size, string.Empty);
+ }
+ }
+
+ public override unsafe int Read([In, Out] byte[] buffer, int offset, int size)
+ {
+ if (!RequestContext.AllowSynchronousIO)
+ {
+ throw new InvalidOperationException("Synchronous IO APIs are disabled, see AllowSynchronousIO.");
+ }
+
+ ValidateReadBuffer(buffer, offset, size);
+ CheckSizeLimit();
+ if (_closed)
+ {
+ return 0;
+ }
+ // TODO: Verbose log parameters
+
+ uint dataRead = 0;
+
+ if (_dataChunkIndex != -1)
+ {
+ dataRead = _requestContext.Request.GetChunks(ref _dataChunkIndex, ref _dataChunkOffset, buffer, offset, size);
+ }
+
+ if (_dataChunkIndex == -1 && dataRead == 0)
+ {
+ uint statusCode = 0;
+ uint extraDataRead = 0;
+
+ // the http.sys team recommends that we limit the size to 128kb
+ if (size > MaxReadSize)
+ {
+ size = MaxReadSize;
+ }
+
+ fixed (byte* pBuffer = buffer)
+ {
+ // issue unmanaged blocking call
+
+ uint flags = 0;
+
+ statusCode =
+ HttpApi.HttpReceiveRequestEntityBody(
+ RequestQueueHandle,
+ RequestId,
+ flags,
+ (IntPtr)(pBuffer + offset),
+ (uint)size,
+ out extraDataRead,
+ SafeNativeOverlapped.Zero);
+
+ dataRead += extraDataRead;
+ }
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
+ {
+ Exception exception = new IOException(string.Empty, new HttpSysException((int)statusCode));
+ LogHelper.LogException(Logger, "Read", exception);
+ Abort();
+ throw exception;
+ }
+ UpdateAfterRead(statusCode, dataRead);
+ }
+ if (TryCheckSizeLimit((int)dataRead, out var ex))
+ {
+ throw ex;
+ }
+
+ // TODO: Verbose log dump data read
+ return (int)dataRead;
+ }
+
+ internal void UpdateAfterRead(uint statusCode, uint dataRead)
+ {
+ if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF || dataRead == 0)
+ {
+ Dispose();
+ }
+ }
+
+ public override unsafe IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback callback, object state)
+ {
+ ValidateReadBuffer(buffer, offset, size);
+ CheckSizeLimit();
+ if (_closed)
+ {
+ RequestStreamAsyncResult result = new RequestStreamAsyncResult(this, state, callback);
+ result.Complete(0);
+ return result;
+ }
+ // TODO: Verbose log parameters
+
+ RequestStreamAsyncResult asyncResult = null;
+
+ uint dataRead = 0;
+ if (_dataChunkIndex != -1)
+ {
+ dataRead = _requestContext.Request.GetChunks(ref _dataChunkIndex, ref _dataChunkOffset, buffer, offset, size);
+
+ if (dataRead > 0)
+ {
+ asyncResult = new RequestStreamAsyncResult(this, state, callback, buffer, offset, 0);
+ asyncResult.Complete((int)dataRead);
+ return asyncResult;
+ }
+ }
+
+ uint statusCode = 0;
+
+ // the http.sys team recommends that we limit the size to 128kb
+ if (size > MaxReadSize)
+ {
+ size = MaxReadSize;
+ }
+
+ asyncResult = new RequestStreamAsyncResult(this, state, callback, buffer, offset, dataRead);
+ uint bytesReturned;
+
+ try
+ {
+ uint flags = 0;
+
+ statusCode =
+ HttpApi.HttpReceiveRequestEntityBody(
+ RequestQueueHandle,
+ RequestId,
+ flags,
+ asyncResult.PinnedBuffer,
+ (uint)size,
+ out bytesReturned,
+ asyncResult.NativeOverlapped);
+ }
+ catch (Exception e)
+ {
+ LogHelper.LogException(Logger, "BeginRead", e);
+ asyncResult.Dispose();
+ throw;
+ }
+
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
+ {
+ asyncResult.Dispose();
+ if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
+ {
+ asyncResult = new RequestStreamAsyncResult(this, state, callback, dataRead);
+ asyncResult.Complete((int)bytesReturned);
+ }
+ else
+ {
+ Exception exception = new IOException(string.Empty, new HttpSysException((int)statusCode));
+ LogHelper.LogException(Logger, "BeginRead", exception);
+ Abort();
+ throw exception;
+ }
+ }
+ else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
+ HttpSysListener.SkipIOCPCallbackOnSuccess)
+ {
+ // IO operation completed synchronously - callback won't be called to signal completion.
+ asyncResult.IOCompleted(statusCode, bytesReturned);
+ }
+ return asyncResult;
+ }
+
+ public override int EndRead(IAsyncResult asyncResult)
+ {
+ if (asyncResult == null)
+ {
+ throw new ArgumentNullException("asyncResult");
+ }
+ RequestStreamAsyncResult castedAsyncResult = asyncResult as RequestStreamAsyncResult;
+ if (castedAsyncResult == null || castedAsyncResult.RequestStream != this)
+ {
+ throw new ArgumentException(Resources.Exception_WrongIAsyncResult, "asyncResult");
+ }
+ if (castedAsyncResult.EndCalled)
+ {
+ throw new InvalidOperationException(Resources.Exception_EndCalledMultipleTimes);
+ }
+ castedAsyncResult.EndCalled = true;
+ // wait & then check for errors
+ // Throws on failure
+ var dataRead = castedAsyncResult.Task.GetAwaiter().GetResult();
+ // TODO: Verbose log #dataRead.
+ return dataRead;
+ }
+
+ public override unsafe Task<int> ReadAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken)
+ {
+ ValidateReadBuffer(buffer, offset, size);
+ CheckSizeLimit();
+ if (_closed)
+ {
+ return Task.FromResult<int>(0);
+ }
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return Task.FromCanceled<int>(cancellationToken);
+ }
+ // TODO: Verbose log parameters
+
+ RequestStreamAsyncResult asyncResult = null;
+
+ uint dataRead = 0;
+ if (_dataChunkIndex != -1)
+ {
+ dataRead = _requestContext.Request.GetChunks(ref _dataChunkIndex, ref _dataChunkOffset, buffer, offset, size);
+ if (dataRead > 0)
+ {
+ UpdateAfterRead(UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS, dataRead);
+ if (TryCheckSizeLimit((int)dataRead, out var exception))
+ {
+ return Task.FromException<int>(exception);
+ }
+ // TODO: Verbose log #dataRead
+ return Task.FromResult<int>((int)dataRead);
+ }
+ }
+
+ uint statusCode = 0;
+ offset += (int)dataRead;
+ size -= (int)dataRead;
+
+ // the http.sys team recommends that we limit the size to 128kb
+ if (size > MaxReadSize)
+ {
+ size = MaxReadSize;
+ }
+
+ var cancellationRegistration = default(CancellationTokenRegistration);
+ if (cancellationToken.CanBeCanceled)
+ {
+ cancellationRegistration = RequestContext.RegisterForCancellation(cancellationToken);
+ }
+
+ asyncResult = new RequestStreamAsyncResult(this, null, null, buffer, offset, dataRead, cancellationRegistration);
+ uint bytesReturned;
+
+ try
+ {
+ uint flags = 0;
+
+ statusCode =
+ HttpApi.HttpReceiveRequestEntityBody(
+ RequestQueueHandle,
+ RequestId,
+ flags,
+ asyncResult.PinnedBuffer,
+ (uint)size,
+ out bytesReturned,
+ asyncResult.NativeOverlapped);
+ }
+ catch (Exception e)
+ {
+ asyncResult.Dispose();
+ Abort();
+ LogHelper.LogException(Logger, "ReadAsync", e);
+ throw;
+ }
+
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
+ {
+ asyncResult.Dispose();
+ if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
+ {
+ uint totalRead = dataRead + bytesReturned;
+ UpdateAfterRead(statusCode, totalRead);
+ if (TryCheckSizeLimit((int)totalRead, out var exception))
+ {
+ return Task.FromException<int>(exception);
+ }
+ // TODO: Verbose log totalRead
+ return Task.FromResult<int>((int)totalRead);
+ }
+ else
+ {
+ Exception exception = new IOException(string.Empty, new HttpSysException((int)statusCode));
+ LogHelper.LogException(Logger, "ReadAsync", exception);
+ Abort();
+ throw exception;
+ }
+ }
+ else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
+ HttpSysListener.SkipIOCPCallbackOnSuccess)
+ {
+ // IO operation completed synchronously - callback won't be called to signal completion.
+ asyncResult.Dispose();
+ uint totalRead = dataRead + bytesReturned;
+ UpdateAfterRead(statusCode, totalRead);
+ if (TryCheckSizeLimit((int)totalRead, out var exception))
+ {
+ return Task.FromException<int>(exception);
+ }
+ // TODO: Verbose log
+ return Task.FromResult<int>((int)totalRead);
+ }
+ return asyncResult.Task;
+ }
+
+ public override void Write(byte[] buffer, int offset, int size)
+ {
+ throw new InvalidOperationException(Resources.Exception_ReadOnlyStream);
+ }
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int size, AsyncCallback callback, object state)
+ {
+ throw new InvalidOperationException(Resources.Exception_ReadOnlyStream);
+ }
+
+ public override void EndWrite(IAsyncResult asyncResult)
+ {
+ throw new InvalidOperationException(Resources.Exception_ReadOnlyStream);
+ }
+
+ // Called before each read
+ private void CheckSizeLimit()
+ {
+ // Note SwitchToOpaqueMode sets HasStarted and clears _maxSize, so these limits don't apply.
+ if (!HasStarted)
+ {
+ var contentLength = RequestContext.Request.ContentLength;
+ if (contentLength.HasValue && _maxSize.HasValue && contentLength.Value > _maxSize.Value)
+ {
+ throw new IOException(
+ $"The request's Content-Length {contentLength.Value} is larger than the request body size limit {_maxSize.Value}.");
+ }
+
+ HasStarted = true;
+ }
+ else if (TryCheckSizeLimit(0, out var exception))
+ {
+ throw exception;
+ }
+ }
+
+ // Called after each read.
+ internal bool TryCheckSizeLimit(int bytesRead, out Exception exception)
+ {
+ _totalRead += bytesRead;
+ if (_maxSize.HasValue && _totalRead > _maxSize.Value)
+ {
+ exception = new IOException($"The total number of bytes read {_totalRead} has exceeded the request body size limit {_maxSize.Value}.");
+ return true;
+ }
+ exception = null;
+ return false;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ try
+ {
+ _closed = true;
+ }
+ finally
+ {
+ base.Dispose(disposing);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStreamAsyncResult.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStreamAsyncResult.cs
new file mode 100644
index 0000000000..a2fa887f56
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/RequestStreamAsyncResult.cs
@@ -0,0 +1,194 @@
+// 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.CodeAnalysis;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.HttpSys.Internal;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal unsafe class RequestStreamAsyncResult : IAsyncResult, IDisposable
+ {
+ private static readonly IOCompletionCallback IOCallback = new IOCompletionCallback(Callback);
+
+ private SafeNativeOverlapped _overlapped;
+ private IntPtr _pinnedBuffer;
+ private uint _dataAlreadyRead;
+ private TaskCompletionSource<int> _tcs;
+ private RequestStream _requestStream;
+ private AsyncCallback _callback;
+ private CancellationTokenRegistration _cancellationRegistration;
+
+ internal RequestStreamAsyncResult(RequestStream requestStream, object userState, AsyncCallback callback)
+ {
+ _requestStream = requestStream;
+ _tcs = new TaskCompletionSource<int>(userState);
+ _callback = callback;
+ }
+
+ internal RequestStreamAsyncResult(RequestStream requestStream, object userState, AsyncCallback callback, uint dataAlreadyRead)
+ : this(requestStream, userState, callback)
+ {
+ _dataAlreadyRead = dataAlreadyRead;
+ }
+
+ internal RequestStreamAsyncResult(RequestStream requestStream, object userState, AsyncCallback callback, byte[] buffer, int offset, uint dataAlreadyRead)
+ : this(requestStream, userState, callback, buffer, offset, dataAlreadyRead, new CancellationTokenRegistration())
+ {
+ }
+
+ internal RequestStreamAsyncResult(RequestStream requestStream, object userState, AsyncCallback callback, byte[] buffer, int offset, uint dataAlreadyRead, CancellationTokenRegistration cancellationRegistration)
+ : this(requestStream, userState, callback)
+ {
+ _dataAlreadyRead = dataAlreadyRead;
+ var boundHandle = requestStream.RequestContext.Server.RequestQueue.BoundHandle;
+ _overlapped = new SafeNativeOverlapped(boundHandle,
+ boundHandle.AllocateNativeOverlapped(IOCallback, this, buffer));
+ _pinnedBuffer = (Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset));
+ _cancellationRegistration = cancellationRegistration;
+ }
+
+ internal RequestStream RequestStream
+ {
+ get { return _requestStream; }
+ }
+
+ internal SafeNativeOverlapped NativeOverlapped
+ {
+ get { return _overlapped; }
+ }
+
+ internal IntPtr PinnedBuffer
+ {
+ get { return _pinnedBuffer; }
+ }
+
+ internal uint DataAlreadyRead
+ {
+ get { return _dataAlreadyRead; }
+ }
+
+ internal Task<int> Task
+ {
+ get { return _tcs.Task; }
+ }
+
+ internal bool EndCalled { get; set; }
+
+ internal void IOCompleted(uint errorCode, uint numBytes)
+ {
+ IOCompleted(this, errorCode, numBytes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Redirecting to callback")]
+ private static void IOCompleted(RequestStreamAsyncResult asyncResult, uint errorCode, uint numBytes)
+ {
+ try
+ {
+ if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
+ {
+ asyncResult.Fail(new IOException(string.Empty, new HttpSysException((int)errorCode)));
+ }
+ else
+ {
+ // TODO: Verbose log dump data read
+ asyncResult.Complete((int)numBytes, errorCode);
+ }
+ }
+ catch (Exception e)
+ {
+ asyncResult.Fail(new IOException(string.Empty, e));
+ }
+ }
+
+ private static unsafe void Callback(uint errorCode, uint numBytes, NativeOverlapped* nativeOverlapped)
+ {
+ var asyncResult = (RequestStreamAsyncResult)ThreadPoolBoundHandle.GetNativeOverlappedState(nativeOverlapped);
+ IOCompleted(asyncResult, errorCode, numBytes);
+ }
+
+ internal void Complete(int read, uint errorCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
+ {
+ if (_requestStream.TryCheckSizeLimit(read + (int)DataAlreadyRead, out var exception))
+ {
+ _tcs.TrySetException(exception);
+ }
+ else if (_tcs.TrySetResult(read + (int)DataAlreadyRead))
+ {
+ RequestStream.UpdateAfterRead((uint)errorCode, (uint)(read + DataAlreadyRead));
+ if (_callback != null)
+ {
+ try
+ {
+ _callback(this);
+ }
+ catch (Exception)
+ {
+ // TODO: Exception handling? This may be an IO callback thread and throwing here could crash the app.
+ }
+ }
+ }
+ Dispose();
+ }
+
+ internal void Fail(Exception ex)
+ {
+ if (_tcs.TrySetException(ex) && _callback != null)
+ {
+ try
+ {
+ _callback(this);
+ }
+ catch (Exception)
+ {
+ // TODO: Exception handling? This may be an IO callback thread and throwing here could crash the app.
+ // TODO: Log
+ }
+ }
+ Dispose();
+ _requestStream.Abort();
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2216:DisposableTypesShouldDeclareFinalizer", Justification = "The disposable resource referenced does have a finalizer.")]
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if (_overlapped != null)
+ {
+ _overlapped.Dispose();
+ }
+ _cancellationRegistration.Dispose();
+ }
+ }
+
+ public object AsyncState
+ {
+ get { return _tcs.Task.AsyncState; }
+ }
+
+ public WaitHandle AsyncWaitHandle
+ {
+ get { return ((IAsyncResult)_tcs.Task).AsyncWaitHandle; }
+ }
+
+ public bool CompletedSynchronously
+ {
+ get { return ((IAsyncResult)_tcs.Task).CompletedSynchronously; }
+ }
+
+ public bool IsCompleted
+ {
+ get { return _tcs.Task.IsCompleted; }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/Response.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/Response.cs
new file mode 100644
index 0000000000..feef66ca24
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/Response.cs
@@ -0,0 +1,627 @@
+// 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.Globalization;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.Extensions.Primitives;
+using static Microsoft.AspNetCore.HttpSys.Internal.UnsafeNclNativeMethods;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal sealed class Response
+ {
+ private ResponseState _responseState;
+ private string _reasonPhrase;
+ private ResponseBody _nativeStream;
+ private AuthenticationSchemes _authChallenges;
+ private TimeSpan? _cacheTtl;
+ private long _expectedBodyLength;
+ private BoundaryType _boundaryType;
+ private HttpApiTypes.HTTP_RESPONSE_V2 _nativeResponse;
+
+ internal Response(RequestContext requestContext)
+ {
+ // TODO: Verbose log
+ RequestContext = requestContext;
+ Headers = new HeaderCollection();
+ // We haven't started yet, or we're just buffered, we can clear any data, headers, and state so
+ // that we can start over (e.g. to write an error message).
+ _nativeResponse = new HttpApiTypes.HTTP_RESPONSE_V2();
+ Headers.IsReadOnly = false;
+ Headers.Clear();
+ _reasonPhrase = null;
+ _boundaryType = BoundaryType.None;
+ _nativeResponse.Response_V1.StatusCode = (ushort)StatusCodes.Status200OK;
+ _nativeResponse.Response_V1.Version.MajorVersion = 1;
+ _nativeResponse.Response_V1.Version.MinorVersion = 1;
+ _responseState = ResponseState.Created;
+ _expectedBodyLength = 0;
+ _nativeStream = null;
+ _cacheTtl = null;
+ _authChallenges = RequestContext.Server.Options.Authentication.Schemes;
+ }
+
+ private enum ResponseState
+ {
+ Created,
+ ComputedHeaders,
+ Started,
+ Closed,
+ }
+
+ private RequestContext RequestContext { get; }
+
+ private Request Request => RequestContext.Request;
+
+ public int StatusCode
+ {
+ get { return _nativeResponse.Response_V1.StatusCode; }
+ set
+ {
+ // Http.Sys automatically sends 100 Continue responses when you read from the request body.
+ if (value <= 100 || 999 < value)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), value, string.Format(Resources.Exception_InvalidStatusCode, value));
+ }
+ CheckResponseStarted();
+ _nativeResponse.Response_V1.StatusCode = (ushort)value;
+ }
+ }
+
+ public string ReasonPhrase
+ {
+ get { return _reasonPhrase; }
+ set
+ {
+ // TODO: Validate user input for illegal chars, length limit, etc.?
+ CheckResponseStarted();
+ _reasonPhrase = value;
+ }
+ }
+
+ public Stream Body
+ {
+ get
+ {
+ EnsureResponseStream();
+ return _nativeStream;
+ }
+ }
+
+ internal bool BodyIsFinished => _nativeStream?.IsDisposed ?? _responseState >= ResponseState.Closed;
+
+ /// <summary>
+ /// The authentication challenges that will be added to the response if the status code is 401.
+ /// This must be a subset of the AuthenticationSchemes enabled on the server.
+ /// </summary>
+ public AuthenticationSchemes AuthenticationChallenges
+ {
+ get { return _authChallenges; }
+ set
+ {
+ CheckResponseStarted();
+ _authChallenges = value;
+ }
+ }
+
+ private string GetReasonPhrase(int statusCode)
+ {
+ string reasonPhrase = ReasonPhrase;
+ if (string.IsNullOrWhiteSpace(reasonPhrase))
+ {
+ // If the user hasn't set this then it is generated on the fly if possible.
+ reasonPhrase = HttpReasonPhrase.Get(statusCode) ?? string.Empty;
+ }
+ return reasonPhrase;
+ }
+
+ // We MUST NOT send message-body when we send responses with these Status codes
+ private static readonly int[] StatusWithNoResponseBody = { 100, 101, 204, 205, 304 };
+
+ private static bool CanSendResponseBody(int responseCode)
+ {
+ for (int i = 0; i < StatusWithNoResponseBody.Length; i++)
+ {
+ if (responseCode == StatusWithNoResponseBody[i])
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public HeaderCollection Headers { get; }
+
+ internal long ExpectedBodyLength
+ {
+ get { return _expectedBodyLength; }
+ }
+
+ // Header accessors
+ public long? ContentLength
+ {
+ get { return Headers.ContentLength; }
+ set { Headers.ContentLength = value; }
+ }
+
+ /// <summary>
+ /// Enable kernel caching for the response with the given timeout. Http.Sys determines if the response
+ /// can be cached.
+ /// </summary>
+ public TimeSpan? CacheTtl
+ {
+ get { return _cacheTtl; }
+ set
+ {
+ CheckResponseStarted();
+ _cacheTtl = value;
+ }
+ }
+
+ internal void Abort()
+ {
+ // Update state for HasStarted. Do not attempt a graceful Dispose.
+ _responseState = ResponseState.Closed;
+ }
+
+ // should only be called from RequestContext
+ internal void Dispose()
+ {
+ if (_responseState >= ResponseState.Closed)
+ {
+ return;
+ }
+ // TODO: Verbose log
+ EnsureResponseStream();
+ _nativeStream.Dispose();
+ _responseState = ResponseState.Closed;
+ }
+
+ internal BoundaryType BoundaryType
+ {
+ get { return _boundaryType; }
+ }
+
+ internal bool HasComputedHeaders
+ {
+ get { return _responseState >= ResponseState.ComputedHeaders; }
+ }
+
+ /// <summary>
+ /// Indicates if the response status, reason, and headers are prepared to send and can
+ /// no longer be modified. This is caused by the first write or flush to the response body.
+ /// </summary>
+ public bool HasStarted
+ {
+ get { return _responseState >= ResponseState.Started; }
+ }
+
+ private void CheckResponseStarted()
+ {
+ if (HasStarted)
+ {
+ throw new InvalidOperationException("Headers already sent.");
+ }
+ }
+
+ private void EnsureResponseStream()
+ {
+ if (_nativeStream == null)
+ {
+ _nativeStream = new ResponseBody(RequestContext);
+ }
+ }
+
+ /*
+ 12.3
+ HttpSendHttpResponse() and HttpSendResponseEntityBody() Flag Values.
+ The following flags can be used on calls to HttpSendHttpResponse() and HttpSendResponseEntityBody() API calls:
+
+ #define HTTP_SEND_RESPONSE_FLAG_DISCONNECT 0x00000001
+ #define HTTP_SEND_RESPONSE_FLAG_MORE_DATA 0x00000002
+ #define HTTP_SEND_RESPONSE_FLAG_RAW_HEADER 0x00000004
+ #define HTTP_SEND_RESPONSE_FLAG_VALID 0x00000007
+
+ HTTP_SEND_RESPONSE_FLAG_DISCONNECT:
+ specifies that the network connection should be disconnected immediately after
+ sending the response, overriding the HTTP protocol's persistent connection features.
+ HTTP_SEND_RESPONSE_FLAG_MORE_DATA:
+ specifies that additional entity body data will be sent by the caller. Thus,
+ the last call HttpSendResponseEntityBody for a RequestId, will have this flag reset.
+ HTTP_SEND_RESPONSE_RAW_HEADER:
+ specifies that a caller of HttpSendResponseEntityBody() is intentionally omitting
+ a call to HttpSendHttpResponse() in order to bypass normal header processing. The
+ actual HTTP header will be generated by the application and sent as entity body.
+ This flag should be passed on the first call to HttpSendResponseEntityBody, and
+ not after. Thus, flag is not applicable to HttpSendHttpResponse.
+ */
+
+ // TODO: Consider using HTTP_SEND_RESPONSE_RAW_HEADER with HttpSendResponseEntityBody instead of calling HttpSendHttpResponse.
+ // This will give us more control of the bytes that hit the wire, including encodings, HTTP 1.0, etc..
+ // It may also be faster to do this work in managed code and then pass down only one buffer.
+ // What would we loose by bypassing HttpSendHttpResponse?
+ //
+ // TODO: Consider using the HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA flag for most/all responses rather than just Opaque.
+ internal unsafe uint SendHeaders(HttpApiTypes.HTTP_DATA_CHUNK[] dataChunks,
+ ResponseStreamAsyncResult asyncResult,
+ HttpApiTypes.HTTP_FLAGS flags,
+ bool isOpaqueUpgrade)
+ {
+ Debug.Assert(!HasStarted, "HttpListenerResponse::SendHeaders()|SentHeaders is true.");
+
+ _responseState = ResponseState.Started;
+ var reasonPhrase = GetReasonPhrase(StatusCode);
+
+ uint statusCode;
+ uint bytesSent;
+ List<GCHandle> pinnedHeaders = SerializeHeaders(isOpaqueUpgrade);
+ try
+ {
+ if (dataChunks != null)
+ {
+ if (pinnedHeaders == null)
+ {
+ pinnedHeaders = new List<GCHandle>();
+ }
+ var handle = GCHandle.Alloc(dataChunks, GCHandleType.Pinned);
+ pinnedHeaders.Add(handle);
+ _nativeResponse.Response_V1.EntityChunkCount = (ushort)dataChunks.Length;
+ _nativeResponse.Response_V1.pEntityChunks = (HttpApiTypes.HTTP_DATA_CHUNK*)handle.AddrOfPinnedObject();
+ }
+ else if (asyncResult != null && asyncResult.DataChunks != null)
+ {
+ _nativeResponse.Response_V1.EntityChunkCount = asyncResult.DataChunkCount;
+ _nativeResponse.Response_V1.pEntityChunks = asyncResult.DataChunks;
+ }
+ else
+ {
+ _nativeResponse.Response_V1.EntityChunkCount = 0;
+ _nativeResponse.Response_V1.pEntityChunks = null;
+ }
+
+ var cachePolicy = new HttpApiTypes.HTTP_CACHE_POLICY();
+ if (_cacheTtl.HasValue && _cacheTtl.Value > TimeSpan.Zero)
+ {
+ cachePolicy.Policy = HttpApiTypes.HTTP_CACHE_POLICY_TYPE.HttpCachePolicyTimeToLive;
+ cachePolicy.SecondsToLive = (uint)Math.Min(_cacheTtl.Value.Ticks / TimeSpan.TicksPerSecond, Int32.MaxValue);
+ }
+
+ byte[] reasonPhraseBytes = HeaderEncoding.GetBytes(reasonPhrase);
+ fixed (byte* pReasonPhrase = reasonPhraseBytes)
+ {
+ _nativeResponse.Response_V1.ReasonLength = (ushort)reasonPhraseBytes.Length;
+ _nativeResponse.Response_V1.pReason = (byte*)pReasonPhrase;
+ fixed (HttpApiTypes.HTTP_RESPONSE_V2* pResponse = &_nativeResponse)
+ {
+ statusCode =
+ HttpApi.HttpSendHttpResponse(
+ RequestContext.Server.RequestQueue.Handle,
+ Request.RequestId,
+ (uint)flags,
+ pResponse,
+ &cachePolicy,
+ &bytesSent,
+ IntPtr.Zero,
+ 0,
+ asyncResult == null ? SafeNativeOverlapped.Zero : asyncResult.NativeOverlapped,
+ IntPtr.Zero);
+
+ if (asyncResult != null &&
+ statusCode == ErrorCodes.ERROR_SUCCESS &&
+ HttpSysListener.SkipIOCPCallbackOnSuccess)
+ {
+ asyncResult.BytesSent = bytesSent;
+ // The caller will invoke IOCompleted
+ }
+ }
+ }
+ }
+ finally
+ {
+ FreePinnedHeaders(pinnedHeaders);
+ }
+ return statusCode;
+ }
+
+ internal HttpApiTypes.HTTP_FLAGS ComputeHeaders(long writeCount, bool endOfRequest = false)
+ {
+ if (StatusCode == (ushort)StatusCodes.Status401Unauthorized)
+ {
+ RequestContext.Server.Options.Authentication.SetAuthenticationChallenge(RequestContext);
+ }
+
+ var flags = HttpApiTypes.HTTP_FLAGS.NONE;
+ Debug.Assert(!HasComputedHeaders, nameof(HasComputedHeaders) + " is true.");
+ _responseState = ResponseState.ComputedHeaders;
+
+ // Gather everything from the request that affects the response:
+ var requestVersion = Request.ProtocolVersion;
+ var requestConnectionString = Request.Headers[HttpKnownHeaderNames.Connection];
+ var isHeadRequest = Request.IsHeadMethod;
+ var requestCloseSet = Matches(Constants.Close, requestConnectionString);
+
+ // Gather everything the app may have set on the response:
+ // Http.Sys does not allow us to specify the response protocol version, assume this is a HTTP/1.1 response when making decisions.
+ var responseConnectionString = Headers[HttpKnownHeaderNames.Connection];
+ var transferEncodingString = Headers[HttpKnownHeaderNames.TransferEncoding];
+ var responseContentLength = ContentLength;
+ var responseCloseSet = Matches(Constants.Close, responseConnectionString);
+ var responseChunkedSet = Matches(Constants.Chunked, transferEncodingString);
+ var statusCanHaveBody = CanSendResponseBody(RequestContext.Response.StatusCode);
+
+ // Determine if the connection will be kept alive or closed.
+ var keepConnectionAlive = true;
+ if (requestVersion <= Constants.V1_0 // Http.Sys does not support "Keep-Alive: true" or "Connection: Keep-Alive"
+ || (requestVersion == Constants.V1_1 && requestCloseSet)
+ || responseCloseSet)
+ {
+ keepConnectionAlive = false;
+ }
+
+ // Determine the body format. If the user asks to do something, let them, otherwise choose a good default for the scenario.
+ if (responseContentLength.HasValue)
+ {
+ _boundaryType = BoundaryType.ContentLength;
+ // ComputeLeftToWrite checks for HEAD requests when setting _leftToWrite
+ _expectedBodyLength = responseContentLength.Value;
+ if (_expectedBodyLength == writeCount && !isHeadRequest)
+ {
+ // A single write with the whole content-length. Http.Sys will set the content-length for us in this scenario.
+ // If we don't remove it then range requests served from cache will have two.
+ // https://github.com/aspnet/HttpSysServer/issues/167
+ ContentLength = null;
+ }
+ }
+ else if (responseChunkedSet)
+ {
+ // The application is performing it's own chunking.
+ _boundaryType = BoundaryType.PassThrough;
+ }
+ else if (endOfRequest)
+ {
+ if (!isHeadRequest && statusCanHaveBody)
+ {
+ Headers[HttpKnownHeaderNames.ContentLength] = Constants.Zero;
+ }
+ _boundaryType = BoundaryType.ContentLength;
+ _expectedBodyLength = 0;
+ }
+ else if (requestVersion == Constants.V1_1)
+ {
+ _boundaryType = BoundaryType.Chunked;
+ Headers[HttpKnownHeaderNames.TransferEncoding] = Constants.Chunked;
+ }
+ else
+ {
+ // v1.0 and the length cannot be determined, so we must close the connection after writing data
+ keepConnectionAlive = false;
+ _boundaryType = BoundaryType.Close;
+ }
+
+ // Managed connection lifetime
+ if (!keepConnectionAlive)
+ {
+ // All Http.Sys responses are v1.1, so use 1.1 response headers
+ // Note that if we don't add this header, Http.Sys will often do it for us.
+ if (!responseCloseSet)
+ {
+ Headers.Append(HttpKnownHeaderNames.Connection, Constants.Close);
+ }
+ flags = HttpApiTypes.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
+ }
+
+ return flags;
+ }
+
+ private static bool Matches(string knownValue, string input)
+ {
+ return string.Equals(knownValue, input?.Trim(), StringComparison.OrdinalIgnoreCase);
+ }
+
+ private unsafe List<GCHandle> SerializeHeaders(bool isOpaqueUpgrade)
+ {
+ Headers.IsReadOnly = true; // Prohibit further modifications.
+ HttpApiTypes.HTTP_UNKNOWN_HEADER[] unknownHeaders = null;
+ HttpApiTypes.HTTP_RESPONSE_INFO[] knownHeaderInfo = null;
+ List<GCHandle> pinnedHeaders;
+ GCHandle gcHandle;
+
+ if (Headers.Count == 0)
+ {
+ return null;
+ }
+ string headerName;
+ string headerValue;
+ int lookup;
+ byte[] bytes = null;
+ pinnedHeaders = new List<GCHandle>();
+
+ int numUnknownHeaders = 0;
+ int numKnownMultiHeaders = 0;
+ foreach (var headerPair in Headers)
+ {
+ if (headerPair.Value.Count == 0)
+ {
+ continue;
+ }
+ // See if this is an unknown header
+ lookup = HttpApiTypes.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerPair.Key);
+
+ // Http.Sys doesn't let us send the Connection: Upgrade header as a Known header.
+ if (lookup == -1 ||
+ (isOpaqueUpgrade && lookup == (int)HttpApiTypes.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderConnection))
+ {
+ numUnknownHeaders += headerPair.Value.Count;
+ }
+ else if (headerPair.Value.Count > 1)
+ {
+ numKnownMultiHeaders++;
+ }
+ // else known single-value header.
+ }
+
+ try
+ {
+ fixed (HttpApiTypes.HTTP_KNOWN_HEADER* pKnownHeaders = &_nativeResponse.Response_V1.Headers.KnownHeaders)
+ {
+ foreach (var headerPair in Headers)
+ {
+ if (headerPair.Value.Count == 0)
+ {
+ continue;
+ }
+ headerName = headerPair.Key;
+ StringValues headerValues = headerPair.Value;
+ lookup = HttpApiTypes.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerName);
+
+ // Http.Sys doesn't let us send the Connection: Upgrade header as a Known header.
+ if (lookup == -1 ||
+ (isOpaqueUpgrade && lookup == (int)HttpApiTypes.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderConnection))
+ {
+ if (unknownHeaders == null)
+ {
+ unknownHeaders = new HttpApiTypes.HTTP_UNKNOWN_HEADER[numUnknownHeaders];
+ gcHandle = GCHandle.Alloc(unknownHeaders, GCHandleType.Pinned);
+ pinnedHeaders.Add(gcHandle);
+ _nativeResponse.Response_V1.Headers.pUnknownHeaders = (HttpApiTypes.HTTP_UNKNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
+ }
+
+ for (int headerValueIndex = 0; headerValueIndex < headerValues.Count; headerValueIndex++)
+ {
+ // Add Name
+ bytes = HeaderEncoding.GetBytes(headerName);
+ unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].NameLength = (ushort)bytes.Length;
+ gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
+ pinnedHeaders.Add(gcHandle);
+ unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].pName = (byte*)gcHandle.AddrOfPinnedObject();
+
+ // Add Value
+ headerValue = headerValues[headerValueIndex] ?? string.Empty;
+ bytes = HeaderEncoding.GetBytes(headerValue);
+ unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].RawValueLength = (ushort)bytes.Length;
+ gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
+ pinnedHeaders.Add(gcHandle);
+ unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].pRawValue = (byte*)gcHandle.AddrOfPinnedObject();
+ _nativeResponse.Response_V1.Headers.UnknownHeaderCount++;
+ }
+ }
+ else if (headerPair.Value.Count == 1)
+ {
+ headerValue = headerValues[0] ?? string.Empty;
+ bytes = HeaderEncoding.GetBytes(headerValue);
+ pKnownHeaders[lookup].RawValueLength = (ushort)bytes.Length;
+ gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
+ pinnedHeaders.Add(gcHandle);
+ pKnownHeaders[lookup].pRawValue = (byte*)gcHandle.AddrOfPinnedObject();
+ }
+ else
+ {
+ if (knownHeaderInfo == null)
+ {
+ knownHeaderInfo = new HttpApiTypes.HTTP_RESPONSE_INFO[numKnownMultiHeaders];
+ gcHandle = GCHandle.Alloc(knownHeaderInfo, GCHandleType.Pinned);
+ pinnedHeaders.Add(gcHandle);
+ _nativeResponse.pResponseInfo = (HttpApiTypes.HTTP_RESPONSE_INFO*)gcHandle.AddrOfPinnedObject();
+ }
+
+ knownHeaderInfo[_nativeResponse.ResponseInfoCount].Type = HttpApiTypes.HTTP_RESPONSE_INFO_TYPE.HttpResponseInfoTypeMultipleKnownHeaders;
+ knownHeaderInfo[_nativeResponse.ResponseInfoCount].Length = (uint)Marshal.SizeOf<HttpApiTypes.HTTP_MULTIPLE_KNOWN_HEADERS>();
+
+ HttpApiTypes.HTTP_MULTIPLE_KNOWN_HEADERS header = new HttpApiTypes.HTTP_MULTIPLE_KNOWN_HEADERS();
+
+ header.HeaderId = (HttpApiTypes.HTTP_RESPONSE_HEADER_ID.Enum)lookup;
+ header.Flags = HttpApiTypes.HTTP_RESPONSE_INFO_FLAGS.PreserveOrder; // TODO: The docs say this is for www-auth only.
+
+ HttpApiTypes.HTTP_KNOWN_HEADER[] nativeHeaderValues = new HttpApiTypes.HTTP_KNOWN_HEADER[headerValues.Count];
+ gcHandle = GCHandle.Alloc(nativeHeaderValues, GCHandleType.Pinned);
+ pinnedHeaders.Add(gcHandle);
+ header.KnownHeaders = (HttpApiTypes.HTTP_KNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
+
+ for (int headerValueIndex = 0; headerValueIndex < headerValues.Count; headerValueIndex++)
+ {
+ // Add Value
+ headerValue = headerValues[headerValueIndex] ?? string.Empty;
+ bytes = HeaderEncoding.GetBytes(headerValue);
+ nativeHeaderValues[header.KnownHeaderCount].RawValueLength = (ushort)bytes.Length;
+ gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
+ pinnedHeaders.Add(gcHandle);
+ nativeHeaderValues[header.KnownHeaderCount].pRawValue = (byte*)gcHandle.AddrOfPinnedObject();
+ header.KnownHeaderCount++;
+ }
+
+ // This type is a struct, not an object, so pinning it causes a boxed copy to be created. We can't do that until after all the fields are set.
+ gcHandle = GCHandle.Alloc(header, GCHandleType.Pinned);
+ pinnedHeaders.Add(gcHandle);
+ knownHeaderInfo[_nativeResponse.ResponseInfoCount].pInfo = (HttpApiTypes.HTTP_MULTIPLE_KNOWN_HEADERS*)gcHandle.AddrOfPinnedObject();
+
+ _nativeResponse.ResponseInfoCount++;
+ }
+ }
+ }
+ }
+ catch
+ {
+ FreePinnedHeaders(pinnedHeaders);
+ throw;
+ }
+ return pinnedHeaders;
+ }
+
+ private static void FreePinnedHeaders(List<GCHandle> pinnedHeaders)
+ {
+ if (pinnedHeaders != null)
+ {
+ foreach (GCHandle gcHandle in pinnedHeaders)
+ {
+ if (gcHandle.IsAllocated)
+ {
+ gcHandle.Free();
+ }
+ }
+ }
+ }
+
+ // Subset of ComputeHeaders
+ internal void SendOpaqueUpgrade()
+ {
+ _boundaryType = BoundaryType.Close;
+
+ // TODO: Send headers async?
+ ulong errorCode = SendHeaders(null, null,
+ HttpApiTypes.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_OPAQUE |
+ HttpApiTypes.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA |
+ HttpApiTypes.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA,
+ true);
+
+ if (errorCode != ErrorCodes.ERROR_SUCCESS)
+ {
+ throw new HttpSysException((int)errorCode);
+ }
+ }
+
+ internal void CancelLastWrite()
+ {
+ _nativeStream?.CancelLastWrite();
+ }
+
+ public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancel)
+ {
+ EnsureResponseStream();
+ return _nativeStream.SendFileAsync(path, offset, count, cancel);
+ }
+
+ internal void SwitchToOpaqueMode()
+ {
+ EnsureResponseStream();
+ _nativeStream.SwitchToOpaqueMode();
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/ResponseBody.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/ResponseBody.cs
new file mode 100644
index 0000000000..8f6341db63
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/ResponseBody.cs
@@ -0,0 +1,705 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.Extensions.Logging;
+using static Microsoft.AspNetCore.HttpSys.Internal.UnsafeNclNativeMethods;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal class ResponseBody : Stream
+ {
+ private RequestContext _requestContext;
+ private long _leftToWrite = long.MinValue;
+ private bool _skipWrites;
+ private bool _disposed;
+
+ // The last write needs special handling to cancel.
+ private ResponseStreamAsyncResult _lastWrite;
+
+ internal ResponseBody(RequestContext requestContext)
+ {
+ _requestContext = requestContext;
+ }
+
+ internal RequestContext RequestContext
+ {
+ get { return _requestContext; }
+ }
+
+ private SafeHandle RequestQueueHandle => RequestContext.Server.RequestQueue.Handle;
+
+ private ulong RequestId => RequestContext.Request.RequestId;
+
+ private ILogger Logger => RequestContext.Server.Logger;
+
+ internal bool ThrowWriteExceptions => RequestContext.Server.Options.ThrowWriteExceptions;
+
+ internal bool IsDisposed => _disposed;
+
+ public override bool CanSeek
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public override bool CanWrite
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public override bool CanRead
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public override long Length
+ {
+ get
+ {
+ throw new NotSupportedException(Resources.Exception_NoSeek);
+ }
+ }
+
+ public override long Position
+ {
+ get
+ {
+ throw new NotSupportedException(Resources.Exception_NoSeek);
+ }
+ set
+ {
+ throw new NotSupportedException(Resources.Exception_NoSeek);
+ }
+ }
+
+ // Send headers
+ public override void Flush()
+ {
+ if (!RequestContext.AllowSynchronousIO)
+ {
+ throw new InvalidOperationException("Synchronous IO APIs are disabled, see AllowSynchronousIO.");
+ }
+
+ if (_disposed)
+ {
+ return;
+ }
+
+ FlushInternal(endOfRequest: false);
+ }
+
+ // We never expect endOfRequest and data at the same time
+ private unsafe void FlushInternal(bool endOfRequest, ArraySegment<byte> data = new ArraySegment<byte>())
+ {
+ Debug.Assert(!(endOfRequest && data.Count > 0), "Data is not supported at the end of the request.");
+
+ if (_skipWrites)
+ {
+ return;
+ }
+
+ var started = _requestContext.Response.HasStarted;
+ if (data.Count == 0 && started && !endOfRequest)
+ {
+ // No data to send and we've already sent the headers
+ return;
+ }
+
+ // Make sure all validation is performed before this computes the headers
+ var flags = ComputeLeftToWrite(data.Count, endOfRequest);
+ if (endOfRequest && _leftToWrite > 0)
+ {
+ _requestContext.Abort();
+ // This is logged rather than thrown because it is too late for an exception to be visible in user code.
+ LogHelper.LogError(Logger, "ResponseStream::Dispose", "Fewer bytes were written than were specified in the Content-Length.");
+ return;
+ }
+
+ uint statusCode = 0;
+ HttpApiTypes.HTTP_DATA_CHUNK[] dataChunks;
+ var pinnedBuffers = PinDataBuffers(endOfRequest, data, out dataChunks);
+ try
+ {
+ if (!started)
+ {
+ statusCode = _requestContext.Response.SendHeaders(dataChunks, null, flags, false);
+ }
+ else
+ {
+ fixed (HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks = dataChunks)
+ {
+ statusCode = HttpApi.HttpSendResponseEntityBody(
+ RequestQueueHandle,
+ RequestId,
+ (uint)flags,
+ (ushort)dataChunks.Length,
+ pDataChunks,
+ null,
+ IntPtr.Zero,
+ 0,
+ SafeNativeOverlapped.Zero,
+ IntPtr.Zero);
+ }
+ }
+ }
+ finally
+ {
+ FreeDataBuffers(pinnedBuffers);
+ }
+
+ if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_HANDLE_EOF
+ // Don't throw for disconnects, we were already finished with the response.
+ && (!endOfRequest || (statusCode != ErrorCodes.ERROR_CONNECTION_INVALID && statusCode != ErrorCodes.ERROR_INVALID_PARAMETER)))
+ {
+ if (ThrowWriteExceptions)
+ {
+ var exception = new IOException(string.Empty, new HttpSysException((int)statusCode));
+ LogHelper.LogException(Logger, "Flush", exception);
+ Abort();
+ throw exception;
+ }
+ else
+ {
+ // Abort the request but do not close the stream, let future writes complete silently
+ LogHelper.LogDebug(Logger, "Flush", $"Ignored write exception: {statusCode}");
+ Abort(dispose: false);
+ }
+ }
+ }
+
+ private List<GCHandle> PinDataBuffers(bool endOfRequest, ArraySegment<byte> data, out HttpApiTypes.HTTP_DATA_CHUNK[] dataChunks)
+ {
+ var pins = new List<GCHandle>();
+ var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
+
+ var currentChunk = 0;
+ // Figure out how many data chunks
+ if (chunked && data.Count == 0 && endOfRequest)
+ {
+ dataChunks = new HttpApiTypes.HTTP_DATA_CHUNK[1];
+ SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.ChunkTerminator));
+ return pins;
+ }
+ else if (data.Count == 0)
+ {
+ // No data
+ dataChunks = new HttpApiTypes.HTTP_DATA_CHUNK[0];
+ return pins;
+ }
+
+ var chunkCount = 1;
+ if (chunked)
+ {
+ // Chunk framing
+ chunkCount += 2;
+
+ if (endOfRequest)
+ {
+ // Chunk terminator
+ chunkCount += 1;
+ }
+ }
+ dataChunks = new HttpApiTypes.HTTP_DATA_CHUNK[chunkCount];
+
+ if (chunked)
+ {
+ var chunkHeaderBuffer = Helpers.GetChunkHeader(data.Count);
+ SetDataChunk(dataChunks, ref currentChunk, pins, chunkHeaderBuffer);
+ }
+
+ SetDataChunk(dataChunks, ref currentChunk, pins, data);
+
+ if (chunked)
+ {
+ SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.CRLF));
+
+ if (endOfRequest)
+ {
+ SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.ChunkTerminator));
+ }
+ }
+
+ return pins;
+ }
+
+ private static void SetDataChunk(HttpApiTypes.HTTP_DATA_CHUNK[] chunks, ref int chunkIndex, List<GCHandle> pins, ArraySegment<byte> buffer)
+ {
+ var handle = GCHandle.Alloc(buffer.Array, GCHandleType.Pinned);
+ pins.Add(handle);
+ chunks[chunkIndex].DataChunkType = HttpApiTypes.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
+ chunks[chunkIndex].fromMemory.pBuffer = handle.AddrOfPinnedObject() + buffer.Offset;
+ chunks[chunkIndex].fromMemory.BufferLength = (uint)buffer.Count;
+ chunkIndex++;
+ }
+
+ private void FreeDataBuffers(List<GCHandle> pinnedBuffers)
+ {
+ foreach (var pin in pinnedBuffers)
+ {
+ if (pin.IsAllocated)
+ {
+ pin.Free();
+ }
+ }
+ }
+
+ public override Task FlushAsync(CancellationToken cancellationToken)
+ {
+ if (_disposed)
+ {
+ return Task.CompletedTask;
+ }
+ return FlushInternalAsync(new ArraySegment<byte>(), cancellationToken);
+ }
+
+ // Simpler than Flush because it will never be called at the end of the request from Dispose.
+ private unsafe Task FlushInternalAsync(ArraySegment<byte> data, CancellationToken cancellationToken)
+ {
+ if (_skipWrites)
+ {
+ return Task.CompletedTask;
+ }
+
+ var started = _requestContext.Response.HasStarted;
+ if (data.Count == 0 && started)
+ {
+ // No data to send and we've already sent the headers
+ return Task.CompletedTask;
+ }
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ Abort(ThrowWriteExceptions);
+ return Task.FromCanceled<int>(cancellationToken);
+ }
+
+ // Make sure all validation is performed before this computes the headers
+ var flags = ComputeLeftToWrite(data.Count);
+ uint statusCode = 0;
+ var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
+ var asyncResult = new ResponseStreamAsyncResult(this, data, chunked, cancellationToken);
+ uint bytesSent = 0;
+ try
+ {
+ if (!started)
+ {
+ statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false);
+ bytesSent = asyncResult.BytesSent;
+ }
+ else
+ {
+ statusCode = HttpApi.HttpSendResponseEntityBody(
+ RequestQueueHandle,
+ RequestId,
+ (uint)flags,
+ asyncResult.DataChunkCount,
+ asyncResult.DataChunks,
+ &bytesSent,
+ IntPtr.Zero,
+ 0,
+ asyncResult.NativeOverlapped,
+ IntPtr.Zero);
+ }
+ }
+ catch (Exception e)
+ {
+ LogHelper.LogException(Logger, "FlushAsync", e);
+ asyncResult.Dispose();
+ Abort();
+ throw;
+ }
+
+ if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_IO_PENDING)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ LogHelper.LogDebug(Logger, "FlushAsync", $"Write cancelled with error code: {statusCode}");
+ asyncResult.Cancel(ThrowWriteExceptions);
+ }
+ else if (ThrowWriteExceptions)
+ {
+ asyncResult.Dispose();
+ Exception exception = new IOException(string.Empty, new HttpSysException((int)statusCode));
+ LogHelper.LogException(Logger, "FlushAsync", exception);
+ Abort();
+ throw exception;
+ }
+ else
+ {
+ // Abort the request but do not close the stream, let future writes complete silently
+ LogHelper.LogDebug(Logger, "FlushAsync", $"Ignored write exception: {statusCode}");
+ asyncResult.FailSilently();
+ }
+ }
+
+ if (statusCode == ErrorCodes.ERROR_SUCCESS && HttpSysListener.SkipIOCPCallbackOnSuccess)
+ {
+ // IO operation completed synchronously - callback won't be called to signal completion.
+ asyncResult.IOCompleted(statusCode, bytesSent);
+ }
+
+ // Last write, cache it for special cancellation handling.
+ if ((flags & HttpApiTypes.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0)
+ {
+ _lastWrite = asyncResult;
+ }
+
+ return asyncResult.Task;
+ }
+
+ #region NotSupported Read/Seek
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException(Resources.Exception_NoSeek);
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException(Resources.Exception_NoSeek);
+ }
+
+ public override int Read([In, Out] byte[] buffer, int offset, int count)
+ {
+ throw new InvalidOperationException(Resources.Exception_WriteOnlyStream);
+ }
+
+ public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ throw new InvalidOperationException(Resources.Exception_WriteOnlyStream);
+ }
+
+ public override int EndRead(IAsyncResult asyncResult)
+ {
+ throw new InvalidOperationException(Resources.Exception_WriteOnlyStream);
+ }
+
+ #endregion
+
+ internal void Abort(bool dispose = true)
+ {
+ if (dispose)
+ {
+ _disposed = true;
+ }
+ else
+ {
+ _skipWrites = true;
+ }
+ _requestContext.Abort();
+ }
+
+ private HttpApiTypes.HTTP_FLAGS ComputeLeftToWrite(long writeCount, bool endOfRequest = false)
+ {
+ var flags = HttpApiTypes.HTTP_FLAGS.NONE;
+ if (!_requestContext.Response.HasComputedHeaders)
+ {
+ flags = _requestContext.Response.ComputeHeaders(writeCount, endOfRequest);
+ }
+ if (_leftToWrite == long.MinValue)
+ {
+ if (_requestContext.Request.IsHeadMethod)
+ {
+ _leftToWrite = 0;
+ }
+ else if (_requestContext.Response.BoundaryType == BoundaryType.ContentLength)
+ {
+ _leftToWrite = _requestContext.Response.ExpectedBodyLength;
+ }
+ else
+ {
+ _leftToWrite = -1; // unlimited
+ }
+ }
+
+ if (endOfRequest && _requestContext.Response.BoundaryType == BoundaryType.Close)
+ {
+ flags |= HttpApiTypes.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
+ }
+ else if (!endOfRequest && _leftToWrite != writeCount)
+ {
+ flags |= HttpApiTypes.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
+ }
+
+ // Update _leftToWrite now so we can queue up additional async writes.
+ if (_leftToWrite > 0)
+ {
+ // keep track of the data transferred
+ _leftToWrite -= writeCount;
+ }
+ if (_leftToWrite == 0)
+ {
+ // in this case we already passed 0 as the flag, so we don't need to call HttpSendResponseEntityBody() when we Close()
+ _disposed = true;
+ }
+ // else -1 unlimited
+
+ return flags;
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ if (!RequestContext.AllowSynchronousIO)
+ {
+ throw new InvalidOperationException("Synchronous IO APIs are disabled, see AllowSynchronousIO.");
+ }
+
+ // Validates for null and bounds. Allows count == 0.
+ // TODO: Verbose log parameters
+ var data = new ArraySegment<byte>(buffer, offset, count);
+
+ CheckDisposed();
+
+ CheckWriteCount(count);
+
+ FlushInternal(endOfRequest: false, data: data);
+ }
+
+ private void CheckWriteCount(long? count)
+ {
+ var contentLength = _requestContext.Response.ContentLength;
+ // First write with more bytes written than the entire content-length
+ if (!_requestContext.Response.HasComputedHeaders && contentLength < count)
+ {
+ throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
+ }
+ // A write in a response that has already started where the count exceeds the remainder of the content-length
+ else if (_requestContext.Response.HasComputedHeaders && _requestContext.Response.BoundaryType == BoundaryType.ContentLength
+ && _leftToWrite < count)
+ {
+ throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
+ }
+ }
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ return WriteAsync(buffer, offset, count).ToIAsyncResult(callback, state);
+ }
+
+ public override void EndWrite(IAsyncResult asyncResult)
+ {
+ if (asyncResult == null)
+ {
+ throw new ArgumentNullException(nameof(asyncResult));
+ }
+ ((Task)asyncResult).GetAwaiter().GetResult();
+ }
+
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ // Validates for null and bounds. Allows count == 0.
+ // TODO: Verbose log parameters
+ var data = new ArraySegment<byte>(buffer, offset, count);
+ CheckDisposed();
+
+ CheckWriteCount(count);
+
+ return FlushInternalAsync(data, cancellationToken);
+ }
+
+ internal async Task SendFileAsync(string fileName, long offset, long? count, CancellationToken cancellationToken)
+ {
+ // It's too expensive to validate the file attributes before opening the file. Open the file and then check the lengths.
+ // This all happens inside of ResponseStreamAsyncResult.
+ // TODO: Verbose log parameters
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ throw new ArgumentNullException("fileName");
+ }
+ CheckDisposed();
+
+ CheckWriteCount(count);
+
+ // We can't mix await and unsafe so separate the unsafe code into another method.
+ await SendFileAsyncCore(fileName, offset, count, cancellationToken);
+ }
+
+ internal unsafe Task SendFileAsyncCore(string fileName, long offset, long? count, CancellationToken cancellationToken)
+ {
+ if (_skipWrites)
+ {
+ return Task.CompletedTask;
+ }
+
+ var started = _requestContext.Response.HasStarted;
+ if (count == 0 && started)
+ {
+ // No data to send and we've already sent the headers
+ return Task.CompletedTask;
+ }
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ Abort(ThrowWriteExceptions);
+ return Task.FromCanceled<int>(cancellationToken);
+ }
+
+ // We are setting buffer size to 1 to prevent FileStream from allocating it's internal buffer
+ // It's too expensive to validate anything before opening the file. Open the file and then check the lengths.
+ var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, bufferSize: 1,
+ options: FileOptions.Asynchronous | FileOptions.SequentialScan); // Extremely expensive.
+
+ try
+ {
+ var length = fileStream.Length; // Expensive, only do it once
+ if (!count.HasValue)
+ {
+ count = length - offset;
+ }
+ if (offset < 0 || offset > length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
+ }
+ if (count < 0 || count > length - offset)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
+ }
+
+ CheckWriteCount(count);
+ }
+ catch
+ {
+ fileStream.Dispose();
+ throw;
+ }
+
+ // Make sure all validation is performed before this computes the headers
+ var flags = ComputeLeftToWrite(count.Value);
+ uint statusCode;
+ uint bytesSent = 0;
+ var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
+ var asyncResult = new ResponseStreamAsyncResult(this, fileStream, offset, count.Value, chunked, cancellationToken);
+
+ try
+ {
+ if (!started)
+ {
+ statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false);
+ bytesSent = asyncResult.BytesSent;
+ }
+ else
+ {
+ // TODO: If opaque then include the buffer data flag.
+ statusCode = HttpApi.HttpSendResponseEntityBody(
+ RequestQueueHandle,
+ RequestId,
+ (uint)flags,
+ asyncResult.DataChunkCount,
+ asyncResult.DataChunks,
+ &bytesSent,
+ IntPtr.Zero,
+ 0,
+ asyncResult.NativeOverlapped,
+ IntPtr.Zero);
+ }
+ }
+ catch (Exception e)
+ {
+ LogHelper.LogException(Logger, "SendFileAsync", e);
+ asyncResult.Dispose();
+ Abort();
+ throw;
+ }
+
+ if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ LogHelper.LogDebug(Logger, "SendFileAsync", $"Write cancelled with error code: {statusCode}");
+ asyncResult.Cancel(ThrowWriteExceptions);
+ }
+ else if (ThrowWriteExceptions)
+ {
+ asyncResult.Dispose();
+ var exception = new IOException(string.Empty, new HttpSysException((int)statusCode));
+ LogHelper.LogException(Logger, "SendFileAsync", exception);
+ Abort();
+ throw exception;
+ }
+ else
+ {
+ // Abort the request but do not close the stream, let future writes complete silently
+ LogHelper.LogDebug(Logger, "SendFileAsync", $"Ignored write exception: {statusCode}");
+ asyncResult.FailSilently();
+ }
+ }
+
+ if (statusCode == ErrorCodes.ERROR_SUCCESS && HttpSysListener.SkipIOCPCallbackOnSuccess)
+ {
+ // IO operation completed synchronously - callback won't be called to signal completion.
+ asyncResult.IOCompleted(statusCode, bytesSent);
+ }
+
+ // Last write, cache it for special cancellation handling.
+ if ((flags & HttpApiTypes.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0)
+ {
+ _lastWrite = asyncResult;
+ }
+
+ return asyncResult.Task;
+ }
+
+ protected override unsafe void Dispose(bool disposing)
+ {
+ try
+ {
+ if (disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+ _disposed = true;
+ FlushInternal(endOfRequest: true);
+ }
+ }
+ finally
+ {
+ base.Dispose(disposing);
+ }
+ }
+
+ internal void SwitchToOpaqueMode()
+ {
+ _leftToWrite = -1;
+ }
+
+ // The final Content-Length async write can only be Canceled by CancelIoEx.
+ // Sync can only be Canceled by CancelSynchronousIo, but we don't attempt this right now.
+ [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", Justification =
+ "It is safe to ignore the return value on a cancel operation because the connection is being closed")]
+ internal unsafe void CancelLastWrite()
+ {
+ ResponseStreamAsyncResult asyncState = _lastWrite;
+ if (asyncState != null && !asyncState.IsCompleted)
+ {
+ UnsafeNclNativeMethods.CancelIoEx(RequestQueueHandle, asyncState.NativeOverlapped);
+ }
+ }
+
+ private void CheckDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(GetType().FullName);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/ResponseStreamAsyncResult.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/ResponseStreamAsyncResult.cs
new file mode 100644
index 0000000000..32a90e4b51
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/RequestProcessing/ResponseStreamAsyncResult.cs
@@ -0,0 +1,341 @@
+// 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.CodeAnalysis;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.HttpSys.Internal;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal unsafe class ResponseStreamAsyncResult : IAsyncResult, IDisposable
+ {
+ private static readonly IOCompletionCallback IOCallback = new IOCompletionCallback(Callback);
+
+ private SafeNativeOverlapped _overlapped;
+ private HttpApiTypes.HTTP_DATA_CHUNK[] _dataChunks;
+ private FileStream _fileStream;
+ private ResponseBody _responseStream;
+ private TaskCompletionSource<object> _tcs;
+ private uint _bytesSent;
+ private CancellationToken _cancellationToken;
+ private CancellationTokenRegistration _cancellationRegistration;
+
+ internal ResponseStreamAsyncResult(ResponseBody responseStream, CancellationToken cancellationToken)
+ {
+ _responseStream = responseStream;
+ _tcs = new TaskCompletionSource<object>();
+
+ var cancellationRegistration = default(CancellationTokenRegistration);
+ if (cancellationToken.CanBeCanceled)
+ {
+ cancellationRegistration = _responseStream.RequestContext.RegisterForCancellation(cancellationToken);
+ }
+ _cancellationToken = cancellationToken;
+ _cancellationRegistration = cancellationRegistration;
+ }
+
+ internal ResponseStreamAsyncResult(ResponseBody responseStream, ArraySegment<byte> data, bool chunked,
+ CancellationToken cancellationToken)
+ : this(responseStream, cancellationToken)
+ {
+ var boundHandle = _responseStream.RequestContext.Server.RequestQueue.BoundHandle;
+ object[] objectsToPin;
+
+ if (data.Count == 0)
+ {
+ _dataChunks = null;
+ _overlapped = new SafeNativeOverlapped(boundHandle,
+ boundHandle.AllocateNativeOverlapped(IOCallback, this, null));
+ return;
+ }
+
+ _dataChunks = new HttpApiTypes.HTTP_DATA_CHUNK[1 + (chunked ? 2 : 0)];
+ objectsToPin = new object[_dataChunks.Length + 1];
+ objectsToPin[0] = _dataChunks;
+ var currentChunk = 0;
+ var currentPin = 1;
+
+ var chunkHeaderBuffer = new ArraySegment<byte>();
+ if (chunked)
+ {
+ chunkHeaderBuffer = Helpers.GetChunkHeader(data.Count);
+ SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, chunkHeaderBuffer);
+ }
+
+ SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, data);
+
+ if (chunked)
+ {
+ SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, new ArraySegment<byte>(Helpers.CRLF));
+ }
+
+ // This call will pin needed memory
+ _overlapped = new SafeNativeOverlapped(boundHandle,
+ boundHandle.AllocateNativeOverlapped(IOCallback, this, objectsToPin));
+
+ currentChunk = 0;
+ if (chunked)
+ {
+ _dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer.Array, chunkHeaderBuffer.Offset);
+ currentChunk++;
+ }
+
+ _dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(data.Array, data.Offset);
+ currentChunk++;
+
+ if (chunked)
+ {
+ _dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(Helpers.CRLF, 0);
+ currentChunk++;
+ }
+ }
+
+ internal ResponseStreamAsyncResult(ResponseBody responseStream, FileStream fileStream, long offset,
+ long count, bool chunked, CancellationToken cancellationToken)
+ : this(responseStream, cancellationToken)
+ {
+ var boundHandle = responseStream.RequestContext.Server.RequestQueue.BoundHandle;
+
+ _fileStream = fileStream;
+
+ if (count == 0)
+ {
+ _dataChunks = null;
+ _overlapped = new SafeNativeOverlapped(boundHandle,
+ boundHandle.AllocateNativeOverlapped(IOCallback, this, null));
+ }
+ else
+ {
+ _dataChunks = new HttpApiTypes.HTTP_DATA_CHUNK[chunked ? 3 : 1];
+
+ object[] objectsToPin = new object[_dataChunks.Length];
+ objectsToPin[_dataChunks.Length - 1] = _dataChunks;
+
+ var chunkHeaderBuffer = new ArraySegment<byte>();
+ if (chunked)
+ {
+ chunkHeaderBuffer = Helpers.GetChunkHeader(count);
+ _dataChunks[0].DataChunkType = HttpApiTypes.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
+ _dataChunks[0].fromMemory.BufferLength = (uint)chunkHeaderBuffer.Count;
+ objectsToPin[0] = chunkHeaderBuffer.Array;
+
+ _dataChunks[1].DataChunkType = HttpApiTypes.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle;
+ _dataChunks[1].fromFile.offset = (ulong)offset;
+ _dataChunks[1].fromFile.count = (ulong)count;
+ _dataChunks[1].fromFile.fileHandle = _fileStream.SafeFileHandle.DangerousGetHandle();
+ // Nothing to pin for the file handle.
+
+ _dataChunks[2].DataChunkType = HttpApiTypes.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
+ _dataChunks[2].fromMemory.BufferLength = (uint)Helpers.CRLF.Length;
+ objectsToPin[1] = Helpers.CRLF;
+ }
+ else
+ {
+ _dataChunks[0].DataChunkType = HttpApiTypes.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle;
+ _dataChunks[0].fromFile.offset = (ulong)offset;
+ _dataChunks[0].fromFile.count = (ulong)count;
+ _dataChunks[0].fromFile.fileHandle = _fileStream.SafeFileHandle.DangerousGetHandle();
+ }
+
+ // This call will pin needed memory
+ _overlapped = new SafeNativeOverlapped(boundHandle,
+ boundHandle.AllocateNativeOverlapped(IOCallback, this, objectsToPin));
+
+ if (chunked)
+ {
+ // These must be set after pinning with Overlapped.
+ _dataChunks[0].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer.Array, chunkHeaderBuffer.Offset);
+ _dataChunks[2].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(Helpers.CRLF, 0);
+ }
+ }
+ }
+
+ private static void SetDataChunk(HttpApiTypes.HTTP_DATA_CHUNK[] chunks, ref int chunkIndex, object[] objectsToPin, ref int pinIndex, ArraySegment<byte> segment)
+ {
+ objectsToPin[pinIndex] = segment.Array;
+ pinIndex++;
+ chunks[chunkIndex].DataChunkType = HttpApiTypes.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
+ // The address is not set until after we pin it with Overlapped
+ chunks[chunkIndex].fromMemory.BufferLength = (uint)segment.Count;
+ chunkIndex++;
+ }
+
+ internal SafeNativeOverlapped NativeOverlapped
+ {
+ get { return _overlapped; }
+ }
+
+ internal Task Task
+ {
+ get { return _tcs.Task; }
+ }
+
+ internal uint BytesSent
+ {
+ get { return _bytesSent; }
+ set { _bytesSent = value; }
+ }
+
+ internal ushort DataChunkCount
+ {
+ get
+ {
+ if (_dataChunks == null)
+ {
+ return 0;
+ }
+ else
+ {
+ return (ushort)_dataChunks.Length;
+ }
+ }
+ }
+
+ internal HttpApiTypes.HTTP_DATA_CHUNK* DataChunks
+ {
+ get
+ {
+ if (_dataChunks == null)
+ {
+ return null;
+ }
+ else
+ {
+ return (HttpApiTypes.HTTP_DATA_CHUNK*)(Marshal.UnsafeAddrOfPinnedArrayElement(_dataChunks, 0));
+ }
+ }
+ }
+
+ internal bool EndCalled { get; set; }
+
+ internal void IOCompleted(uint errorCode)
+ {
+ IOCompleted(this, errorCode, BytesSent);
+ }
+
+ internal void IOCompleted(uint errorCode, uint numBytes)
+ {
+ IOCompleted(this, errorCode, numBytes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Redirecting to callback")]
+ private static void IOCompleted(ResponseStreamAsyncResult asyncResult, uint errorCode, uint numBytes)
+ {
+ var logger = asyncResult._responseStream.RequestContext.Logger;
+ try
+ {
+ if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
+ {
+ if (asyncResult._cancellationToken.IsCancellationRequested)
+ {
+ LogHelper.LogDebug(logger, "FlushAsync.IOCompleted", $"Write cancelled with error code: {errorCode}");
+ asyncResult.Cancel(asyncResult._responseStream.ThrowWriteExceptions);
+ }
+ else if (asyncResult._responseStream.ThrowWriteExceptions)
+ {
+ var exception = new IOException(string.Empty, new HttpSysException((int)errorCode));
+ LogHelper.LogException(logger, "FlushAsync.IOCompleted", exception);
+ asyncResult.Fail(exception);
+ }
+ else
+ {
+ LogHelper.LogDebug(logger, "FlushAsync.IOCompleted", $"Ignored write exception: {errorCode}");
+ asyncResult.FailSilently();
+ }
+ }
+ else
+ {
+ if (asyncResult._dataChunks == null)
+ {
+ // TODO: Verbose log data written
+ }
+ else
+ {
+ // TODO: Verbose log
+ // for (int i = 0; i < asyncResult._dataChunks.Length; i++)
+ // {
+ // Logging.Dump(Logging.HttpListener, asyncResult, "Callback", (IntPtr)asyncResult._dataChunks[0].fromMemory.pBuffer, (int)asyncResult._dataChunks[0].fromMemory.BufferLength);
+ // }
+ }
+ asyncResult.Complete();
+ }
+ }
+ catch (Exception e)
+ {
+ LogHelper.LogException(logger, "FlushAsync.IOCompleted", e);
+ asyncResult.Fail(e);
+ }
+ }
+
+ private static unsafe void Callback(uint errorCode, uint numBytes, NativeOverlapped* nativeOverlapped)
+ {
+ var asyncResult = (ResponseStreamAsyncResult)ThreadPoolBoundHandle.GetNativeOverlappedState(nativeOverlapped);
+ IOCompleted(asyncResult, errorCode, numBytes);
+ }
+
+ internal void Complete()
+ {
+ Dispose();
+ _tcs.TrySetResult(null);
+ }
+
+ internal void FailSilently()
+ {
+ Dispose();
+ // Abort the request but do not close the stream, let future writes complete silently
+ _responseStream.Abort(dispose: false);
+ _tcs.TrySetResult(null);
+ }
+
+ internal void Cancel(bool dispose)
+ {
+ Dispose();
+ _responseStream.Abort(dispose);
+ _tcs.TrySetCanceled();
+ }
+
+ internal void Fail(Exception ex)
+ {
+ Dispose();
+ _responseStream.Abort();
+ _tcs.TrySetException(ex);
+ }
+
+ public object AsyncState
+ {
+ get { return _tcs.Task.AsyncState; }
+ }
+
+ public WaitHandle AsyncWaitHandle
+ {
+ get { return ((IAsyncResult)_tcs.Task).AsyncWaitHandle; }
+ }
+
+ public bool CompletedSynchronously
+ {
+ get { return ((IAsyncResult)_tcs.Task).CompletedSynchronously; }
+ }
+
+ public bool IsCompleted
+ {
+ get { return _tcs.Task.IsCompleted; }
+ }
+
+ public void Dispose()
+ {
+ if (_overlapped != null)
+ {
+ _overlapped.Dispose();
+ }
+ if (_fileStream != null)
+ {
+ _fileStream.Dispose();
+ }
+ _cancellationRegistration.Dispose();
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Resources.resx b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Resources.resx
new file mode 100644
index 0000000000..67b954a934
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/Resources.resx
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Exception_ArrayTooSmall" xml:space="preserve">
+ <value>The destination array is too small.</value>
+ </data>
+ <data name="Exception_EndCalledMultipleTimes" xml:space="preserve">
+ <value>End has already been called.</value>
+ </data>
+ <data name="Exception_InvalidStatusCode" xml:space="preserve">
+ <value>The status code '{0}' is not supported.</value>
+ </data>
+ <data name="Exception_NoSeek" xml:space="preserve">
+ <value>The stream is not seekable.</value>
+ </data>
+ <data name="Exception_PrefixAlreadyRegistered" xml:space="preserve">
+ <value>The prefix '{0}' is already registered.</value>
+ </data>
+ <data name="Exception_ReadOnlyStream" xml:space="preserve">
+ <value>This stream only supports read operations.</value>
+ </data>
+ <data name="Exception_TooMuchWritten" xml:space="preserve">
+ <value>More data written than specified in the Content-Length header.</value>
+ </data>
+ <data name="Exception_UnsupportedScheme" xml:space="preserve">
+ <value>Only the http and https schemes are supported.</value>
+ </data>
+ <data name="Exception_WriteOnlyStream" xml:space="preserve">
+ <value>This stream only supports write operations.</value>
+ </data>
+ <data name="Exception_WrongIAsyncResult" xml:space="preserve">
+ <value>The given IAsyncResult does not match this opperation.</value>
+ </data>
+ <data name="Warning_ExceptionInOnResponseCompletedAction" xml:space="preserve">
+ <value>An exception occured while running an action registered with {0}.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/ResponseStream.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/ResponseStream.cs
new file mode 100644
index 0000000000..130303482d
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/ResponseStream.cs
@@ -0,0 +1,115 @@
+// 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 System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal class ResponseStream : Stream
+ {
+ private readonly Stream _innerStream;
+ private readonly Func<Task> _onStart;
+
+ internal ResponseStream(Stream innerStream, Func<Task> onStart)
+ {
+ _innerStream = innerStream;
+ _onStart = onStart;
+ }
+
+ public override bool CanRead => _innerStream.CanRead;
+
+ public override bool CanSeek => _innerStream.CanSeek;
+
+ public override bool CanWrite => _innerStream.CanWrite;
+
+ public override long Length => _innerStream.Length;
+
+ public override long Position
+ {
+ get { return _innerStream.Position; }
+ set { _innerStream.Position = value; }
+ }
+
+ public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);
+
+ public override void SetLength(long value) => _innerStream.SetLength(value);
+
+ public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count);
+
+ public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ return _innerStream.BeginRead(buffer, offset, count, callback, state);
+ }
+
+ public override int EndRead(IAsyncResult asyncResult)
+ {
+ return _innerStream.EndRead(asyncResult);
+ }
+ public override void Flush()
+ {
+ _onStart().GetAwaiter().GetResult();
+ _innerStream.Flush();
+ }
+
+ public override async Task FlushAsync(CancellationToken cancellationToken)
+ {
+ await _onStart();
+ await _innerStream.FlushAsync(cancellationToken);
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ _onStart().GetAwaiter().GetResult();
+ _innerStream.Write(buffer, offset, count);
+ }
+
+ public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ await _onStart();
+ await _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
+ }
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ return ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state);
+ }
+
+ public override void EndWrite(IAsyncResult asyncResult)
+ {
+ if (asyncResult == null)
+ {
+ throw new ArgumentNullException(nameof(asyncResult));
+ }
+ ((Task)asyncResult).GetAwaiter().GetResult();
+ }
+
+ private static IAsyncResult ToIAsyncResult(Task task, AsyncCallback callback, object state)
+ {
+ var tcs = new TaskCompletionSource<int>(state);
+ task.ContinueWith(t =>
+ {
+ if (t.IsFaulted)
+ {
+ tcs.TrySetException(t.Exception.InnerExceptions);
+ }
+ else if (t.IsCanceled)
+ {
+ tcs.TrySetCanceled();
+ }
+ else
+ {
+ tcs.TrySetResult(0);
+ }
+
+ if (callback != null)
+ {
+ callback(tcs.Task);
+ }
+ }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
+ return tcs.Task;
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/StandardFeatureCollection.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/StandardFeatureCollection.cs
new file mode 100644
index 0000000000..2adf51d1fc
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/StandardFeatureCollection.cs
@@ -0,0 +1,106 @@
+// 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;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Http.Features.Authentication;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal sealed class StandardFeatureCollection : IFeatureCollection
+ {
+ private static readonly Func<FeatureContext, object> _identityFunc = ReturnIdentity;
+ private static readonly Dictionary<Type, Func<FeatureContext, object>> _featureFuncLookup = new Dictionary<Type, Func<FeatureContext, object>>()
+ {
+ { typeof(IHttpRequestFeature), _identityFunc },
+ { typeof(IHttpConnectionFeature), _identityFunc },
+ { typeof(IHttpResponseFeature), _identityFunc },
+ { typeof(IHttpSendFileFeature), _identityFunc },
+ { typeof(ITlsConnectionFeature), ctx => ctx.GetTlsConnectionFeature() },
+ // { typeof(ITlsTokenBindingFeature), ctx => ctx.GetTlsTokenBindingFeature() }, TODO: https://github.com/aspnet/HttpSysServer/issues/231
+ { typeof(IHttpBufferingFeature), _identityFunc },
+ { typeof(IHttpRequestLifetimeFeature), _identityFunc },
+ { typeof(IHttpAuthenticationFeature), _identityFunc },
+ { typeof(IHttpRequestIdentifierFeature), _identityFunc },
+ { typeof(RequestContext), ctx => ctx.RequestContext },
+ { typeof(IHttpMaxRequestBodySizeFeature), _identityFunc },
+ { typeof(IHttpBodyControlFeature), _identityFunc },
+ };
+
+ private readonly FeatureContext _featureContext;
+
+ static StandardFeatureCollection()
+ {
+ if (ComNetOS.IsWin8orLater)
+ {
+ // Only add the upgrade feature if it stands a chance of working.
+ // SignalR uses the presence of the feature to detect feature support.
+ // https://github.com/aspnet/HttpSysServer/issues/427
+ _featureFuncLookup[typeof(IHttpUpgradeFeature)] = _identityFunc;
+ }
+ }
+
+ public StandardFeatureCollection(FeatureContext featureContext)
+ {
+ _featureContext = featureContext;
+ }
+
+ public bool IsReadOnly
+ {
+ get { return true; }
+ }
+
+ public int Revision
+ {
+ get { return 0; }
+ }
+
+ public object this[Type key]
+ {
+ get
+ {
+ Func<FeatureContext, object> lookupFunc;
+ _featureFuncLookup.TryGetValue(key, out lookupFunc);
+ return lookupFunc?.Invoke(_featureContext);
+ }
+ set
+ {
+ throw new InvalidOperationException("The collection is read-only");
+ }
+ }
+
+ private static object ReturnIdentity(FeatureContext featureContext)
+ {
+ return featureContext;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable<KeyValuePair<Type, object>>)this).GetEnumerator();
+ }
+
+ IEnumerator<KeyValuePair<Type, object>> IEnumerable<KeyValuePair<Type, object>>.GetEnumerator()
+ {
+ foreach (var featureFunc in _featureFuncLookup)
+ {
+ var feature = featureFunc.Value(_featureContext);
+ if (feature != null)
+ {
+ yield return new KeyValuePair<Type, object>(featureFunc.Key, feature);
+ }
+ }
+ }
+
+ public TFeature Get<TFeature>()
+ {
+ return (TFeature)this[typeof(TFeature)];
+ }
+
+ public void Set<TFeature>(TFeature instance)
+ {
+ this[typeof(TFeature)] = instance;
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/TimeoutManager.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/TimeoutManager.cs
new file mode 100644
index 0000000000..e9bb62dfce
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/TimeoutManager.cs
@@ -0,0 +1,235 @@
+// 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.Runtime.InteropServices;
+using Microsoft.AspNetCore.HttpSys.Internal;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ // See the native HTTP_TIMEOUT_LIMIT_INFO structure documentation for additional information.
+ // http://msdn.microsoft.com/en-us/library/aa364661.aspx
+
+ /// <summary>
+ /// Exposes the Http.Sys timeout configurations. These may also be configured in the registry.
+ /// </summary>
+ public sealed class TimeoutManager
+ {
+ private static readonly int TimeoutLimitSize =
+ Marshal.SizeOf<HttpApiTypes.HTTP_TIMEOUT_LIMIT_INFO>();
+
+ private UrlGroup _urlGroup;
+ private int[] _timeouts;
+ private uint _minSendBytesPerSecond;
+
+ internal TimeoutManager()
+ {
+ // We have to maintain local state since we allow applications to set individual timeouts. Native Http
+ // API for setting timeouts expects all timeout values in every call so we have remember timeout values
+ // to fill in the blanks. Except MinSendBytesPerSecond, local state for remaining five timeouts is
+ // maintained in timeouts array.
+ //
+ // No initialization is required because a value of zero indicates that system defaults should be used.
+ _timeouts = new int[5];
+ }
+
+ #region Properties
+
+ /// <summary>
+ /// The time, in seconds, allowed for the request entity body to arrive. The default timer is 2 minutes.
+ ///
+ /// The HTTP Server API turns on this timer when the request has an entity body. The timer expiration is
+ /// initially set to the configured value. When the HTTP Server API receives additional data indications on the
+ /// request, it resets the timer to give the connection another interval.
+ ///
+ /// Use TimeSpan.Zero to indicate that system defaults should be used.
+ /// </summary>
+ public TimeSpan EntityBody
+ {
+ get
+ {
+ return GetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.EntityBody);
+ }
+ set
+ {
+ SetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.EntityBody, value);
+ }
+ }
+
+ /// <summary>
+ /// The time, in seconds, allowed for the HTTP Server API to drain the entity body on a Keep-Alive connection.
+ /// The default timer is 2 minutes.
+ ///
+ /// On a Keep-Alive connection, after the application has sent a response for a request and before the request
+ /// entity body has completely arrived, the HTTP Server API starts draining the remainder of the entity body to
+ /// reach another potentially pipelined request from the client. If the time to drain the remaining entity body
+ /// exceeds the allowed period the connection is timed out.
+ ///
+ /// Use TimeSpan.Zero to indicate that system defaults should be used.
+ /// </summary>
+ public TimeSpan DrainEntityBody
+ {
+ get
+ {
+ return GetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.DrainEntityBody);
+ }
+ set
+ {
+ SetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.DrainEntityBody, value);
+ }
+ }
+
+ /// <summary>
+ /// The time, in seconds, allowed for the request to remain in the request queue before the application picks
+ /// it up. The default timer is 2 minutes.
+ ///
+ /// Use TimeSpan.Zero to indicate that system defaults should be used.
+ /// </summary>
+ public TimeSpan RequestQueue
+ {
+ get
+ {
+ return GetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.RequestQueue);
+ }
+ set
+ {
+ SetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.RequestQueue, value);
+ }
+ }
+
+ /// <summary>
+ /// The time, in seconds, allowed for an idle connection. The default timer is 2 minutes.
+ ///
+ /// This timeout is only enforced after the first request on the connection is routed to the application.
+ ///
+ /// Use TimeSpan.Zero to indicate that system defaults should be used.
+ /// </summary>
+ public TimeSpan IdleConnection
+ {
+ get
+ {
+ return GetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.IdleConnection);
+ }
+ set
+ {
+ SetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.IdleConnection, value);
+ }
+ }
+
+ /// <summary>
+ /// The time, in seconds, allowed for the HTTP Server API to parse the request header. The default timer is
+ /// 2 minutes.
+ ///
+ /// This timeout is only enforced after the first request on the connection is routed to the application.
+ ///
+ /// Use TimeSpan.Zero to indicate that system defaults should be used.
+ /// </summary>
+ public TimeSpan HeaderWait
+ {
+ get
+ {
+ return GetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.HeaderWait);
+ }
+ set
+ {
+ SetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE.HeaderWait, value);
+ }
+ }
+
+ /// <summary>
+ /// The minimum send rate, in bytes-per-second, for the response. The default response send rate is 150
+ /// bytes-per-second.
+ ///
+ /// Use 0 to indicate that system defaults should be used.
+ ///
+ /// To disable this timer set it to UInt32.MaxValue
+ /// </summary>
+ public long MinSendBytesPerSecond
+ {
+ get
+ {
+ // Since we maintain local state, GET is local.
+ return _minSendBytesPerSecond;
+ }
+ set
+ {
+ // MinSendRate value is ULONG in native layer.
+ if (value < 0 || value > uint.MaxValue)
+ {
+ throw new ArgumentOutOfRangeException("value");
+ }
+
+ SetUrlGroupTimeouts(_timeouts, (uint)value);
+ _minSendBytesPerSecond = (uint)value;
+ }
+ }
+
+ #endregion Properties
+
+ #region Helpers
+
+ private TimeSpan GetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE type)
+ {
+ // Since we maintain local state, GET is local.
+ return new TimeSpan(0, 0, (int)_timeouts[(int)type]);
+ }
+
+ private void SetTimeSpanTimeout(HttpApiTypes.HTTP_TIMEOUT_TYPE type, TimeSpan value)
+ {
+ // All timeouts are defined as USHORT in native layer (except MinSendRate, which is ULONG). Make sure that
+ // timeout value is within range.
+
+ var timeoutValue = Convert.ToInt64(value.TotalSeconds);
+
+ if (timeoutValue < 0 || timeoutValue > ushort.MaxValue)
+ {
+ throw new ArgumentOutOfRangeException("value");
+ }
+
+ // Use local state to get values for other timeouts. Call into the native layer and if that
+ // call succeeds, update local state.
+ var newTimeouts = (int[])_timeouts.Clone();
+ newTimeouts[(int)type] = (int)timeoutValue;
+ SetUrlGroupTimeouts(newTimeouts, _minSendBytesPerSecond);
+ _timeouts[(int)type] = (int)timeoutValue;
+ }
+
+ internal void SetUrlGroupTimeouts(UrlGroup urlGroup)
+ {
+ _urlGroup = urlGroup;
+ SetUrlGroupTimeouts(_timeouts, _minSendBytesPerSecond);
+ }
+
+ private unsafe void SetUrlGroupTimeouts(int[] timeouts, uint minSendBytesPerSecond)
+ {
+ if (_urlGroup == null)
+ {
+ // Not started yet
+ return;
+ }
+
+ var timeoutinfo = new HttpApiTypes.HTTP_TIMEOUT_LIMIT_INFO();
+
+ timeoutinfo.Flags = HttpApiTypes.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT;
+ timeoutinfo.DrainEntityBody =
+ (ushort)timeouts[(int)HttpApiTypes.HTTP_TIMEOUT_TYPE.DrainEntityBody];
+ timeoutinfo.EntityBody =
+ (ushort)timeouts[(int)HttpApiTypes.HTTP_TIMEOUT_TYPE.EntityBody];
+ timeoutinfo.RequestQueue =
+ (ushort)timeouts[(int)HttpApiTypes.HTTP_TIMEOUT_TYPE.RequestQueue];
+ timeoutinfo.IdleConnection =
+ (ushort)timeouts[(int)HttpApiTypes.HTTP_TIMEOUT_TYPE.IdleConnection];
+ timeoutinfo.HeaderWait =
+ (ushort)timeouts[(int)HttpApiTypes.HTTP_TIMEOUT_TYPE.HeaderWait];
+ timeoutinfo.MinSendRate = minSendBytesPerSecond;
+
+ var infoptr = new IntPtr(&timeoutinfo);
+
+ _urlGroup.SetProperty(
+ HttpApiTypes.HTTP_SERVER_PROPERTY.HttpServerTimeoutsProperty,
+ infoptr, (uint)TimeoutLimitSize);
+ }
+
+ #endregion Helpers
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/UrlPrefix.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/UrlPrefix.cs
new file mode 100644
index 0000000000..4286870d71
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/UrlPrefix.cs
@@ -0,0 +1,170 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.HttpSys.Internal;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class UrlPrefix
+ {
+ private UrlPrefix(bool isHttps, string scheme, string host, string port, int portValue, string path)
+ {
+ IsHttps = isHttps;
+ Scheme = scheme;
+ Host = host;
+ Port = port;
+ PortValue = portValue;
+ Path = path;
+ FullPrefix = string.Format(CultureInfo.InvariantCulture, "{0}://{1}:{2}{3}", Scheme, Host, Port, Path);
+ }
+
+ /// <summary>
+ /// http://msdn.microsoft.com/en-us/library/windows/desktop/aa364698(v=vs.85).aspx
+ /// </summary>
+ /// <param name="scheme">http or https. Will be normalized to lower case.</param>
+ /// <param name="host">+, *, IPv4, [IPv6], or a dns name. Http.Sys does not permit punycode (xn--), use Unicode instead.</param>
+ /// <param name="port">If empty, the default port for the given scheme will be used (80 or 443).</param>
+ /// <param name="path">Should start and end with a '/', though a missing trailing slash will be added. This value must be un-escaped.</param>
+ public static UrlPrefix Create(string scheme, string host, string port, string path)
+ {
+ int? portValue = null;
+ if (!string.IsNullOrWhiteSpace(port))
+ {
+ portValue = int.Parse(port, NumberStyles.None, CultureInfo.InvariantCulture);
+ }
+
+ return UrlPrefix.Create(scheme, host, portValue, path);
+ }
+
+ /// <summary>
+ /// http://msdn.microsoft.com/en-us/library/windows/desktop/aa364698(v=vs.85).aspx
+ /// </summary>
+ /// <param name="scheme">http or https. Will be normalized to lower case.</param>
+ /// <param name="host">+, *, IPv4, [IPv6], or a dns name. Http.Sys does not permit punycode (xn--), use Unicode instead.</param>
+ /// <param name="portValue">If empty, the default port for the given scheme will be used (80 or 443).</param>
+ /// <param name="path">Should start and end with a '/', though a missing trailing slash will be added. This value must be un-escaped.</param>
+ public static UrlPrefix Create(string scheme, string host, int? portValue, string path)
+ {
+ bool isHttps;
+ if (string.Equals(Constants.HttpScheme, scheme, StringComparison.OrdinalIgnoreCase))
+ {
+ scheme = Constants.HttpScheme; // Always use a lower case scheme
+ isHttps = false;
+ }
+ else if (string.Equals(Constants.HttpsScheme, scheme, StringComparison.OrdinalIgnoreCase))
+ {
+ scheme = Constants.HttpsScheme; // Always use a lower case scheme
+ isHttps = true;
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException("scheme", scheme, Resources.Exception_UnsupportedScheme);
+ }
+
+ if (string.IsNullOrWhiteSpace(host))
+ {
+ throw new ArgumentNullException("host");
+ }
+
+ string port;
+ if (!portValue.HasValue)
+ {
+ port = isHttps ? "443" : "80";
+ portValue = isHttps ? 443 : 80;
+ }
+ else
+ {
+ port = portValue.Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ // Http.Sys requires the path end with a slash.
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ path = "/";
+ }
+ else if (!path.EndsWith("/", StringComparison.Ordinal))
+ {
+ path += "/";
+ }
+
+ return new UrlPrefix(isHttps, scheme, host, port, portValue.Value, path);
+ }
+
+ public static UrlPrefix Create(string prefix)
+ {
+ string scheme = null;
+ string host = null;
+ int? port = null;
+ string path = null;
+ var whole = prefix ?? string.Empty;
+
+ var schemeDelimiterEnd = whole.IndexOf("://", StringComparison.Ordinal);
+ if (schemeDelimiterEnd < 0)
+ {
+ throw new FormatException("Invalid prefix, missing scheme separator: " + prefix);
+ }
+ var hostDelimiterStart = schemeDelimiterEnd + "://".Length;
+
+ var pathDelimiterStart = whole.IndexOf("/", hostDelimiterStart, StringComparison.Ordinal);
+ if (pathDelimiterStart < 0)
+ {
+ pathDelimiterStart = whole.Length;
+ }
+ var hostDelimiterEnd = whole.LastIndexOf(":", pathDelimiterStart - 1, pathDelimiterStart - hostDelimiterStart, StringComparison.Ordinal);
+ if (hostDelimiterEnd < 0)
+ {
+ hostDelimiterEnd = pathDelimiterStart;
+ }
+
+ scheme = whole.Substring(0, schemeDelimiterEnd);
+ var portString = whole.Substring(hostDelimiterEnd, pathDelimiterStart - hostDelimiterEnd); // The leading ":" is included
+ int portValue;
+ if (!string.IsNullOrEmpty(portString))
+ {
+ var portValueString = portString.Substring(1); // Trim the leading ":"
+ if (int.TryParse(portValueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out portValue))
+ {
+ host = whole.Substring(hostDelimiterStart, hostDelimiterEnd - hostDelimiterStart);
+ port = portValue;
+ }
+ else
+ {
+ // This means a port was specified but was invalid or empty.
+ throw new FormatException("Invalid prefix, invalid port specified: " + prefix);
+ }
+ }
+ else
+ {
+ host = whole.Substring(hostDelimiterStart, pathDelimiterStart - hostDelimiterStart);
+ }
+ path = whole.Substring(pathDelimiterStart);
+
+ return Create(scheme, host, port, path);
+ }
+
+ public bool IsHttps { get; private set; }
+ public string Scheme { get; private set; }
+ public string Host { get; private set; }
+ public string Port { get; private set; }
+ public int PortValue { get; private set; }
+ public string Path { get; private set; }
+ public string FullPrefix { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ return string.Equals(FullPrefix, Convert.ToString(obj), StringComparison.OrdinalIgnoreCase);
+ }
+
+ public override int GetHashCode()
+ {
+ return StringComparer.OrdinalIgnoreCase.GetHashCode(FullPrefix);
+ }
+
+ public override string ToString()
+ {
+ return FullPrefix;
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/UrlPrefixCollection.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/UrlPrefixCollection.cs
new file mode 100644
index 0000000000..92c50aea09
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/UrlPrefixCollection.cs
@@ -0,0 +1,162 @@
+// 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;
+using System.Collections.Generic;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ /// <summary>
+ /// A collection or URL prefixes
+ /// </summary>
+ public class UrlPrefixCollection : ICollection<UrlPrefix>
+ {
+ private readonly IDictionary<int, UrlPrefix> _prefixes = new Dictionary<int, UrlPrefix>(1);
+ private UrlGroup _urlGroup;
+ private int _nextId = 1;
+
+ internal UrlPrefixCollection()
+ {
+ }
+
+ public int Count
+ {
+ get
+ {
+ lock (_prefixes)
+ {
+ return _prefixes.Count;
+ }
+ }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return false; }
+ }
+
+ public void Add(string prefix)
+ {
+ Add(UrlPrefix.Create(prefix));
+ }
+
+ public void Add(UrlPrefix item)
+ {
+ lock (_prefixes)
+ {
+ var id = _nextId++;
+ if (_urlGroup != null)
+ {
+ _urlGroup.RegisterPrefix(item.FullPrefix, id);
+ }
+ _prefixes.Add(id, item);
+ }
+ }
+
+ internal UrlPrefix GetPrefix(int id)
+ {
+ lock (_prefixes)
+ {
+ return _prefixes[id];
+ }
+ }
+
+ public void Clear()
+ {
+ lock (_prefixes)
+ {
+ if (_urlGroup != null)
+ {
+ UnregisterAllPrefixes();
+ }
+ _prefixes.Clear();
+ }
+ }
+
+ public bool Contains(UrlPrefix item)
+ {
+ lock (_prefixes)
+ {
+ return _prefixes.Values.Contains(item);
+ }
+ }
+
+ public void CopyTo(UrlPrefix[] array, int arrayIndex)
+ {
+ lock (_prefixes)
+ {
+ _prefixes.Values.CopyTo(array, arrayIndex);
+ }
+ }
+
+ public bool Remove(string prefix)
+ {
+ return Remove(UrlPrefix.Create(prefix));
+ }
+
+ public bool Remove(UrlPrefix item)
+ {
+ lock (_prefixes)
+ {
+ int? id = null;
+ foreach (var pair in _prefixes)
+ {
+ if (pair.Value.Equals(item))
+ {
+ id = pair.Key;
+ if (_urlGroup != null)
+ {
+ _urlGroup.UnregisterPrefix(pair.Value.FullPrefix);
+ }
+ }
+ }
+ if (id.HasValue)
+ {
+ _prefixes.Remove(id.Value);
+ return true;
+ }
+ return false;
+ }
+ }
+
+ public IEnumerator<UrlPrefix> GetEnumerator()
+ {
+ lock (_prefixes)
+ {
+ return _prefixes.Values.GetEnumerator();
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ internal void RegisterAllPrefixes(UrlGroup urlGroup)
+ {
+ lock (_prefixes)
+ {
+ _urlGroup = urlGroup;
+ // go through the uri list and register for each one of them
+ foreach (var pair in _prefixes)
+ {
+ // We'll get this index back on each request and use it to look up the prefix to calculate PathBase.
+ _urlGroup.RegisterPrefix(pair.Value.FullPrefix, pair.Key);
+ }
+ }
+ }
+
+ internal void UnregisterAllPrefixes()
+ {
+ lock (_prefixes)
+ {
+ // go through the uri list and unregister for each one of them
+ foreach (var prefix in _prefixes.Values)
+ {
+ // ignore possible failures
+ _urlGroup.UnregisterPrefix(prefix.FullPrefix);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/ValidationHelper.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/ValidationHelper.cs
new file mode 100644
index 0000000000..7d4c10d7d0
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/ValidationHelper.cs
@@ -0,0 +1,64 @@
+// 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.Globalization;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal static class ValidationHelper
+ {
+ public static string ExceptionMessage(Exception exception)
+ {
+ if (exception == null)
+ {
+ return string.Empty;
+ }
+ if (exception.InnerException == null)
+ {
+ return exception.Message;
+ }
+ return exception.Message + " (" + ExceptionMessage(exception.InnerException) + ")";
+ }
+
+ public static string ToString(object objectValue)
+ {
+ if (objectValue == null)
+ {
+ return "(null)";
+ }
+ else if (objectValue is string && ((string)objectValue).Length == 0)
+ {
+ return "(string.empty)";
+ }
+ else if (objectValue is Exception)
+ {
+ return ExceptionMessage(objectValue as Exception);
+ }
+ else if (objectValue is IntPtr)
+ {
+ return "0x" + ((IntPtr)objectValue).ToString("x");
+ }
+ else
+ {
+ return objectValue.ToString();
+ }
+ }
+
+ public static string HashString(object objectValue)
+ {
+ if (objectValue == null)
+ {
+ return "(null)";
+ }
+ else if (objectValue is string && ((string)objectValue).Length == 0)
+ {
+ return "(string.empty)";
+ }
+ else
+ {
+ return objectValue.GetHashCode().ToString(NumberFormatInfo.InvariantInfo);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/WebHostBuilderHttpSysExtensions.cs b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/WebHostBuilderHttpSysExtensions.cs
new file mode 100644
index 0000000000..d7042643b9
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/WebHostBuilderHttpSysExtensions.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 System;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Server.HttpSys;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Hosting
+{
+ public static class WebHostBuilderHttpSysExtensions
+ {
+ /// <summary>
+ /// Specify HttpSys as the server to be used by the web host.
+ /// </summary>
+ /// <param name="hostBuilder">
+ /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder to configure.
+ /// </param>
+ /// <returns>
+ /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder.
+ /// </returns>
+ public static IWebHostBuilder UseHttpSys(this IWebHostBuilder hostBuilder)
+ {
+ return hostBuilder.ConfigureServices(services => {
+ services.AddSingleton<IServer, MessagePump>();
+ services.AddAuthenticationCore();
+ });
+ }
+
+ /// <summary>
+ /// Specify HttpSys as the server to be used by the web host.
+ /// </summary>
+ /// <param name="hostBuilder">
+ /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder to configure.
+ /// </param>
+ /// <param name="options">
+ /// A callback to configure HttpSys options.
+ /// </param>
+ /// <returns>
+ /// The Microsoft.AspNetCore.Hosting.IWebHostBuilder.
+ /// </returns>
+ public static IWebHostBuilder UseHttpSys(this IWebHostBuilder hostBuilder, Action<HttpSysOptions> options)
+ {
+ return hostBuilder.UseHttpSys().ConfigureServices(services =>
+ {
+ services.Configure(options);
+ });
+ }
+ }
+}
diff --git a/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/baseline.netcore.json b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/baseline.netcore.json
new file mode 100644
index 0000000000..a2a3a393fa
--- /dev/null
+++ b/src/HttpSysServer/src/Microsoft.AspNetCore.Server.HttpSys/baseline.netcore.json
@@ -0,0 +1,881 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Server.HttpSys, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.Hosting.WebHostBuilderHttpSysExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseHttpSys",
+ "Parameters": [
+ {
+ "Name": "hostBuilder",
+ "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseHttpSys",
+ "Parameters": [
+ {
+ "Name": "hostBuilder",
+ "Type": "Microsoft.AspNetCore.Hosting.IWebHostBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "System.Action<Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Hosting.IWebHostBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Server.HttpSys.AuthenticationManager",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Schemes",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Server.HttpSys.AuthenticationSchemes",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Schemes",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Server.HttpSys.AuthenticationSchemes"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AllowAnonymous",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AllowAnonymous",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Server.HttpSys.AuthenticationSchemes",
+ "Visibility": "Public",
+ "Kind": "Enumeration",
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "None",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "0"
+ },
+ {
+ "Kind": "Field",
+ "Name": "Basic",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "1"
+ },
+ {
+ "Kind": "Field",
+ "Name": "NTLM",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "4"
+ },
+ {
+ "Kind": "Field",
+ "Name": "Negotiate",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "8"
+ },
+ {
+ "Kind": "Field",
+ "Name": "Kerberos",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "16"
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Server.HttpSys.Http503VerbosityLevel",
+ "Visibility": "Public",
+ "Kind": "Enumeration",
+ "Sealed": true,
+ "BaseType": "System.Int64",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "Basic",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "0"
+ },
+ {
+ "Kind": "Field",
+ "Name": "Limited",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "1"
+ },
+ {
+ "Kind": "Field",
+ "Name": "Full",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "2"
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Server.HttpSys.HttpSysDefaults",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "AuthenticationScheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Server.HttpSys.HttpSysException",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "System.ComponentModel.Win32Exception",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ErrorCode",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_MaxAccepts",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_MaxAccepts",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_EnableResponseCaching",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EnableResponseCaching",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_UrlPrefixes",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Server.HttpSys.UrlPrefixCollection",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Authentication",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Server.HttpSys.AuthenticationManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Timeouts",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Server.HttpSys.TimeoutManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ThrowWriteExceptions",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ThrowWriteExceptions",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_MaxConnections",
+ "Parameters": [],
+ "ReturnType": "System.Nullable<System.Int64>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_MaxConnections",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Nullable<System.Int64>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RequestQueueLimit",
+ "Parameters": [],
+ "ReturnType": "System.Int64",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RequestQueueLimit",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Int64"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_MaxRequestBodySize",
+ "Parameters": [],
+ "ReturnType": "System.Nullable<System.Int64>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_MaxRequestBodySize",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Nullable<System.Int64>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AllowSynchronousIO",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AllowSynchronousIO",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Http503Verbosity",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Server.HttpSys.Http503VerbosityLevel",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Http503Verbosity",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Server.HttpSys.Http503VerbosityLevel"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Server.HttpSys.TimeoutManager",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_EntityBody",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EntityBody",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_DrainEntityBody",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_DrainEntityBody",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RequestQueue",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RequestQueue",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IdleConnection",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_IdleConnection",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_HeaderWait",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_HeaderWait",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_MinSendBytesPerSecond",
+ "Parameters": [],
+ "ReturnType": "System.Int64",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_MinSendBytesPerSecond",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Int64"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Server.HttpSys.UrlPrefix",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "scheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "host",
+ "Type": "System.String"
+ },
+ {
+ "Name": "port",
+ "Type": "System.String"
+ },
+ {
+ "Name": "path",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Server.HttpSys.UrlPrefix",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "scheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "host",
+ "Type": "System.String"
+ },
+ {
+ "Name": "portValue",
+ "Type": "System.Nullable<System.Int32>"
+ },
+ {
+ "Name": "path",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Server.HttpSys.UrlPrefix",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "prefix",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Server.HttpSys.UrlPrefix",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IsHttps",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Scheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Host",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Port",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_PortValue",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Path",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_FullPrefix",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Equals",
+ "Parameters": [
+ {
+ "Name": "obj",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetHashCode",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ToString",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Server.HttpSys.UrlPrefixCollection",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "System.Collections.Generic.ICollection<Microsoft.AspNetCore.Server.HttpSys.UrlPrefix>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetEnumerator",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IEnumerator<Microsoft.AspNetCore.Server.HttpSys.UrlPrefix>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Server.HttpSys.UrlPrefix>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Count",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.ICollection<Microsoft.AspNetCore.Server.HttpSys.UrlPrefix>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IsReadOnly",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.ICollection<Microsoft.AspNetCore.Server.HttpSys.UrlPrefix>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Add",
+ "Parameters": [
+ {
+ "Name": "prefix",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Add",
+ "Parameters": [
+ {
+ "Name": "item",
+ "Type": "Microsoft.AspNetCore.Server.HttpSys.UrlPrefix"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.ICollection<Microsoft.AspNetCore.Server.HttpSys.UrlPrefix>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Clear",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.ICollection<Microsoft.AspNetCore.Server.HttpSys.UrlPrefix>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Contains",
+ "Parameters": [
+ {
+ "Name": "item",
+ "Type": "Microsoft.AspNetCore.Server.HttpSys.UrlPrefix"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.ICollection<Microsoft.AspNetCore.Server.HttpSys.UrlPrefix>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CopyTo",
+ "Parameters": [
+ {
+ "Name": "array",
+ "Type": "Microsoft.AspNetCore.Server.HttpSys.UrlPrefix[]"
+ },
+ {
+ "Name": "arrayIndex",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.ICollection<Microsoft.AspNetCore.Server.HttpSys.UrlPrefix>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Remove",
+ "Parameters": [
+ {
+ "Name": "prefix",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Remove",
+ "Parameters": [
+ {
+ "Name": "item",
+ "Type": "Microsoft.AspNetCore.Server.HttpSys.UrlPrefix"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.ICollection<Microsoft.AspNetCore.Server.HttpSys.UrlPrefix>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/HttpSysServer/test/Directory.Build.props b/src/HttpSysServer/test/Directory.Build.props
new file mode 100644
index 0000000000..036af693c5
--- /dev/null
+++ b/src/HttpSysServer/test/Directory.Build.props
@@ -0,0 +1,18 @@
+<Project>
+ <Import Project="..\Directory.Build.props" />
+
+ <PropertyGroup>
+ <DeveloperBuildTestTfms>netcoreapp2.1</DeveloperBuildTestTfms>
+ <StandardTestTfms>$(DeveloperBuildTestTfms)</StandardTestTfms>
+ <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' ">$(StandardTestTfms);netcoreapp2.0</StandardTestTfms>
+ <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' AND '$(OS)' == 'Windows_NT' ">$(StandardTestTfms);net461</StandardTestTfms>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
+ <PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/AuthenticationTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/AuthenticationTests.cs
new file mode 100644
index 0000000000..06bc6f2c32
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/AuthenticationTests.cs
@@ -0,0 +1,383 @@
+// 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.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class AuthenticationTests
+ {
+ private static bool AllowAnoymous = true;
+ private static bool DenyAnoymous = false;
+
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.None)]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)]
+ [InlineData(AuthenticationSchemes.Basic)]
+ [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationSchemes.Digest |*/ AuthenticationSchemes.Basic)]
+ public async Task AuthTypes_AllowAnonymous_NoChallenge(AuthenticationSchemes authType)
+ {
+ using (var server = Utilities.CreateDynamicHost(authType, AllowAnoymous, out var address, httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ Assert.False(httpContext.User.Identity.IsAuthenticated);
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Empty(response.Headers.WwwAuthenticate);
+ }
+ }
+#if !NETCOREAPP2_0
+ // https://github.com/aspnet/ServerTests/issues/82
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)] // TODO: Not implemented
+ [InlineData(AuthenticationSchemes.Basic)]
+ public async Task AuthType_RequireAuth_ChallengesAdded(AuthenticationSchemes authType)
+ {
+ using (var server = Utilities.CreateDynamicHost(authType, DenyAnoymous, out var address, httpContext =>
+ {
+ throw new NotImplementedException();
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal(authType.ToString(), response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)] // TODO: Not implemented
+ [InlineData(AuthenticationSchemes.Basic)]
+ public async Task AuthType_AllowAnonymousButSpecify401_ChallengesAdded(AuthenticationSchemes authType)
+ {
+ using (var server = Utilities.CreateDynamicHost(authType, AllowAnoymous, out var address, httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ Assert.False(httpContext.User.Identity.IsAuthenticated);
+ httpContext.Response.StatusCode = 401;
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal(authType.ToString(), response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task MultipleAuthTypes_AllowAnonymousButSpecify401_ChallengesAdded()
+ {
+ string address;
+ using (Utilities.CreateHttpAuthServer(
+ AuthenticationSchemes.Negotiate
+ | AuthenticationSchemes.NTLM
+ /* | AuthenticationSchemes.Digest TODO: Not implemented */
+ | AuthenticationSchemes.Basic,
+ true,
+ out address,
+ httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ Assert.False(httpContext.User.Identity.IsAuthenticated);
+ httpContext.Response.StatusCode = 401;
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal("Negotiate, NTLM, basic", response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase);
+ }
+ }
+#endif
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)] // TODO: Not implemented
+ // [InlineData(AuthenticationSchemes.Basic)] // Doesn't work with default creds
+ [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /* AuthenticationSchemes.Digest |*/ AuthenticationSchemes.Basic)]
+ public async Task AuthTypes_AllowAnonymousButSpecify401_Success(AuthenticationSchemes authType)
+ {
+ int requestId = 0;
+ using (var server = Utilities.CreateDynamicHost(authType, AllowAnoymous, out var address, httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ if (requestId == 0)
+ {
+ Assert.False(httpContext.User.Identity.IsAuthenticated);
+ httpContext.Response.StatusCode = 401;
+ }
+ else if (requestId == 1)
+ {
+ Assert.True(httpContext.User.Identity.IsAuthenticated);
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ requestId++;
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address, useDefaultCredentials: true);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)] // TODO: Not implemented
+ // [InlineData(AuthenticationSchemes.Basic)] // Doesn't work with default creds
+ [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /* AuthenticationSchemes.Digest |*/ AuthenticationSchemes.Basic)]
+ public async Task AuthTypes_RequireAuth_Success(AuthenticationSchemes authType)
+ {
+ using (var server = Utilities.CreateDynamicHost(authType, DenyAnoymous, out var address, httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ Assert.True(httpContext.User.Identity.IsAuthenticated);
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address, useDefaultCredentials: true);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+ }
+
+ // https://github.com/aspnet/Logging/issues/543#issuecomment-321907828
+ [ConditionalFact]
+ public async Task AuthTypes_AccessUserInOnCompleted_Success()
+ {
+ var completed = new ManualResetEvent(false);
+ string userName = null;
+ var authTypes = AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM;
+ using (var server = Utilities.CreateDynamicHost(authTypes, DenyAnoymous, out var address, httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ Assert.True(httpContext.User.Identity.IsAuthenticated);
+ httpContext.Response.OnCompleted(() =>
+ {
+ userName = httpContext.User.Identity.Name;
+ completed.Set();
+ return Task.FromResult(0);
+ });
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address, useDefaultCredentials: true);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.True(completed.WaitOne(TimeSpan.FromSeconds(5)));
+ Assert.False(string.IsNullOrEmpty(userName));
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)]
+ [InlineData(AuthenticationSchemes.Basic)]
+ [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationSchemes.Digest |*/ AuthenticationSchemes.Basic)]
+ public async Task AuthTypes_AuthenticateWithNoUser_NoResults(AuthenticationSchemes authType)
+ {
+ var authTypeList = authType.ToString().Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ using (var server = Utilities.CreateDynamicHost(authType, AllowAnoymous, out var address, async httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ Assert.False(httpContext.User.Identity.IsAuthenticated);
+ var authResults = await httpContext.AuthenticateAsync(HttpSysDefaults.AuthenticationScheme);
+ Assert.False(authResults.Succeeded);
+ Assert.True(authResults.None);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Empty(response.Headers.WwwAuthenticate);
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)]
+ // [InlineData(AuthenticationSchemes.Basic)] // Doesn't work with default creds
+ [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationSchemes.Digest |*/ AuthenticationSchemes.Basic)]
+ public async Task AuthTypes_AuthenticateWithUser_OneResult(AuthenticationSchemes authType)
+ {
+ using (var server = Utilities.CreateDynamicHost(authType, DenyAnoymous, out var address, async httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ Assert.True(httpContext.User.Identity.IsAuthenticated);
+ var authResults = await httpContext.AuthenticateAsync(HttpSysDefaults.AuthenticationScheme);
+ Assert.True(authResults.Succeeded);
+ }))
+ {
+ var response = await SendRequestAsync(address, useDefaultCredentials: true);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+ }
+#if !NETCOREAPP2_0
+ // https://github.com/aspnet/ServerTests/issues/82
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)]
+ [InlineData(AuthenticationSchemes.Basic)]
+ [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationSchemes.Digest |*/ AuthenticationSchemes.Basic)]
+ public async Task AuthTypes_ChallengeWithoutAuthTypes_AllChallengesSent(AuthenticationSchemes authType)
+ {
+ var authTypeList = authType.ToString().Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ using (var server = Utilities.CreateDynamicHost(authType, AllowAnoymous, out var address, httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ Assert.False(httpContext.User.Identity.IsAuthenticated);
+ return httpContext.ChallengeAsync(HttpSysDefaults.AuthenticationScheme);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal(authTypeList.Count(), response.Headers.WwwAuthenticate.Count);
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)]
+ [InlineData(AuthenticationSchemes.Basic)]
+ [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationSchemes.Digest |*/ AuthenticationSchemes.Basic)]
+ public async Task AuthTypes_ChallengeWithAllAuthTypes_AllChallengesSent(AuthenticationSchemes authType)
+ {
+ var authTypeList = authType.ToString().Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ using (var server = Utilities.CreateDynamicHost(authType, AllowAnoymous, out var address, async httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ Assert.False(httpContext.User.Identity.IsAuthenticated);
+ await httpContext.ChallengeAsync(HttpSysDefaults.AuthenticationScheme);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal(authTypeList.Count(), response.Headers.WwwAuthenticate.Count);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task AuthTypes_OneChallengeSent()
+ {
+ var authTypes = AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationSchemes.Digest |*/ AuthenticationSchemes.Basic;
+ using (var server = Utilities.CreateDynamicHost(authTypes, AllowAnoymous, out var address, httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ Assert.False(httpContext.User.Identity.IsAuthenticated);
+ return httpContext.ChallengeAsync(HttpSysDefaults.AuthenticationScheme);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal(3, response.Headers.WwwAuthenticate.Count);
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)]
+ [InlineData(AuthenticationSchemes.Basic)]
+ [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationSchemes.Digest |*/ AuthenticationSchemes.Basic)]
+ [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM)]
+ [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.Basic)]
+ [InlineData(AuthenticationSchemes.NTLM | AuthenticationSchemes.Basic)]
+ public async Task AuthTypes_ChallengeWillAskForAllEnabledSchemes(AuthenticationSchemes authType)
+ {
+ var authTypeList = authType.ToString().Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ using (var server = Utilities.CreateDynamicHost(authType, AllowAnoymous, out var address, httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ Assert.False(httpContext.User.Identity.IsAuthenticated);
+ return httpContext.ChallengeAsync(HttpSysDefaults.AuthenticationScheme);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal(authTypeList.Count(), response.Headers.WwwAuthenticate.Count);
+ }
+ }
+#endif
+ [ConditionalFact]
+ public async Task AuthTypes_Forbid_Forbidden()
+ {
+ var authTypes = AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationSchemes.Digest |*/ AuthenticationSchemes.Basic;
+ using (var server = Utilities.CreateDynamicHost(authTypes, AllowAnoymous, out var address, httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ Assert.False(httpContext.User.Identity.IsAuthenticated);
+ return httpContext.ForbidAsync(HttpSysDefaults.AuthenticationScheme);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
+ Assert.Empty(response.Headers.WwwAuthenticate);
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)] // Not implemented
+ // [InlineData(AuthenticationSchemes.Basic)] // Can't log in with UseDefaultCredentials
+ public async Task AuthTypes_UnathorizedAuthenticatedAuthType_Unauthorized(AuthenticationSchemes authType)
+ {
+ using (var server = Utilities.CreateDynamicHost(authType, DenyAnoymous, out var address, httpContext =>
+ {
+ Assert.NotNull(httpContext.User);
+ Assert.NotNull(httpContext.User.Identity);
+ Assert.True(httpContext.User.Identity.IsAuthenticated);
+ return httpContext.ChallengeAsync(HttpSysDefaults.AuthenticationScheme, null);
+ }))
+ {
+ var response = await SendRequestAsync(address, useDefaultCredentials: true);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Single(response.Headers.WwwAuthenticate);
+ Assert.Equal(authType.ToString(), response.Headers.WwwAuthenticate.First().Scheme);
+ }
+ }
+
+ private async Task<HttpResponseMessage> SendRequestAsync(string uri, bool useDefaultCredentials = false)
+ {
+ HttpClientHandler handler = new HttpClientHandler();
+ handler.UseDefaultCredentials = useDefaultCredentials;
+ using (HttpClient client = new HttpClient(handler))
+ {
+ return await client.GetAsync(uri);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/DummyApplication.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/DummyApplication.cs
new file mode 100644
index 0000000000..14d975e1fa
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/DummyApplication.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;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal class DummyApplication : IHttpApplication<HttpContext>
+ {
+ private readonly RequestDelegate _requestDelegate;
+
+ public DummyApplication() : this(context => Task.CompletedTask) { }
+
+ public DummyApplication(RequestDelegate requestDelegate)
+ {
+ _requestDelegate = requestDelegate;
+ }
+
+ public HttpContext CreateContext(IFeatureCollection contextFeatures)
+ {
+ return new DefaultHttpContext(contextFeatures);
+ }
+
+ public void DisposeContext(HttpContext httpContext, Exception exception)
+ {
+
+ }
+
+ public async Task ProcessRequestAsync(HttpContext httpContext)
+ {
+ await _requestDelegate(httpContext);
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/HttpsTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/HttpsTests.cs
new file mode 100644
index 0000000000..b3ae85687a
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/HttpsTests.cs
@@ -0,0 +1,166 @@
+// 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.Net.Http;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class HttpsTests
+ {
+ private const string Address = "https://localhost:9090/";
+
+ [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ public async Task Https_200OK_Success()
+ {
+ using (Utilities.CreateHttpsServer(httpContext =>
+ {
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(Address);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ public async Task Https_SendHelloWorld_Success()
+ {
+ using (Utilities.CreateHttpsServer(httpContext =>
+ {
+ byte[] body = Encoding.UTF8.GetBytes("Hello World");
+ httpContext.Response.ContentLength = body.Length;
+ return httpContext.Response.Body.WriteAsync(body, 0, body.Length);
+ }))
+ {
+ string response = await SendRequestAsync(Address);
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ public async Task Https_EchoHelloWorld_Success()
+ {
+ using (Utilities.CreateHttpsServer(httpContext =>
+ {
+ string input = new StreamReader(httpContext.Request.Body).ReadToEnd();
+ Assert.Equal("Hello World", input);
+ byte[] body = Encoding.UTF8.GetBytes("Hello World");
+ httpContext.Response.ContentLength = body.Length;
+ httpContext.Response.Body.Write(body, 0, body.Length);
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(Address, "Hello World");
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ public async Task Https_ClientCertNotSent_ClientCertNotPresent()
+ {
+ using (Utilities.CreateHttpsServer(async httpContext =>
+ {
+ var tls = httpContext.Features.Get<ITlsConnectionFeature>();
+ Assert.NotNull(tls);
+ var cert = await tls.GetClientCertificateAsync(CancellationToken.None);
+ Assert.Null(cert);
+ Assert.Null(tls.ClientCertificate);
+ }))
+ {
+ string response = await SendRequestAsync(Address);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ public async Task Https_ClientCertRequested_ClientCertPresent()
+ {
+ using (Utilities.CreateHttpsServer(async httpContext =>
+ {
+ var tls = httpContext.Features.Get<ITlsConnectionFeature>();
+ Assert.NotNull(tls);
+ var cert = await tls.GetClientCertificateAsync(CancellationToken.None);
+ Assert.NotNull(cert);
+ Assert.NotNull(tls.ClientCertificate);
+ }))
+ {
+ X509Certificate2 cert = FindClientCert();
+ Assert.NotNull(cert);
+ string response = await SendRequestAsync(Address, cert);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ private async Task<string> SendRequestAsync(string uri,
+ X509Certificate cert = null)
+ {
+ var handler = new WinHttpHandler();
+ handler.ServerCertificateValidationCallback = (a, b, c, d) => true;
+ if (cert != null)
+ {
+ handler.ClientCertificates.Add(cert);
+ }
+ using (HttpClient client = new HttpClient(handler))
+ {
+ return await client.GetStringAsync(uri);
+ }
+ }
+
+ private async Task<string> SendRequestAsync(string uri, string upload)
+ {
+ var handler = new WinHttpHandler();
+ handler.ServerCertificateValidationCallback = (a, b, c, d) => true;
+ using (HttpClient client = new HttpClient(handler))
+ {
+ HttpResponseMessage response = await client.PostAsync(uri, new StringContent(upload));
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadAsStringAsync();
+ }
+ }
+
+ private X509Certificate2 FindClientCert()
+ {
+ var store = new X509Store();
+ store.Open(OpenFlags.ReadOnly);
+
+ foreach (var cert in store.Certificates)
+ {
+ bool isClientAuth = false;
+ bool isSmartCard = false;
+ foreach (var extension in cert.Extensions)
+ {
+ var eku = extension as X509EnhancedKeyUsageExtension;
+ if (eku != null)
+ {
+ foreach (var oid in eku.EnhancedKeyUsages)
+ {
+ if (oid.FriendlyName == "Client Authentication")
+ {
+ isClientAuth = true;
+ }
+ else if (oid.FriendlyName == "Smart Card Logon")
+ {
+ isSmartCard = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (isClientAuth && !isSmartCard)
+ {
+ return cert;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/AuthenticationTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/AuthenticationTests.cs
new file mode 100644
index 0000000000..db32323bdf
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/AuthenticationTests.cs
@@ -0,0 +1,220 @@
+// 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.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.Listener
+{
+ public class AuthenticationTests
+ {
+ private static bool AllowAnoymous = true;
+ private static bool DenyAnoymous = false;
+
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.None)]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)]
+ [InlineData(AuthenticationSchemes.Basic)]
+ [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationSchemes.Digest |*/ AuthenticationSchemes.Basic)]
+ public async Task AuthTypes_AllowAnonymous_NoChallenge(AuthenticationSchemes authType)
+ {
+ string address;
+ using (var server = Utilities.CreateHttpAuthServer(authType, AllowAnoymous, out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.NotNull(context.User);
+ Assert.False(context.User.Identity.IsAuthenticated);
+ Assert.Equal(authType, context.Response.AuthenticationChallenges);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Empty(response.Headers.WwwAuthenticate);
+ }
+ }
+#if !NETCOREAPP2_0
+ // https://github.com/aspnet/ServerTests/issues/82
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationType.Digest)] // TODO: Not implemented
+ [InlineData(AuthenticationSchemes.Basic)]
+ public async Task AuthType_RequireAuth_ChallengesAdded(AuthenticationSchemes authType)
+ {
+ string address;
+ using (var server = Utilities.CreateHttpAuthServer(authType, DenyAnoymous, out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var contextTask = server.AcceptAsync(Utilities.DefaultTimeout); // Fails when the server shuts down, the challenge happens internally.
+ var response = await responseTask;
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal(authType.ToString(), response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)] // TODO: Not implemented
+ [InlineData(AuthenticationSchemes.Basic)]
+ public async Task AuthType_AllowAnonymousButSpecify401_ChallengesAdded(AuthenticationSchemes authType)
+ {
+ string address;
+ using (var server = Utilities.CreateHttpAuthServer(authType, AllowAnoymous, out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.NotNull(context.User);
+ Assert.False(context.User.Identity.IsAuthenticated);
+ Assert.Equal(authType, context.Response.AuthenticationChallenges);
+ context.Response.StatusCode = 401;
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal(authType.ToString(), response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task MultipleAuthTypes_AllowAnonymousButSpecify401_ChallengesAdded()
+ {
+ string address;
+ AuthenticationSchemes authType =
+ AuthenticationSchemes.Negotiate
+ | AuthenticationSchemes.NTLM
+ /* | AuthenticationSchemes.Digest TODO: Not implemented */
+ | AuthenticationSchemes.Basic;
+ using (var server = Utilities.CreateHttpAuthServer(authType, AllowAnoymous, out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.NotNull(context.User);
+ Assert.False(context.User.Identity.IsAuthenticated);
+ Assert.Equal(authType, context.Response.AuthenticationChallenges);
+ context.Response.StatusCode = 401;
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal("Negotiate, NTLM, basic", response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase);
+ }
+ }
+#endif
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)] // TODO: Not implemented
+ // [InlineData(AuthenticationSchemes.Basic)] // Doesn't work with default creds
+ [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationType.Digest |*/ AuthenticationSchemes.Basic)]
+ public async Task AuthTypes_AllowAnonymousButSpecify401_Success(AuthenticationSchemes authType)
+ {
+ string address;
+ using (var server = Utilities.CreateHttpAuthServer(authType, AllowAnoymous, out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address, useDefaultCredentials: true);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.NotNull(context.User);
+ Assert.False(context.User.Identity.IsAuthenticated);
+ Assert.Equal(authType, context.Response.AuthenticationChallenges);
+ context.Response.StatusCode = 401;
+ context.Dispose();
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.NotNull(context.User);
+ Assert.True(context.User.Identity.IsAuthenticated);
+ Assert.Equal(authType, context.Response.AuthenticationChallenges);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData(AuthenticationSchemes.Negotiate)]
+ [InlineData(AuthenticationSchemes.NTLM)]
+ // [InlineData(AuthenticationSchemes.Digest)] // TODO: Not implemented
+ // [InlineData(AuthenticationSchemes.Basic)] // Doesn't work with default creds
+ [InlineData(AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM | /*AuthenticationType.Digest |*/ AuthenticationSchemes.Basic)]
+ public async Task AuthTypes_RequireAuth_Success(AuthenticationSchemes authType)
+ {
+ string address;
+ using (var server = Utilities.CreateHttpAuthServer(authType, DenyAnoymous, out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address, useDefaultCredentials: true);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.NotNull(context.User);
+ Assert.True(context.User.Identity.IsAuthenticated);
+ Assert.Equal(authType, context.Response.AuthenticationChallenges);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+ }
+
+ [ConditionalFact(Skip = "Requires a domain joined machine - https://github.com/aspnet/HttpSysServer/issues/357")]
+ public async Task AuthTypes_RequireKerberosAuth_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpAuthServer(AuthenticationSchemes.Kerberos, DenyAnoymous, out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address, useDefaultCredentials: true);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.NotNull(context.User);
+ Assert.True(context.User.Identity.IsAuthenticated);
+ Assert.Equal(AuthenticationSchemes.Kerberos, context.Response.AuthenticationChallenges);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+ }
+
+ [ConditionalFact(Skip = "Requires a domain joined machine - https://github.com/aspnet/HttpSysServer/issues/357")]
+ public async Task MultipleAuthTypes_KerberosAllowAnonymousButSpecify401_ChallengesAdded()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpAuthServer(AuthenticationSchemes.Kerberos, AllowAnoymous, out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.NotNull(context.User);
+ Assert.False(context.User.Identity.IsAuthenticated);
+ Assert.Equal(AuthenticationSchemes.Kerberos, context.Response.AuthenticationChallenges);
+ context.Response.StatusCode = 401;
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ Assert.Equal("Kerberos", response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ private async Task<HttpResponseMessage> SendRequestAsync(string uri, bool useDefaultCredentials = false)
+ {
+ HttpClientHandler handler = new HttpClientHandler();
+ handler.UseDefaultCredentials = useDefaultCredentials;
+ using (HttpClient client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(5) })
+ {
+ return await client.GetAsync(uri);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/HttpsTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/HttpsTests.cs
new file mode 100644
index 0000000000..a4c16c785c
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/HttpsTests.cs
@@ -0,0 +1,172 @@
+// 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.Net.Http;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.Listener
+{
+ public class HttpsTests
+ {
+ // Note these tests can't use dynamic ports or run concurrently because the ssl cert must be pre-registered with a specific port.
+ private const string Address = "https://localhost:9090/";
+
+ [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ public async Task Https_200OK_Success()
+ {
+ using (var server = Utilities.CreateHttpsServer())
+ {
+ Task<string> responseTask = SendRequestAsync(Address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ public async Task Https_SendHelloWorld_Success()
+ {
+ using (var server = Utilities.CreateHttpsServer())
+ {
+ Task<string> responseTask = SendRequestAsync(Address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] body = Encoding.UTF8.GetBytes("Hello World");
+ context.Response.ContentLength = body.Length;
+ await context.Response.Body.WriteAsync(body, 0, body.Length);
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ public async Task Https_EchoHelloWorld_Success()
+ {
+ using (var server = Utilities.CreateHttpsServer())
+ {
+ Task<string> responseTask = SendRequestAsync(Address, "Hello World");
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ string input = new StreamReader(context.Request.Body).ReadToEnd();
+ Assert.Equal("Hello World", input);
+ context.Response.ContentLength = 11;
+ var writer = new StreamWriter(context.Response.Body);
+ await writer.WriteAsync("Hello World");
+ await writer.FlushAsync();
+
+ string response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ public async Task Https_ClientCertNotSent_ClientCertNotPresent()
+ {
+ using (var server = Utilities.CreateHttpsServer())
+ {
+ Task<string> responseTask = SendRequestAsync(Address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cert = await context.Request.GetClientCertificateAsync();
+ Assert.Null(cert);
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ public async Task Https_ClientCertRequested_ClientCertPresent()
+ {
+ using (var server = Utilities.CreateHttpsServer())
+ {
+ X509Certificate2 clientCert = FindClientCert();
+ Assert.NotNull(clientCert);
+ Task<string> responseTask = SendRequestAsync(Address, clientCert);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cert = await context.Request.GetClientCertificateAsync();
+ Assert.NotNull(cert);
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ private async Task<string> SendRequestAsync(string uri,
+ X509Certificate cert = null)
+ {
+ WinHttpHandler handler = new WinHttpHandler();
+ handler.ServerCertificateValidationCallback = (a, b, c, d) => true;
+ if (cert != null)
+ {
+ handler.ClientCertificates.Add(cert);
+ }
+ using (HttpClient client = new HttpClient(handler))
+ {
+ return await client.GetStringAsync(uri);
+ }
+ }
+
+ private async Task<string> SendRequestAsync(string uri, string upload)
+ {
+ WinHttpHandler handler = new WinHttpHandler();
+ handler.ServerCertificateValidationCallback = (a, b, c, d) => true;
+ using (HttpClient client = new HttpClient(handler))
+ {
+ HttpResponseMessage response = await client.PostAsync(uri, new StringContent(upload));
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadAsStringAsync();
+ }
+ }
+
+ private X509Certificate2 FindClientCert()
+ {
+ var store = new X509Store();
+ store.Open(OpenFlags.ReadOnly);
+
+ foreach (var cert in store.Certificates)
+ {
+ bool isClientAuth = false;
+ bool isSmartCard = false;
+ foreach (var extension in cert.Extensions)
+ {
+ var eku = extension as X509EnhancedKeyUsageExtension;
+ if (eku != null)
+ {
+ foreach (var oid in eku.EnhancedKeyUsages)
+ {
+ if (oid.FriendlyName == "Client Authentication")
+ {
+ isClientAuth = true;
+ }
+ else if (oid.FriendlyName == "Smart Card Logon")
+ {
+ isSmartCard = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (isClientAuth && !isSmartCard)
+ {
+ return cert;
+ }
+ }
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/OpaqueUpgradeTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/OpaqueUpgradeTests.cs
new file mode 100644
index 0000000000..d749fdb285
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/OpaqueUpgradeTests.cs
@@ -0,0 +1,217 @@
+// 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 System.Net.Http;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.Listener
+{
+ public class OpaqueUpgradeTests
+ {
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
+ public async Task OpaqueUpgrade_AfterHeadersSent_Throws()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> clientTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] body = Encoding.UTF8.GetBytes("Hello World");
+ await context.Response.Body.WriteAsync(body, 0, body.Length);
+
+ Assert.Throws<InvalidOperationException>(() => context.Response.Headers["Upgrade"] = "WebSocket"); // Win8.1 blocks anything but WebSocket
+ await Assert.ThrowsAsync<InvalidOperationException>(async () => await context.UpgradeAsync());
+ context.Dispose();
+ HttpResponseMessage response = await clientTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
+ public async Task OpaqueUpgrade_GetUpgrade_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<Stream> clientTask = SendOpaqueRequestAsync("GET", address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.True(context.IsUpgradableRequest);
+ context.Response.Headers["Upgrade"] = "WebSocket"; // Win8.1 blocks anything but WebSocket
+ Stream serverStream = await context.UpgradeAsync();
+ Assert.True(serverStream.CanRead);
+ Assert.True(serverStream.CanWrite);
+ Stream clientStream = await clientTask;
+ serverStream.Dispose();
+ context.Dispose();
+ clientStream.Dispose();
+ }
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
+ // See HTTP_VERB for known verbs
+ [InlineData("UNKNOWN", null)]
+ [InlineData("INVALID", null)]
+ [InlineData("OPTIONS", null)]
+ [InlineData("GET", null)]
+ [InlineData("HEAD", null)]
+ [InlineData("DELETE", null)]
+ [InlineData("TRACE", null)]
+ [InlineData("CONNECT", null)]
+ [InlineData("TRACK", null)]
+ [InlineData("MOVE", null)]
+ [InlineData("COPY", null)]
+ [InlineData("PROPFIND", null)]
+ [InlineData("PROPPATCH", null)]
+ [InlineData("MKCOL", null)]
+ [InlineData("LOCK", null)]
+ [InlineData("UNLOCK", null)]
+ [InlineData("SEARCH", null)]
+ [InlineData("CUSTOMVERB", null)]
+ [InlineData("PATCH", null)]
+ [InlineData("POST", "Content-Length: 0")]
+ [InlineData("PUT", "Content-Length: 0")]
+ public async Task OpaqueUpgrade_VariousMethodsUpgradeSendAndReceive_Success(string method, string extraHeader)
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<Stream> clientTask = SendOpaqueRequestAsync(method, address, extraHeader);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.True(context.IsUpgradableRequest);
+ context.Response.Headers["Upgrade"] = "WebSocket"; // Win8.1 blocks anything but WebSocket
+ Stream serverStream = await context.UpgradeAsync();
+ Stream clientStream = await clientTask;
+
+ byte[] clientBuffer = new byte[] { 0x00, 0x01, 0xFF, 0x00, 0x00 };
+ await clientStream.WriteAsync(clientBuffer, 0, 3);
+
+ byte[] serverBuffer = new byte[clientBuffer.Length];
+ int read = await serverStream.ReadAsync(serverBuffer, 0, serverBuffer.Length);
+ Assert.Equal(clientBuffer, serverBuffer);
+
+ await serverStream.WriteAsync(serverBuffer, 0, read);
+
+ byte[] clientEchoBuffer = new byte[clientBuffer.Length];
+ read = await clientStream.ReadAsync(clientEchoBuffer, 0, clientEchoBuffer.Length);
+ Assert.Equal(clientBuffer, clientEchoBuffer);
+
+ serverStream.Dispose();
+ context.Dispose();
+ clientStream.Dispose();
+ }
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
+ // Http.Sys returns a 411 Length Required if PUT or POST does not specify content-length or chunked.
+ [InlineData("POST", "Content-Length: 10")]
+ [InlineData("POST", "Transfer-Encoding: chunked")]
+ [InlineData("PUT", "Content-Length: 10")]
+ [InlineData("PUT", "Transfer-Encoding: chunked")]
+ [InlineData("CUSTOMVERB", "Content-Length: 10")]
+ [InlineData("CUSTOMVERB", "Transfer-Encoding: chunked")]
+ public async Task OpaqueUpgrade_InvalidMethodUpgrade_Disconnected(string method, string extraHeader)
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var clientTask = SendOpaqueRequestAsync(method, address, extraHeader);
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.False(context.IsUpgradableRequest);
+ context.Dispose();
+
+ await Assert.ThrowsAsync<InvalidOperationException>(async () => await clientTask);
+ }
+ }
+
+ private async Task<HttpResponseMessage> SendRequestAsync(string uri)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetAsync(uri);
+ }
+ }
+
+ // Returns a bidirectional opaque stream or throws if the upgrade fails
+ private async Task<Stream> SendOpaqueRequestAsync(string method, string address, string extraHeader = null)
+ {
+ // Connect with a socket
+ Uri uri = new Uri(address);
+ TcpClient client = new TcpClient();
+ try
+ {
+ await client.ConnectAsync(uri.Host, uri.Port);
+ NetworkStream stream = client.GetStream();
+
+ // Send an HTTP GET request
+ byte[] requestBytes = BuildGetRequest(method, uri, extraHeader);
+ await stream.WriteAsync(requestBytes, 0, requestBytes.Length);
+
+ // Read the response headers, fail if it's not a 101
+ await ParseResponseAsync(stream);
+
+ // Return the opaque network stream
+ return stream;
+ }
+ catch (Exception)
+ {
+ ((IDisposable)client).Dispose();
+ throw;
+ }
+ }
+
+ private byte[] BuildGetRequest(string method, Uri uri, string extraHeader)
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.Append(method);
+ builder.Append(" ");
+ builder.Append(uri.PathAndQuery);
+ builder.Append(" HTTP/1.1");
+ builder.AppendLine();
+
+ builder.Append("Host: ");
+ builder.Append(uri.Host);
+ builder.Append(':');
+ builder.Append(uri.Port);
+ builder.AppendLine();
+
+ if (!string.IsNullOrEmpty(extraHeader))
+ {
+ builder.AppendLine(extraHeader);
+ }
+
+ builder.AppendLine();
+ return Encoding.ASCII.GetBytes(builder.ToString());
+ }
+
+ // Read the response headers, fail if it's not a 101
+ private async Task ParseResponseAsync(NetworkStream stream)
+ {
+ StreamReader reader = new StreamReader(stream);
+ string statusLine = await reader.ReadLineAsync();
+ string[] parts = statusLine.Split(' ');
+ if (int.Parse(parts[1]) != 101)
+ {
+ throw new InvalidOperationException("The response status code was incorrect: " + statusLine);
+ }
+
+ // Scan to the end of the headers
+ while (!string.IsNullOrEmpty(reader.ReadLine()))
+ {
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/RequestBodyTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/RequestBodyTests.cs
new file mode 100644
index 0000000000..baec5a8204
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/RequestBodyTests.cs
@@ -0,0 +1,436 @@
+// 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 System.Net;
+using System.Net.Http;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.Listener
+{
+ public class RequestBodyTests
+ {
+ [ConditionalFact]
+ public async Task RequestBody_SyncReadEnabledByDefault_ThrowsWhenDisabled()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address, "Hello World");
+
+ Assert.True(server.Options.AllowSynchronousIO);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] input = new byte[100];
+
+ Assert.True(context.AllowSynchronousIO);
+ var read = context.Request.Body.Read(input, 0, input.Length);
+ context.Response.ContentLength = read;
+ context.Response.Body.Write(input, 0, read);
+
+ context.AllowSynchronousIO = false;
+ Assert.Throws<InvalidOperationException>(() => context.Request.Body.Read(input, 0, input.Length));
+
+ string response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadSync_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address, "Hello World");
+
+ server.Options.AllowSynchronousIO = true;
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] input = new byte[100];
+ int read = context.Request.Body.Read(input, 0, input.Length);
+ context.Response.ContentLength = read;
+ context.Response.Body.Write(input, 0, read);
+
+ string response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadAsync_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address, "Hello World");
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] input = new byte[100];
+ int read = await context.Request.Body.ReadAsync(input, 0, input.Length);
+ context.Response.ContentLength = read;
+ await context.Response.Body.WriteAsync(input, 0, read);
+
+ string response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadBeginEnd_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address, "Hello World");
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] input = new byte[100];
+ int read = context.Request.Body.EndRead(context.Request.Body.BeginRead(input, 0, input.Length, null, null));
+ context.Response.ContentLength = read;
+ context.Response.Body.EndWrite(context.Response.Body.BeginWrite(input, 0, read, null, null));
+
+ string response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_InvalidBuffer_ArgumentException()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address, "Hello World");
+
+ server.Options.AllowSynchronousIO = true;
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] input = new byte[100];
+ Assert.Throws<ArgumentNullException>("buffer", () => context.Request.Body.Read(null, 0, 1));
+ Assert.Throws<ArgumentOutOfRangeException>("offset", () => context.Request.Body.Read(input, -1, 1));
+ Assert.Throws<ArgumentOutOfRangeException>("offset", () => context.Request.Body.Read(input, input.Length + 1, 1));
+ Assert.Throws<ArgumentOutOfRangeException>("size", () => context.Request.Body.Read(input, 10, -1));
+ Assert.Throws<ArgumentOutOfRangeException>("size", () => context.Request.Body.Read(input, 0, 0));
+ Assert.Throws<ArgumentOutOfRangeException>("size", () => context.Request.Body.Read(input, 1, input.Length));
+ Assert.Throws<ArgumentOutOfRangeException>("size", () => context.Request.Body.Read(input, 0, input.Length + 1));
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadSyncPartialBody_Success()
+ {
+ StaggardContent content = new StaggardContent();
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address, content);
+
+ server.Options.AllowSynchronousIO = true;
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] input = new byte[10];
+ int read = context.Request.Body.Read(input, 0, input.Length);
+ Assert.Equal(5, read);
+ content.Block.Release();
+ read = context.Request.Body.Read(input, 0, input.Length);
+ Assert.Equal(5, read);
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadAsyncPartialBody_Success()
+ {
+ StaggardContent content = new StaggardContent();
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address, content);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] input = new byte[10];
+ int read = await context.Request.Body.ReadAsync(input, 0, input.Length);
+ Assert.Equal(5, read);
+ content.Block.Release();
+ read = await context.Request.Body.ReadAsync(input, 0, input.Length);
+ Assert.Equal(5, read);
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_PostWithImidateBody_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendSocketRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] input = new byte[11];
+ int read = await context.Request.Body.ReadAsync(input, 0, input.Length);
+ Assert.Equal(10, read);
+ read = await context.Request.Body.ReadAsync(input, 0, input.Length);
+ Assert.Equal(0, read);
+ context.Response.ContentLength = 10;
+ await context.Response.Body.WriteAsync(input, 0, 10);
+ context.Dispose();
+
+ string response = await responseTask;
+ string[] lines = response.Split('\r', '\n');
+ Assert.Equal(13, lines.Length);
+ Assert.Equal("HTTP/1.1 200 OK", lines[0]);
+ Assert.Equal("0123456789", lines[12]);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadAsyncAlreadyCanceled_ReturnsCanceledTask()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address, "Hello World");
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+
+ byte[] input = new byte[10];
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ Task<int> task = context.Request.Body.ReadAsync(input, 0, input.Length, cts.Token);
+ Assert.True(task.IsCanceled);
+
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadAsyncPartialBodyWithCancellationToken_Success()
+ {
+ StaggardContent content = new StaggardContent();
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address, content);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] input = new byte[10];
+ var cts = new CancellationTokenSource();
+ int read = await context.Request.Body.ReadAsync(input, 0, input.Length, cts.Token);
+ Assert.Equal(5, read);
+ content.Block.Release();
+ read = await context.Request.Body.ReadAsync(input, 0, input.Length, cts.Token);
+ Assert.Equal(5, read);
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadAsyncPartialBodyWithTimeout_Success()
+ {
+ StaggardContent content = new StaggardContent();
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address, content);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] input = new byte[10];
+ var cts = new CancellationTokenSource();
+ cts.CancelAfter(TimeSpan.FromSeconds(5));
+ int read = await context.Request.Body.ReadAsync(input, 0, input.Length, cts.Token);
+ Assert.Equal(5, read);
+ content.Block.Release();
+ read = await context.Request.Body.ReadAsync(input, 0, input.Length, cts.Token);
+ Assert.Equal(5, read);
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadAsyncPartialBodyAndCancel_Canceled()
+ {
+ StaggardContent content = new StaggardContent();
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address, content);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] input = new byte[10];
+ var cts = new CancellationTokenSource();
+ int read = await context.Request.Body.ReadAsync(input, 0, input.Length, cts.Token);
+ Assert.Equal(5, read);
+ var readTask = context.Request.Body.ReadAsync(input, 0, input.Length, cts.Token);
+ Assert.False(readTask.IsCanceled);
+ cts.Cancel();
+ await Assert.ThrowsAsync<IOException>(async () => await readTask);
+ content.Block.Release();
+ context.Dispose();
+
+ await Assert.ThrowsAsync<HttpRequestException>(async () => await responseTask);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadAsyncPartialBodyAndExpiredTimeout_Canceled()
+ {
+ StaggardContent content = new StaggardContent();
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address, content);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] input = new byte[10];
+ var cts = new CancellationTokenSource();
+ int read = await context.Request.Body.ReadAsync(input, 0, input.Length, cts.Token);
+ Assert.Equal(5, read);
+ cts.CancelAfter(TimeSpan.FromMilliseconds(100));
+ var readTask = context.Request.Body.ReadAsync(input, 0, input.Length, cts.Token);
+ Assert.False(readTask.IsCanceled);
+ await Assert.ThrowsAsync<IOException>(async () => await readTask);
+ content.Block.Release();
+ context.Dispose();
+
+ await Assert.ThrowsAsync<HttpRequestException>(async () => await responseTask);
+ }
+ }
+
+ // Make sure that using our own disconnect token as a read cancellation token doesn't
+ // cause recursion problems when it fires and calls Abort.
+ [ConditionalFact]
+ public async Task RequestBody_ReadAsyncPartialBodyAndDisconnectedClient_Canceled()
+ {
+ StaggardContent content = new StaggardContent();
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var client = new HttpClient();
+ var responseTask = client.PostAsync(address, content);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ byte[] input = new byte[10];
+ int read = await context.Request.Body.ReadAsync(input, 0, input.Length, context.DisconnectToken);
+ Assert.False(context.DisconnectToken.IsCancellationRequested);
+ // The client should timeout and disconnect, making this read fail.
+ var assertTask = Assert.ThrowsAsync<IOException>(async () => await context.Request.Body.ReadAsync(input, 0, input.Length, context.DisconnectToken));
+ client.CancelPendingRequests();
+ await assertTask;
+ content.Block.Release();
+ context.Dispose();
+
+ await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await responseTask);
+ }
+ }
+
+ private Task<string> SendRequestAsync(string uri, string upload)
+ {
+ return SendRequestAsync(uri, new StringContent(upload));
+ }
+
+ private async Task<string> SendRequestAsync(string uri, HttpContent content)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ client.Timeout = TimeSpan.FromSeconds(10);
+ HttpResponseMessage response = await client.PostAsync(uri, content);
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadAsStringAsync();
+ }
+ }
+
+ private async Task<string> SendSocketRequestAsync(string address)
+ {
+ // Connect with a socket
+ Uri uri = new Uri(address);
+ TcpClient client = new TcpClient();
+ try
+ {
+ await client.ConnectAsync(uri.Host, uri.Port);
+ NetworkStream stream = client.GetStream();
+
+ // Send an HTTP GET request
+ byte[] requestBytes = BuildPostRequest(uri);
+ await stream.WriteAsync(requestBytes, 0, requestBytes.Length);
+ StreamReader reader = new StreamReader(stream);
+ return await reader.ReadToEndAsync();
+ }
+ catch (Exception)
+ {
+ ((IDisposable)client).Dispose();
+ throw;
+ }
+ }
+
+ private byte[] BuildPostRequest(Uri uri)
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.Append("POST");
+ builder.Append(" ");
+ builder.Append(uri.PathAndQuery);
+ builder.Append(" HTTP/1.1");
+ builder.AppendLine();
+
+ builder.Append("Host: ");
+ builder.Append(uri.Host);
+ builder.Append(':');
+ builder.Append(uri.Port);
+ builder.AppendLine();
+
+ builder.AppendLine("Connection: close");
+ builder.AppendLine("Content-Length: 10");
+ builder.AppendLine();
+ builder.Append("0123456789");
+ return Encoding.ASCII.GetBytes(builder.ToString());
+ }
+
+ private class StaggardContent : HttpContent
+ {
+ public StaggardContent()
+ {
+ Block = new SemaphoreSlim(0, 1);
+ }
+
+ public SemaphoreSlim Block { get; private set; }
+
+ protected async override Task SerializeToStreamAsync(Stream stream, TransportContext context)
+ {
+ await stream.WriteAsync(new byte[5], 0, 5);
+ await stream.FlushAsync();
+ await Block.WaitAsync();
+ await stream.WriteAsync(new byte[5], 0, 5);
+ }
+
+ protected override bool TryComputeLength(out long length)
+ {
+ length = 10;
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/RequestHeaderTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/RequestHeaderTests.cs
new file mode 100644
index 0000000000..4197d4faf0
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/RequestHeaderTests.cs
@@ -0,0 +1,184 @@
+// 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.Net.Http;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.Listener
+{
+ public class RequestHeaderTests
+ {
+ [ConditionalFact]
+ public async Task RequestHeaders_ClientSendsDefaultHeaders_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var requestHeaders = context.Request.Headers;
+ // NOTE: The System.Net client only sends the Connection: keep-alive header on the first connection per service-point.
+ // Assert.Equal(2, requestHeaders.Count);
+ // Assert.Equal("Keep-Alive", requestHeaders.Get("Connection"));
+ Assert.Equal(new Uri(address).Authority, requestHeaders["Host"]);
+ StringValues values;
+ Assert.False(requestHeaders.TryGetValue("Accept", out values));
+ Assert.False(requestHeaders.ContainsKey("Accept"));
+ Assert.True(StringValues.IsNullOrEmpty(requestHeaders["Accept"]));
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestHeaders_ClientSendsCustomHeaders_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ string[] customValues = new string[] { "custom1, and custom2", "custom3" };
+ Task responseTask = SendRequestAsync(address, "Custom-Header", customValues);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var requestHeaders = context.Request.Headers;
+ Assert.Equal(4, requestHeaders.Count);
+ Assert.Equal(new Uri(address).Authority, requestHeaders["Host"]);
+ Assert.Equal(new[] { new Uri(address).Authority }, requestHeaders.GetValues("Host"));
+ Assert.Equal("close", requestHeaders["Connection"]);
+ Assert.Equal(new[] { "close" }, requestHeaders.GetValues("Connection"));
+ // Apparently Http.Sys squashes request headers together.
+ Assert.Equal("custom1, and custom2, custom3", requestHeaders["Custom-Header"]);
+ Assert.Equal(new[] { "custom1", "and custom2", "custom3" }, requestHeaders.GetValues("Custom-Header"));
+ Assert.Equal("spacervalue, spacervalue", requestHeaders["Spacer-Header"]);
+ Assert.Equal(new[] { "spacervalue", "spacervalue" }, requestHeaders.GetValues("Spacer-Header"));
+ context.Dispose();
+
+ await responseTask;
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestHeaders_ClientSendsUtf8Headers_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ string[] customValues = new string[] { "custom1, and custom测试2", "custom3" };
+ Task responseTask = SendRequestAsync(address, "Custom-Header", customValues);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var requestHeaders = context.Request.Headers;
+ Assert.Equal(4, requestHeaders.Count);
+ Assert.Equal(new Uri(address).Authority, requestHeaders["Host"]);
+ Assert.Equal(new[] { new Uri(address).Authority }, requestHeaders.GetValues("Host"));
+ Assert.Equal("close", requestHeaders["Connection"]);
+ Assert.Equal(new[] { "close" }, requestHeaders.GetValues("Connection"));
+ // Apparently Http.Sys squashes request headers together.
+ Assert.Equal("custom1, and custom测试2, custom3", requestHeaders["Custom-Header"]);
+ Assert.Equal(new[] { "custom1", "and custom测试2", "custom3" }, requestHeaders.GetValues("Custom-Header"));
+ Assert.Equal("spacervalue, spacervalue", requestHeaders["Spacer-Header"]);
+ Assert.Equal(new[] { "spacervalue", "spacervalue" }, requestHeaders.GetValues("Spacer-Header"));
+ context.Dispose();
+
+ await responseTask;
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestHeaders_ClientSendsKnownHeaderWithNoValue_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ string[] customValues = new string[] { "" };
+ Task responseTask = SendRequestAsync(address, "If-None-Match", customValues);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var requestHeaders = context.Request.Headers;
+ Assert.Equal(3, requestHeaders.Count);
+ Assert.Equal(new Uri(address).Authority, requestHeaders["Host"]);
+ Assert.Equal(new[] { new Uri(address).Authority }, requestHeaders.GetValues("Host"));
+ Assert.Equal("close", requestHeaders["Connection"]);
+ Assert.Equal(new[] { "close" }, requestHeaders.GetValues("Connection"));
+ Assert.Equal(StringValues.Empty, requestHeaders["If-None-Match"]);
+ Assert.Empty(requestHeaders.GetValues("If-None-Match"));
+ Assert.Equal("spacervalue", requestHeaders["Spacer-Header"]);
+ context.Dispose();
+
+ await responseTask;
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestHeaders_ClientSendsUnknownHeaderWithNoValue_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ string[] customValues = new string[] { "" };
+ Task responseTask = SendRequestAsync(address, "Custom-Header", customValues);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var requestHeaders = context.Request.Headers;
+ Assert.Equal(4, requestHeaders.Count);
+ Assert.Equal(new Uri(address).Authority, requestHeaders["Host"]);
+ Assert.Equal(new[] { new Uri(address).Authority }, requestHeaders.GetValues("Host"));
+ Assert.Equal("close", requestHeaders["Connection"]);
+ Assert.Equal(new[] { "close" }, requestHeaders.GetValues("Connection"));
+ Assert.Equal("", requestHeaders["Custom-Header"]);
+ Assert.Empty(requestHeaders.GetValues("Custom-Header"));
+ Assert.Equal("spacervalue", requestHeaders["Spacer-Header"]);
+ context.Dispose();
+
+ await responseTask;
+ }
+ }
+
+ private async Task<string> SendRequestAsync(string uri)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetStringAsync(uri);
+ }
+ }
+
+ private async Task SendRequestAsync(string address, string customHeader, string[] customValues)
+ {
+ var uri = new Uri(address);
+ StringBuilder builder = new StringBuilder();
+ builder.AppendLine("GET / HTTP/1.1");
+ builder.AppendLine("Connection: close");
+ builder.Append("HOST: ");
+ builder.AppendLine(uri.Authority);
+ foreach (string value in customValues)
+ {
+ builder.Append(customHeader);
+ builder.Append(": ");
+ builder.AppendLine(value);
+ builder.AppendLine("Spacer-Header: spacervalue");
+ }
+ builder.AppendLine();
+
+ byte[] request = Encoding.UTF8.GetBytes(builder.ToString());
+
+ Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
+ socket.Connect(uri.Host, uri.Port);
+
+ socket.Send(request);
+
+ byte[] response = new byte[1024 * 5];
+ await Task.Run(() => socket.Receive(response));
+ socket.Dispose();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/RequestTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/RequestTests.cs
new file mode 100644
index 0000000000..20ea25c514
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/RequestTests.cs
@@ -0,0 +1,312 @@
+// 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 System.Net.Http;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.Listener
+{
+ public class RequestTests
+ {
+ [ConditionalFact]
+ public async Task Request_SimpleGet_Success()
+ {
+ string root;
+ using (var server = Utilities.CreateHttpServerReturnRoot("/basepath", out root))
+ {
+ Task<string> responseTask = SendRequestAsync(root + "/basepath/SomePath?SomeQuery");
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+
+ // General fields
+ var request = context.Request;
+
+ // Request Keys
+ Assert.Equal("GET", request.Method);
+ Assert.Equal(Stream.Null, request.Body);
+ Assert.NotNull(request.Headers);
+ Assert.Equal("http", request.Scheme);
+ Assert.Equal("/basepath", request.PathBase);
+ Assert.Equal("/SomePath", request.Path);
+ Assert.Equal("?SomeQuery", request.QueryString);
+ Assert.Equal(new Version(1, 1), request.ProtocolVersion);
+
+ Assert.Equal("::1", request.RemoteIpAddress.ToString());
+ Assert.NotEqual(0, request.RemotePort);
+ Assert.Equal("::1", request.LocalIpAddress.ToString());
+ Assert.NotEqual(0, request.LocalPort);
+
+ // Note: Response keys are validated in the ResponseTests
+
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData("/", "/", "", "/")]
+ [InlineData("/basepath/", "/basepath", "/basepath", "")]
+ [InlineData("/basepath/", "/basepath/", "/basepath", "/")]
+ [InlineData("/basepath/", "/basepath/subpath", "/basepath", "/subpath")]
+ [InlineData("/base path/", "/base%20path/sub%20path", "/base path", "/sub path")]
+ [InlineData("/base葉path/", "/base%E8%91%89path/sub%E8%91%89path", "/base葉path", "/sub葉path")]
+ [InlineData("/basepath/", "/basepath/sub%2Fpath", "/basepath", "/sub%2Fpath")]
+ public async Task Request_PathSplitting(string pathBase, string requestPath, string expectedPathBase, string expectedPath)
+ {
+ string root;
+ using (var server = Utilities.CreateHttpServerReturnRoot(pathBase, out root))
+ {
+ Task<string> responseTask = SendRequestAsync(root + requestPath);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+
+ // General fields
+ var request = context.Request;
+
+ // Request Keys
+ Assert.Equal("http", request.Scheme);
+ Assert.Equal(expectedPath, request.Path);
+ Assert.Equal(expectedPathBase, request.PathBase);
+ Assert.Equal(string.Empty, request.QueryString);
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData("/path%")]
+ [InlineData("/path%XY")]
+ [InlineData("/path%F")]
+ [InlineData("/path with spaces")]
+ public async Task Request_MalformedPathReturns400StatusCode(string requestPath)
+ {
+ string root;
+ using (var server = Utilities.CreateHttpServerReturnRoot("/", out root))
+ {
+ var responseTask = SendSocketRequestAsync(root, requestPath);
+ var contextTask = server.AcceptAsync(Utilities.DefaultTimeout);
+ var response = await responseTask;
+ var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 "
+ Assert.Equal("400", responseStatusCode);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Request_DoubleEscapingAllowed()
+ {
+ string root;
+ using (var server = Utilities.CreateHttpServerReturnRoot("/", out root))
+ {
+ var responseTask = SendSocketRequestAsync(root, "/%252F");
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.Equal("/%2F", context.Request.Path);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Request_FullUriInRequestLine_ParsesPath()
+ {
+ string root;
+ using (var server = Utilities.CreateHttpServerReturnRoot("/", out root))
+ {
+ // Send a HTTP request with the request line:
+ // GET http://localhost:5001 HTTP/1.1
+ var responseTask = SendSocketRequestAsync(root, root);
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.Equal("/", context.Request.Path);
+ Assert.Equal("", context.Request.PathBase);
+ Assert.Equal(root, context.Request.RawUrl);
+ Assert.False(root.EndsWith("/")); // make sure root doesn't have a trailing slash
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Request_OptionsStar_EmptyPath()
+ {
+ string root;
+ using (var server = Utilities.CreateHttpServerReturnRoot("/", out root))
+ {
+ var responseTask = SendSocketRequestAsync(root, "*", "OPTIONS");
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.Equal("", context.Request.PathBase);
+ Assert.Equal("", context.Request.Path);
+ Assert.Equal("*", context.Request.RawUrl);
+ context.Dispose();
+ }
+ }
+
+ [ConditionalTheory]
+ // The test server defines these prefixes: "/", "/11", "/2/3", "/2", "/11/2"
+ [InlineData("/", "", "/")]
+ [InlineData("/random", "", "/random")]
+ [InlineData("/11", "/11", "")]
+ [InlineData("/11/", "/11", "/")]
+ [InlineData("/11/random", "/11", "/random")]
+ [InlineData("/2", "/2", "")]
+ [InlineData("/2/", "/2", "/")]
+ [InlineData("/2/random", "/2", "/random")]
+ [InlineData("/2/3", "/2/3", "")]
+ [InlineData("/2/3/", "/2/3", "/")]
+ [InlineData("/2/3/random", "/2/3", "/random")]
+ public async Task Request_MultiplePrefixes(string requestUri, string expectedPathBase, string expectedPath)
+ {
+ // TODO: We're just doing this to get a dynamic port. This can be removed later when we add support for hot-adding prefixes.
+ string root;
+ var server = Utilities.CreateHttpServerReturnRoot("/", out root);
+ server.Dispose();
+ server = new HttpSysListener(new HttpSysOptions(), new LoggerFactory());
+ using (server)
+ {
+ var uriBuilder = new UriBuilder(root);
+ foreach (string path in new[] { "/", "/11", "/2/3", "/2", "/11/2" })
+ {
+ server.Options.UrlPrefixes.Add(UrlPrefix.Create(uriBuilder.Scheme, uriBuilder.Host, uriBuilder.Port, path));
+ }
+ server.Start();
+
+ Task<string> responseTask = SendRequestAsync(root + requestUri);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var request = context.Request;
+
+ Assert.Equal(expectedPath, request.Path);
+ Assert.Equal(expectedPathBase, request.PathBase);
+
+ context.Dispose();
+
+ string response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData("%D0%A4", "Ф")]
+ [InlineData("%d0%a4", "Ф")]
+ [InlineData("%E0%A4%AD", "भ")]
+ [InlineData("%e0%A4%Ad", "भ")]
+ [InlineData("%F0%A4%AD%A2", "𤭢")]
+ [InlineData("%F0%a4%Ad%a2", "𤭢")]
+ [InlineData("%48%65%6C%6C%6F%20%57%6F%72%6C%64", "Hello World")]
+ [InlineData("%48%65%6C%6C%6F%2D%C2%B5%40%C3%9F%C3%B6%C3%A4%C3%BC%C3%A0%C3%A1", "Hello-µ@ßöäüàá")]
+ // Test the borderline cases of overlong UTF8.
+ [InlineData("%C2%80", "\u0080")]
+ [InlineData("%E0%A0%80", "\u0800")]
+ [InlineData("%F0%90%80%80", "\U00010000")]
+ [InlineData("%63", "c")]
+ [InlineData("%32", "2")]
+ [InlineData("%20", " ")]
+ // Internationalized
+ [InlineData("%C3%84ra%20Benetton", "Ära Benetton")]
+ [InlineData("%E6%88%91%E8%87%AA%E6%A8%AA%E5%88%80%E5%90%91%E5%A4%A9%E7%AC%91%E5%8E%BB%E7%95%99%E8%82%9D%E8%83%86%E4%B8%A4%E6%98%86%E4%BB%91", "我自横刀向天笑去留肝胆两昆仑")]
+ // Skip forward slash
+ [InlineData("%2F", "%2F")]
+ [InlineData("foo%2Fbar", "foo%2Fbar")]
+ [InlineData("foo%2F%20bar", "foo%2F bar")]
+ public async Task Request_PathDecodingValidUTF8(string requestPath, string expect)
+ {
+ string root;
+ string actualPath;
+ using (var server = Utilities.CreateHttpServerReturnRoot("/", out root))
+ {
+ var responseTask = SendSocketRequestAsync(root, "/" + requestPath);
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ actualPath = context.Request.Path;
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal("200", response.Substring(9));
+ }
+
+ Assert.Equal(expect, actualPath.TrimStart('/'));
+ }
+
+ [ConditionalTheory]
+ [InlineData("/%%32")]
+ [InlineData("/%%20")]
+ [InlineData("/%F0%8F%8F%BF")]
+ [InlineData("/%")]
+ [InlineData("/%%")]
+ [InlineData("/%A")]
+ [InlineData("/%Y")]
+ public async Task Request_PathDecodingInvalidUTF8(string requestPath)
+ {
+ string root;
+ using (var server = Utilities.CreateHttpServerReturnRoot("/", out root))
+ {
+ var responseTask = SendSocketRequestAsync(root, requestPath);
+ var contextTask = server.AcceptAsync(Utilities.DefaultTimeout);
+
+ var response = await responseTask;
+ Assert.Equal("400", response.Substring(9));
+ }
+ }
+
+ [ConditionalTheory]
+ // Overlong ASCII
+ [InlineData("/%C0%A4", "/%C0%A4")]
+ [InlineData("/%C1%BF", "/%C1%BF")]
+ [InlineData("/%E0%80%AF", "/%E0%80%AF")]
+ [InlineData("/%E0%9F%BF", "/%E0%9F%BF")]
+ [InlineData("/%F0%80%80%AF", "/%F0%80%80%AF")]
+ [InlineData("/%F0%80%BF%BF", "/%F0%80%BF%BF")]
+ // Mixed
+ [InlineData("/%C0%A4%32", "/%C0%A42")]
+ [InlineData("/%32%C0%A4%32", "/2%C0%A42")]
+ [InlineData("/%C0%32%A4", "/%C02%A4")]
+ public async Task Request_OverlongUTF8Path(string requestPath, string expectedPath)
+ {
+ string root;
+ using (var server = Utilities.CreateHttpServerReturnRoot("/", out root))
+ {
+ var responseTask = SendSocketRequestAsync(root, requestPath);
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.Equal(expectedPath, context.Request.Path);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal("200", response.Substring(9));
+ }
+ }
+
+ private async Task<string> SendRequestAsync(string uri)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetStringAsync(uri);
+ }
+ }
+
+ private async Task<string> SendSocketRequestAsync(string address, string path, string method = "GET")
+ {
+ var uri = new Uri(address);
+ StringBuilder builder = new StringBuilder();
+ builder.AppendLine($"{method} {path} HTTP/1.1");
+ builder.AppendLine("Connection: close");
+ builder.Append("HOST: ");
+ builder.AppendLine(uri.Authority);
+ builder.AppendLine();
+
+ byte[] request = Encoding.ASCII.GetBytes(builder.ToString());
+
+ using (var socket = new Socket(SocketType.Stream, ProtocolType.Tcp))
+ {
+ socket.Connect(uri.Host, uri.Port);
+ socket.Send(request);
+ var response = new byte[12];
+ await Task.Run(() => socket.Receive(response));
+ return Encoding.ASCII.GetString(response);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseBodyTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseBodyTests.cs
new file mode 100644
index 0000000000..0c91833773
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseBodyTests.cs
@@ -0,0 +1,663 @@
+// 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.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.Listener
+{
+ public class ResponseBodyTests
+ {
+ [ConditionalFact]
+ public async Task ResponseBody_SyncWriteEnabledByDefault_ThrowsWhenDisabled()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+
+ Assert.True(context.AllowSynchronousIO);
+
+ context.Response.Body.Flush();
+ context.Response.Body.Write(new byte[10], 0, 10);
+ context.Response.Body.Flush();
+
+ context.AllowSynchronousIO = false;
+
+ Assert.Throws<InvalidOperationException>(() => context.Response.Body.Flush());
+ Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(new byte[10], 0, 10));
+ Assert.Throws<InvalidOperationException>(() => context.Response.Body.Flush());
+
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
+ Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteNoHeaders_DefaultsToChunked()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ server.Options.AllowSynchronousIO = true;
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Body.Write(new byte[10], 0, 10);
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
+ Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_FlushThenWrite_DefaultsToChunkedAndTerminates()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ server.Options.AllowSynchronousIO = true;
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Body.Write(new byte[10], 0, 10);
+ context.Response.Body.Flush();
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.HasValue);
+ Assert.Equal(20, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteChunked_ManuallyChunked()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["transfeR-Encoding"] = "CHunked";
+ Stream stream = context.Response.Body;
+ var responseBytes = Encoding.ASCII.GetBytes("10\r\nManually Chunked\r\n0\r\n\r\n");
+ await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
+ Assert.Equal("Manually Chunked", await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteContentLength_PassedThrough()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ server.Options.AllowSynchronousIO = true;
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["Content-lenGth"] = " 30 ";
+ var stream = context.Response.Body;
+ stream.EndWrite(stream.BeginWrite(new byte[10], 0, 10, null, null));
+ stream.Write(new byte[10], 0, 10);
+ await stream.WriteAsync(new byte[10], 0, 10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ IEnumerable<string> contentLength;
+ Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.Equal("30", contentLength.First());
+ Assert.Null(response.Headers.TransferEncodingChunked);
+ Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteContentLengthNoneWritten_Aborts()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["Content-lenGth"] = " 20 ";
+ context.Dispose();
+#if NET461
+ // HttpClient retries the request because it didn't get a response.
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["Content-lenGth"] = " 20 ";
+ context.Dispose();
+#elif NETCOREAPP2_0 || NETCOREAPP2_1
+#else
+#error Target framework needs to be updated
+#endif
+ await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteContentLengthNotEnoughWritten_Aborts()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["Content-lenGth"] = " 20 ";
+ context.Response.Body.Write(new byte[5], 0, 5);
+ context.Dispose();
+
+ await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteContentLengthTooMuchWritten_Throws()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["Content-lenGth"] = " 10 ";
+ context.Response.Body.Write(new byte[5], 0, 5);
+ Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(new byte[6], 0, 6));
+ context.Dispose();
+
+ await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteContentLengthExtraWritten_Throws()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ server.Options.AllowSynchronousIO = true;
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["Content-lenGth"] = " 10 ";
+ context.Response.Body.Write(new byte[10], 0, 10);
+ Assert.Throws<ObjectDisposedException>(() => context.Response.Body.Write(new byte[6], 0, 6));
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ IEnumerable<string> contentLength;
+ Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.Equal("10", contentLength.First());
+ Assert.Null(response.Headers.TransferEncodingChunked);
+ Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteZeroCount_StartsChunkedResponse()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ server.Options.AllowSynchronousIO = true;
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Body.Write(new byte[10], 0, 0);
+ Assert.True(context.Response.HasStarted);
+ await context.Response.Body.WriteAsync(new byte[10], 0, 0);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteAsyncWithActiveCancellationToken_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cts = new CancellationTokenSource();
+ // First write sends headers
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteAsyncWithTimerCancellationToken_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cts = new CancellationTokenSource();
+ cts.CancelAfter(TimeSpan.FromSeconds(10));
+ // First write sends headers
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBodyWriteExceptions_FirstWriteAsyncWithCanceledCancellationToken_CancelsAndAborts()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ server.Options.ThrowWriteExceptions = true;
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+ // First write sends headers
+ var writeTask = context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
+ Assert.True(writeTask.IsCanceled);
+ context.Dispose();
+#if NET461
+ // HttpClient retries the request because it didn't get a response.
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ cts = new CancellationTokenSource();
+ cts.Cancel();
+ // First write sends headers
+ writeTask = context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
+ Assert.True(writeTask.IsCanceled);
+ context.Dispose();
+#elif NETCOREAPP2_0 || NETCOREAPP2_1
+#else
+#error Target framework needs to be updated
+#endif
+ await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_FirstWriteAsyncWithCanceledCancellationToken_CancelsAndAborts()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+ // First write sends headers
+ var writeTask = context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
+ Assert.True(writeTask.IsCanceled);
+ context.Dispose();
+#if NET461
+ // HttpClient retries the request because it didn't get a response.
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ cts = new CancellationTokenSource();
+ cts.Cancel();
+ // First write sends headers
+ writeTask = context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
+ Assert.True(writeTask.IsCanceled);
+ context.Dispose();
+#elif NETCOREAPP2_0 || NETCOREAPP2_1
+#else
+#error Target framework needs to be updated
+#endif
+ await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBodyWriteExceptions_SecondWriteAsyncWithCanceledCancellationToken_CancelsAndAborts()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ server.Options.ThrowWriteExceptions = true;
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cts = new CancellationTokenSource();
+ // First write sends headers
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
+ cts.Cancel();
+ var writeTask = context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
+ Assert.True(writeTask.IsCanceled);
+ context.Dispose();
+
+ await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_SecondWriteAsyncWithCanceledCancellationToken_CancelsAndAborts()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cts = new CancellationTokenSource();
+ // First write sends headers
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
+ cts.Cancel();
+ var writeTask = context.Response.Body.WriteAsync(new byte[10], 0, 10, cts.Token);
+ Assert.True(writeTask.IsCanceled);
+ context.Dispose();
+
+ await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBodyWriteExceptions_ClientDisconnectsBeforeFirstWrite_WriteThrows()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ server.Options.ThrowWriteExceptions = true;
+ server.Options.AllowSynchronousIO = true;
+ var cts = new CancellationTokenSource();
+ var responseTask = SendRequestAsync(address, cts.Token);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ // First write sends headers
+ cts.Cancel();
+ await Assert.ThrowsAnyAsync<OperationCanceledException>(() => responseTask);
+ Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
+ Assert.Throws<IOException>(() =>
+ {
+ // It can take several tries before Write notices the disconnect.
+ for (int i = 0; i < Utilities.WriteRetryLimit; i++)
+ {
+ context.Response.Body.Write(new byte[1000], 0, 1000);
+ }
+ });
+
+ Assert.Throws<ObjectDisposedException>(() => context.Response.Body.Write(new byte[1000], 0, 1000));
+
+ context.Dispose();
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBodyWriteExceptions_ClientDisconnectsBeforeFirstWriteAsync_WriteThrows()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ server.Options.ThrowWriteExceptions = true;
+ var cts = new CancellationTokenSource();
+ var responseTask = SendRequestAsync(address, cts.Token);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+
+ // First write sends headers
+ cts.Cancel();
+ await Assert.ThrowsAnyAsync<OperationCanceledException>(() => responseTask);
+
+ Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
+ await Assert.ThrowsAsync<IOException>(async () =>
+ {
+ // It can take several tries before Write notices the disconnect.
+ for (int i = 0; i < Utilities.WriteRetryLimit; i++)
+ {
+ await context.Response.Body.WriteAsync(new byte[1000], 0, 1000);
+ }
+ });
+
+ await Assert.ThrowsAsync<ObjectDisposedException>(() => context.Response.Body.WriteAsync(new byte[1000], 0, 1000));
+
+ context.Dispose();
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_ClientDisconnectsBeforeFirstWrite_WriteCompletesSilently()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var cts = new CancellationTokenSource();
+ var responseTask = SendRequestAsync(address, cts.Token);
+
+ server.Options.AllowSynchronousIO = true;
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ // First write sends headers
+ cts.Cancel();
+ await Assert.ThrowsAnyAsync<OperationCanceledException>(() => responseTask);
+ Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
+ // It can take several tries before Write notices the disconnect.
+ for (int i = 0; i < Utilities.WriteRetryLimit; i++)
+ {
+ context.Response.Body.Write(new byte[1000], 0, 1000);
+ }
+ context.Dispose();
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_ClientDisconnectsBeforeFirstWriteAsync_WriteCompletesSilently()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var cts = new CancellationTokenSource();
+ var responseTask = SendRequestAsync(address, cts.Token);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ // First write sends headers
+ cts.Cancel();
+ await Assert.ThrowsAnyAsync<OperationCanceledException>(() => responseTask);
+ Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
+ // It can take several tries before Write notices the disconnect.
+ for (int i = 0; i < Utilities.WriteRetryLimit; i++)
+ {
+ await context.Response.Body.WriteAsync(new byte[1000], 0, 1000);
+ }
+ context.Dispose();
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBodyWriteExceptions_ClientDisconnectsBeforeSecondWrite_WriteThrows()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ server.Options.ThrowWriteExceptions = true;
+ RequestContext context;
+ using (var client = new HttpClient())
+ {
+ var responseTask = client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ // First write sends headers
+ context.Response.Body.Write(new byte[10], 0, 10);
+
+ var response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ response.Dispose();
+ }
+
+ Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
+ Assert.Throws<IOException>(() =>
+ {
+ // It can take several tries before Write notices the disconnect.
+ for (int i = 0; i < Utilities.WriteRetryLimit; i++)
+ {
+ context.Response.Body.Write(new byte[1000], 0, 1000);
+ }
+ });
+ context.Dispose();
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBodyWriteExceptions_ClientDisconnectsBeforeSecondWriteAsync_WriteThrows()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ server.Options.ThrowWriteExceptions = true;
+ RequestContext context;
+ using (var client = new HttpClient())
+ {
+ var responseTask = client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ // First write sends headers
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+
+ var response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ response.Dispose();
+ }
+
+ Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
+ await Assert.ThrowsAsync<IOException>(async () =>
+ {
+ // It can take several tries before Write notices the disconnect.
+ for (int i = 0; i < Utilities.WriteRetryLimit; i++)
+ {
+ await context.Response.Body.WriteAsync(new byte[1000], 0, 1000);
+ }
+ });
+ context.Dispose();
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_ClientDisconnectsBeforeSecondWrite_WriteCompletesSilently()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ server.Options.AllowSynchronousIO = true;
+ RequestContext context;
+ using (var client = new HttpClient())
+ {
+ var responseTask = client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ // First write sends headers
+ context.Response.Body.Write(new byte[10], 0, 10);
+
+ var response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ response.Dispose();
+ }
+
+ Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
+ // It can take several tries before Write notices the disconnect.
+ for (int i = 0; i < Utilities.WriteRetryLimit; i++)
+ {
+ context.Response.Body.Write(new byte[1000], 0, 1000);
+ }
+ context.Dispose();
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_ClientDisconnectsBeforeSecondWriteAsync_WriteCompletesSilently()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ RequestContext context;
+ using (var client = new HttpClient())
+ {
+ var responseTask = client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ // First write sends headers
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+
+ var response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ response.Dispose();
+ }
+
+ Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
+ // It can take several tries before Write notices the disconnect.
+ for (int i = 0; i < Utilities.WriteRetryLimit; i++)
+ {
+ await context.Response.Body.WriteAsync(new byte[1000], 0, 1000);
+ }
+ context.Dispose();
+ }
+ }
+
+ private async Task<HttpResponseMessage> SendRequestAsync(string uri, CancellationToken cancellationToken = new CancellationToken())
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetAsync(uri, cancellationToken);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseCachingTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseCachingTests.cs
new file mode 100644
index 0000000000..b3bc680e72
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseCachingTests.cs
@@ -0,0 +1,1168 @@
+// 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 System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.Listener
+{
+ public class ResponseCachingTests
+ {
+ private readonly string _absoluteFilePath;
+ private readonly long _fileLength;
+
+ public ResponseCachingTests()
+ {
+ _absoluteFilePath = Directory.GetFiles(Directory.GetCurrentDirectory()).First();
+ _fileLength = new FileInfo(_absoluteFilePath).Length;
+ }
+
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win2008R2, WindowsVersions.Win7, SkipReason = "Content type not required for caching on Win7 and Win2008R2.")]
+ public async Task Caching_SetTtlWithoutContentType_NotCached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ responseTask = SendRequestAsync(address);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "2";
+ // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_SetTtlWithoutContentType_Cached_OnWin7AndWin2008R2()
+ {
+ if (Utilities.IsWin8orLater)
+ {
+ return;
+ }
+
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ // Http.sys does not require a content-type to cache on Win7 and Win2008R2
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_SetTtlWithContentType_Cached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ // Http.Sys does not set the optional Age header for cached content.
+ // http://tools.ietf.org/html/rfc7234#section-5.1
+ public async Task Caching_CheckAge_NotSentWithCachedContent()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ Assert.False(response.Headers.Age.HasValue);
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ Assert.False(response.Headers.Age.HasValue);
+ }
+ }
+
+ [ConditionalFact]
+ // Http.Sys does not update the optional Age header for cached content.
+ // http://tools.ietf.org/html/rfc7234#section-5.1
+ public async Task Caching_SetAge_AgeHeaderCachedAndNotUpdated()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.Headers["age"] = "12345";
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ Assert.True(response.Headers.Age.HasValue);
+ Assert.Equal(TimeSpan.FromSeconds(12345), response.Headers.Age.Value);
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ Assert.True(response.Headers.Age.HasValue);
+ Assert.Equal(TimeSpan.FromSeconds(12345), response.Headers.Age.Value);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_SetTtlZeroSeconds_NotCached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(0);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ responseTask = SendRequestAsync(address);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "2";
+ // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_SetTtlMiliseconds_NotCached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromMilliseconds(900);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ responseTask = SendRequestAsync(address);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "2";
+ // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_SetTtlNegative_NotCached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(-10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ responseTask = SendRequestAsync(address);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "2";
+ // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_SetTtlHuge_Cached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.MaxValue;
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_SetTtlAndWriteBody_Cached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.ContentLength = 10;
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+ // Http.Sys will add this for us
+ Assert.Null(context.Response.ContentLength);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_SetTtlAndWriteAsyncBody_Cached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.ContentLength = 10;
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+ // Http.Sys will add this for us
+ Assert.Null(context.Response.ContentLength);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_Flush_NotCached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ server.Options.AllowSynchronousIO = true;
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Response.Body.Flush();
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ responseTask = SendRequestAsync(address);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "2";
+ context.Dispose();
+
+ response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_WriteFlush_NotCached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+ await context.Response.Body.FlushAsync();
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
+
+ responseTask = SendRequestAsync(address);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "2";
+ context.Dispose();
+
+ response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_WriteFullContentLength_Cached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.ContentLength = 10;
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+ // Http.Sys will add this for us
+ Assert.Null(context.Response.ContentLength);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(10, response.Content.Headers.ContentLength);
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(10, response.Content.Headers.ContentLength);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_SendFileNoContentLength_NotCached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ await context.Response.SendFileAsync(_absoluteFilePath, 0, null, CancellationToken.None);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(_fileLength, response.Content.Headers.ContentLength);
+
+ responseTask = SendRequestAsync(address);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "2";
+ context.Dispose();
+
+ response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_SendFileWithFullContentLength_Cached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.ContentLength =_fileLength;
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ await context.Response.SendFileAsync(_absoluteFilePath, 0, null, CancellationToken.None);
+ // Http.Sys will add this for us
+ Assert.Null(context.Response.ContentLength);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(_fileLength, response.Content.Headers.ContentLength);
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(_fileLength, response.Content.Headers.ContentLength);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_SetTtlAndStatusCode_Cached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ // Http.Sys will cache almost any status code.
+ for (int status = 200; status < 600; status++)
+ {
+ switch (status)
+ {
+ case 206: // 206 (Partial Content) is not cached
+ case 407: // 407 (Proxy Authentication Required) makes CoreCLR's HttpClient throw
+ continue;
+ }
+
+ var responseTask = SendRequestAsync(address + status);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.StatusCode = status;
+ context.Response.Headers["x-request-count"] = status.ToString();
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ HttpResponseMessage response;
+ try
+ {
+ response = await responseTask;
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Failed to get first response for {status}", ex);
+ }
+ Assert.Equal(status, (int)response.StatusCode);
+ Assert.Equal(status.ToString(), response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ try
+ {
+ response = await SendRequestAsync(address + status);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Failed to get second response for {status}", ex);
+ }
+ Assert.Equal(status, (int)response.StatusCode);
+ Assert.Equal(status.ToString(), response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+ }
+
+ // Only GET requests can have cached responses.
+ [ConditionalTheory]
+ // See HTTP_VERB for known verbs
+ [InlineData("HEAD")]
+ [InlineData("UNKNOWN")]
+ [InlineData("INVALID")]
+ [InlineData("OPTIONS")]
+ [InlineData("DELETE")]
+ [InlineData("TRACE")]
+ [InlineData("TRACK")]
+ [InlineData("MOVE")]
+ [InlineData("COPY")]
+ [InlineData("PROPFIND")]
+ [InlineData("PROPPATCH")]
+ [InlineData("MKCOL")]
+ [InlineData("LOCK")]
+ [InlineData("UNLOCK")]
+ [InlineData("SEARCH")]
+ [InlineData("CUSTOMVERB")]
+ [InlineData("PATCH")]
+ [InlineData("POST")]
+ [InlineData("PUT")]
+ // [InlineData("CONNECT", null)] 400 bad request if it's not a WebSocket handshake.
+ public async Task Caching_VariousUnsupportedRequestMethods_NotCached(string method)
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address, method);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = context.Request.Method + "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(method + "1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ responseTask = SendRequestAsync(address, method);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = context.Request.Method + "2";
+ // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(method + "2", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ // RFC violation. http://tools.ietf.org/html/rfc7234#section-4.4
+ // "A cache MUST invalidate the effective Request URI ... when a non-error status code
+ // is received in response to an unsafe request method."
+ [ConditionalTheory]
+ // See HTTP_VERB for known verbs
+ [InlineData("HEAD")]
+ [InlineData("UNKNOWN")]
+ [InlineData("INVALID")]
+ [InlineData("OPTIONS")]
+ [InlineData("DELETE")]
+ [InlineData("TRACE")]
+ [InlineData("TRACK")]
+ [InlineData("MOVE")]
+ [InlineData("COPY")]
+ [InlineData("PROPFIND")]
+ [InlineData("PROPPATCH")]
+ [InlineData("MKCOL")]
+ [InlineData("LOCK")]
+ [InlineData("UNLOCK")]
+ [InlineData("SEARCH")]
+ [InlineData("CUSTOMVERB")]
+ [InlineData("PATCH")]
+ [InlineData("POST")]
+ [InlineData("PUT")]
+ // [InlineData("CONNECT", null)] 400 bad request if it's not a WebSocket handshake.
+ public async Task Caching_UnsupportedRequestMethods_BypassCacheAndLeaveItIntact(string method)
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ // Cache the first response
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = context.Request.Method + "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("GET1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ // Try to clear the cache with a second request
+ responseTask = SendRequestAsync(address, method);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = context.Request.Method + "2";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Dispose();
+
+ response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(method + "2", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ // Send a third request to check the cache.
+ responseTask = SendRequestAsync(address);
+
+ // The cache wasn't cleared when it should have been
+ response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("GET1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ // RFC violation / implementation limiation, Vary is not respected.
+ // http://tools.ietf.org/html/rfc7234#section-4.1
+ [ConditionalFact]
+ public async Task Caching_SetVary_NotRespected()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address, "GET", "x-vary", "vary1");
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.Headers["vary"] = "x-vary";
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal("x-vary", response.Headers.GetValues("vary").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ response = await SendRequestAsync(address, "GET", "x-vary", "vary2");
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal("x-vary", response.Headers.GetValues("vary").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ // http://tools.ietf.org/html/rfc7234#section-3.2
+ [ConditionalFact]
+ public async Task Caching_RequestAuthorization_NotCached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address, "GET", "Authorization", "Basic abc123");
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ responseTask = SendRequestAsync(address, "GET", "Authorization", "Basic abc123");
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "2";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Dispose();
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_RequestAuthorization_NotServedFromCache()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ responseTask = SendRequestAsync(address, "GET", "Authorization", "Basic abc123");
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "2";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Dispose();
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ // Responses can be cached for requests with Pragma: no-cache.
+ // http://tools.ietf.org/html/rfc7234#section-5.2.1.4
+ [ConditionalFact]
+ public async Task Caching_RequestPragmaNoCache_Cached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address, "GET", "Pragma", "no-cache");
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ // RFC violation, Requests with Pragma: no-cache should not be served from cache.
+ // http://tools.ietf.org/html/rfc7234#section-5.4
+ // http://tools.ietf.org/html/rfc7234#section-5.2.1.4
+ [ConditionalFact]
+ public async Task Caching_RequestPragmaNoCache_NotRespectedAndServedFromCache()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ response = await SendRequestAsync(address, "GET", "Pragma", "no-cache");
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ // Responses can be cached for requests with cache-control: no-cache.
+ // http://tools.ietf.org/html/rfc7234#section-5.2.1.4
+ [ConditionalFact]
+ public async Task Caching_RequestCacheControlNoCache_Cached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address, "GET", "Cache-Control", "no-cache");
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ // RFC violation, Requests with Cache-Control: no-cache should not be served from cache.
+ // http://tools.ietf.org/html/rfc7234#section-5.2.1.4
+ [ConditionalFact]
+ public async Task Caching_RequestCacheControlNoCache_NotRespectedAndServedFromCache()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ response = await SendRequestAsync(address, "GET", "Cache-Control", "no-cache");
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ // RFC violation
+ // http://tools.ietf.org/html/rfc7234#section-5.2.1.1
+ [ConditionalFact]
+ public async Task Caching_RequestCacheControlMaxAgeZero_NotRespectedAndServedFromCache()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ response = await SendRequestAsync(address, "GET", "Cache-Control", "min-fresh=0");
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ // RFC violation
+ // http://tools.ietf.org/html/rfc7234#section-5.2.1.3
+ [ConditionalFact]
+ public async Task Caching_RequestCacheControlMinFreshOutOfRange_NotRespectedAndServedFromCache()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+
+ response = await SendRequestAsync(address, "GET", "Cache-Control", "min-fresh=20");
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ // Http.Sys limitation, partial responses are not cached.
+ [ConditionalFact]
+ public async Task Caching_CacheRange_NotCached()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address, "GET", "Range", "bytes=0-10");
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.StatusCode = 206;
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.Headers["content-range"] = "bytes 0-10/100";
+ context.Response.ContentLength = 11;
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ await context.Response.Body.WriteAsync(new byte[100], 0, 11);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(206, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[11], await response.Content.ReadAsByteArrayAsync());
+
+ responseTask = SendRequestAsync(address, "GET", "Range", "bytes=0-10");
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.StatusCode = 206;
+ context.Response.Headers["x-request-count"] = "2";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.Headers["content-range"] = "bytes 0-10/100";
+ context.Response.ContentLength = 11;
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ await context.Response.Body.WriteAsync(new byte[100], 0, 11);
+ context.Dispose();
+
+ response = await responseTask;
+ Assert.Equal(206, (int)response.StatusCode);
+ Assert.Equal("2", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal("bytes 0-10/100", response.Content.Headers.GetValues("content-range").FirstOrDefault());
+ Assert.Equal(new byte[11], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ // http://tools.ietf.org/html/rfc7233#section-4.1
+ [ConditionalFact]
+ public async Task Caching_RequestRangeFromCache_RangeServedFromCache()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.ContentLength = 100;
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ await context.Response.Body.WriteAsync(new byte[100], 0, 100);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[100], await response.Content.ReadAsByteArrayAsync());
+
+ response = await SendRequestAsync(address, "GET", "Range", "bytes=0-10", HttpCompletionOption.ResponseHeadersRead);
+ Assert.Equal(206, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal("bytes 0-10/100", response.Content.Headers.GetValues("content-range").FirstOrDefault());
+ Assert.Equal(11, response.Content.Headers.ContentLength);
+ }
+ }
+
+ // http://tools.ietf.org/html/rfc7233#section-4.1
+ [ConditionalFact]
+ public async Task Caching_RequestMultipleRangesFromCache_RangesServedFromCache()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.ContentLength = 100;
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ await context.Response.Body.WriteAsync(new byte[100], 0, 100);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(new byte[100], await response.Content.ReadAsByteArrayAsync());
+
+ response = await SendRequestAsync(address, "GET", "Range", "bytes=0-10,15-20");
+ Assert.Equal(206, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.StartsWith("multipart/byteranges;", response.Content.Headers.GetValues("content-type").First());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_RequestRangeFromCachedFile_ServedFromCache()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseLength = _fileLength / 2; // Make sure it handles partial files.
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.ContentLength = responseLength;
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ await context.Response.SendFileAsync(_absoluteFilePath, 0, responseLength, CancellationToken.None);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(responseLength, response.Content.Headers.ContentLength);
+
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ var rangeLength = responseLength / 2;
+ response = await SendRequestAsync(address, "GET", "Range", "bytes=0-" + (rangeLength - 1), HttpCompletionOption.ResponseHeadersRead);
+ Assert.Equal(206, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(rangeLength, response.Content.Headers.ContentLength);
+ Assert.Equal("bytes 0-" + (rangeLength - 1) + "/" + responseLength, response.Content.Headers.GetValues("content-range").FirstOrDefault());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_RequestMultipleRangesFromCachedFile_ServedFromCache()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseLength = _fileLength / 2; // Make sure it handles partial files.
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["x-request-count"] = "1";
+ context.Response.Headers["content-type"] = "some/thing"; // Http.sys requires a content-type to cache
+ context.Response.ContentLength = responseLength;
+ context.Response.CacheTtl = TimeSpan.FromSeconds(10);
+ await context.Response.SendFileAsync(_absoluteFilePath, 0, responseLength, CancellationToken.None);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.Equal(responseLength, response.Content.Headers.ContentLength);
+ // Send a second request and make sure we get the same response (without listening for one on the server).
+ var rangeLength = responseLength / 4;
+ response = await SendRequestAsync(address, "GET", "Range", "bytes=0-" + (rangeLength - 1) + "," + rangeLength + "-" + (rangeLength + rangeLength - 1), HttpCompletionOption.ResponseHeadersRead);
+ Assert.Equal(206, (int)response.StatusCode);
+ Assert.Equal("1", response.Headers.GetValues("x-request-count").FirstOrDefault());
+ Assert.StartsWith("multipart/byteranges;", response.Content.Headers.GetValues("content-type").First());
+ }
+ }
+
+ private async Task<HttpResponseMessage> SendRequestAsync(string uri, string method = "GET", string extraHeader = null, string extraHeaderValue = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
+ {
+ using (var handler = new HttpClientHandler() { AllowAutoRedirect = false })
+ {
+ using (var client = new HttpClient(handler) { Timeout = Utilities.DefaultTimeout })
+ {
+ var request = new HttpRequestMessage(new HttpMethod(method), uri);
+ if (!string.IsNullOrEmpty(extraHeader))
+ {
+ request.Headers.Add(extraHeader, extraHeaderValue);
+ }
+ return await client.SendAsync(request, httpCompletionOption);
+ }
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseHeaderTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseHeaderTests.cs
new file mode 100644
index 0000000000..727dd69ec6
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseHeaderTests.cs
@@ -0,0 +1,541 @@
+// 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.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.Listener
+{
+ public class ResponseHeaderTests : IDisposable
+ {
+ private HttpClient _client = new HttpClient();
+
+ void IDisposable.Dispose()
+ {
+ _client.Dispose();
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_11Request_ServerSendsDefaultHeaders()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(2, response.Headers.Count());
+ Assert.False(response.Headers.TransferEncodingChunked.HasValue);
+ Assert.True(response.Headers.Date.HasValue);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
+ Assert.Single(response.Content.Headers);
+ Assert.Equal(0, response.Content.Headers.ContentLength);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_10Request_ServerSendsDefaultHeaders()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address, usehttp11: false);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(3, response.Headers.Count());
+ Assert.False(response.Headers.TransferEncodingChunked.HasValue);
+ Assert.True(response.Headers.ConnectionClose.Value);
+ Assert.True(response.Headers.Date.HasValue);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
+ Assert.Single(response.Content.Headers);
+ Assert.Equal(0, response.Content.Headers.ContentLength);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_11HeadRequest_ServerSendsDefaultHeaders()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendHeadRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(2, response.Headers.Count());
+ Assert.False(response.Headers.TransferEncodingChunked.HasValue);
+ Assert.True(response.Headers.Date.HasValue);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
+ Assert.False(response.Content.Headers.Contains("Content-Length"));
+ Assert.Empty(response.Content.Headers);
+
+ // Send a second request to check that the connection wasn't corrupted.
+ responseTask = SendHeadRequestAsync(address);
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+ response = await responseTask;
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_10HeadRequest_ServerSendsDefaultHeaders()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendHeadRequestAsync(address, usehttp11: false);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(3, response.Headers.Count());
+ Assert.False(response.Headers.TransferEncodingChunked.HasValue);
+ Assert.True(response.Headers.ConnectionClose.Value);
+ Assert.True(response.Headers.Date.HasValue);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
+ Assert.False(response.Content.Headers.Contains("Content-Length"));
+ Assert.Empty(response.Content.Headers);
+
+ // Send a second request to check that the connection wasn't corrupted.
+ responseTask = SendHeadRequestAsync(address);
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+ response = await responseTask;
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_11HeadRequestWithContentLength_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendHeadRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.ContentLength = 20;
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(2, response.Headers.Count());
+ Assert.False(response.Headers.TransferEncodingChunked.HasValue);
+ Assert.True(response.Headers.Date.HasValue);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
+ Assert.Single(response.Content.Headers);
+ Assert.Equal(20, response.Content.Headers.ContentLength);
+
+ // Send a second request to check that the connection wasn't corrupted.
+ responseTask = SendHeadRequestAsync(address);
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+ response = await responseTask;
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_11RequestStatusCodeWithoutBody_NoContentLengthOrChunkedOrClose()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.StatusCode = 204; // No Content
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(2, response.Headers.Count());
+ Assert.False(response.Headers.TransferEncodingChunked.HasValue);
+ Assert.True(response.Headers.Date.HasValue);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
+ Assert.False(response.Content.Headers.Contains("Content-Length"));
+ Assert.Empty(response.Content.Headers);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_11HeadRequestStatusCodeWithoutBody_NoContentLengthOrChunkedOrClose()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendHeadRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.StatusCode = 204; // No Content
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(2, response.Headers.Count());
+ Assert.False(response.Headers.TransferEncodingChunked.HasValue);
+ Assert.True(response.Headers.Date.HasValue);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
+ Assert.False(response.Content.Headers.Contains("Content-Length"));
+ Assert.Empty(response.Content.Headers);
+
+ // Send a second request to check that the connection wasn't corrupted.
+ responseTask = SendHeadRequestAsync(address);
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+ response = await responseTask;
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_ServerSendsSingleValueKnownHeaders_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ WebRequest request = WebRequest.Create(address);
+ Task<WebResponse> responseTask = request.GetResponseAsync();
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var responseHeaders = context.Response.Headers;
+ responseHeaders["WWW-Authenticate"] = "custom1";
+ context.Dispose();
+
+ // HttpClient would merge the headers no matter what
+ HttpWebResponse response = (HttpWebResponse)await responseTask;
+ Assert.Equal(4, response.Headers.Count);
+ Assert.Null(response.Headers["Transfer-Encoding"]);
+ Assert.Equal(0, response.ContentLength);
+ Assert.NotNull(response.Headers["Date"]);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]);
+ Assert.Equal("custom1", response.Headers["WWW-Authenticate"]);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_ServerSendsMultiValueKnownHeaders_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ WebRequest request = WebRequest.Create(address);
+ Task<WebResponse> responseTask = request.GetResponseAsync();
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var responseHeaders = context.Response.Headers;
+ responseHeaders["WWW-Authenticate"] = new[] { "custom1, and custom2", "custom3" };
+ context.Dispose();
+
+ // HttpClient would merge the headers no matter what
+ HttpWebResponse response = (HttpWebResponse)await responseTask;
+ Assert.Equal(4, response.Headers.Count);
+ Assert.Null(response.Headers["Transfer-Encoding"]);
+ Assert.Equal(0, response.ContentLength);
+ Assert.NotNull(response.Headers["Date"]);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]);
+#if NETCOREAPP2_0 || NETCOREAPP2_1 // WebHeaderCollection.GetValues() not available in CoreCLR.
+ Assert.Equal("custom1, and custom2, custom3", response.Headers["WWW-Authenticate"]);
+#elif NET461
+ Assert.Equal(new string[] { "custom1, and custom2", "custom3" }, response.Headers.GetValues("WWW-Authenticate"));
+#else
+#error Target framework needs to be updated
+#endif
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_ServerSendsCustomHeaders_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ WebRequest request = WebRequest.Create(address);
+ Task<WebResponse> responseTask = request.GetResponseAsync();
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var responseHeaders = context.Response.Headers;
+ responseHeaders["Custom-Header1"] = new[] { "custom1, and custom2", "custom3" };
+ context.Dispose();
+
+ // HttpClient would merge the headers no matter what
+ HttpWebResponse response = (HttpWebResponse)await responseTask;
+ Assert.Equal(4, response.Headers.Count);
+ Assert.Null(response.Headers["Transfer-Encoding"]);
+ Assert.Equal(0, response.ContentLength);
+ Assert.NotNull(response.Headers["Date"]);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]);
+#if NETCOREAPP2_0 || NETCOREAPP2_1 // WebHeaderCollection.GetValues() not available in CoreCLR.
+ Assert.Equal("custom1, and custom2, custom3", response.Headers["Custom-Header1"]);
+#elif NET461
+ Assert.Equal(new string[] { "custom1, and custom2", "custom3" }, response.Headers.GetValues("Custom-Header1"));
+#else
+#error Target framework needs to be updated
+#endif
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_ServerSendsConnectionClose_Closed()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var responseHeaders = context.Response.Headers;
+ responseHeaders["Connection"] = "Close";
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.True(response.Headers.ConnectionClose.Value);
+ Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_HTTP10Request_Gets11Close()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address, usehttp11: false);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.True(response.Headers.ConnectionClose.Value);
+ Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_HTTP10Request_AllowsManualChunking()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, address);
+ request.Version = new Version(1, 0);
+ Task<HttpResponseMessage> responseTask = client.SendAsync(request);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var responseHeaders = context.Response.Headers;
+ responseHeaders["Transfer-Encoding"] = "chunked";
+ var responseBytes = Encoding.ASCII.GetBytes("10\r\nManually Chunked\r\n0\r\n\r\n");
+ await context.Response.Body.WriteAsync(responseBytes, 0, responseBytes.Length);
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.True(response.Headers.TransferEncodingChunked.Value);
+ Assert.False(response.Content.Headers.Contains("Content-Length"));
+ Assert.True(response.Headers.ConnectionClose.Value);
+ Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
+ Assert.Equal("Manually Chunked", await response.Content.ReadAsStringAsync());
+ }
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_HTTP10KeepAliveRequest_Gets11Close()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ // Http.Sys does not support 1.0 keep-alives.
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address, usehttp11: false, sendKeepAlive: true);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.True(response.Headers.ConnectionClose.Value);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Headers_FlushSendsHeaders_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ server.Options.AllowSynchronousIO = true;
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var responseHeaders = context.Response.Headers;
+
+ responseHeaders["Custom1"] = new[] { "value1a", "value1b" };
+ responseHeaders["Custom2"] = "value2a, value2b";
+ var body = context.Response.Body;
+ Assert.False(context.Response.HasStarted);
+ body.Flush();
+ Assert.True(context.Response.HasStarted);
+ var ex = Assert.Throws<InvalidOperationException>(() => context.Response.StatusCode = 404);
+ Assert.Equal("Headers already sent.", ex.Message);
+ ex = Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
+ Assert.Equal("The response headers cannot be modified because the response has already started.", ex.Message);
+
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(5, response.Headers.Count()); // Date, Server, Chunked
+
+ Assert.Equal(2, response.Headers.GetValues("Custom1").Count());
+ Assert.Equal("value1a", response.Headers.GetValues("Custom1").First());
+ Assert.Equal("value1b", response.Headers.GetValues("Custom1").Skip(1).First());
+ Assert.Single(response.Headers.GetValues("Custom2"));
+ Assert.Equal("value2a, value2b", response.Headers.GetValues("Custom2").First());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Headers_FlushAsyncSendsHeaders_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var responseHeaders = context.Response.Headers;
+
+ responseHeaders["Custom1"] = new[] { "value1a", "value1b" };
+ responseHeaders["Custom2"] = "value2a, value2b";
+ var body = context.Response.Body;
+ Assert.False(context.Response.HasStarted);
+ await body.FlushAsync();
+ Assert.True(context.Response.HasStarted);
+ var ex = Assert.Throws<InvalidOperationException>(() => context.Response.StatusCode = 404);
+ Assert.Equal("Headers already sent.", ex.Message);
+ ex = Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
+ Assert.Equal("The response headers cannot be modified because the response has already started.", ex.Message);
+
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(5, response.Headers.Count()); // Date, Server, Chunked
+
+ Assert.Equal(2, response.Headers.GetValues("Custom1").Count());
+ Assert.Equal("value1a", response.Headers.GetValues("Custom1").First());
+ Assert.Equal("value1b", response.Headers.GetValues("Custom1").Skip(1).First());
+ Assert.Single(response.Headers.GetValues("Custom2"));
+ Assert.Equal("value2a, value2b", response.Headers.GetValues("Custom2").First());
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData("Server", "\r\nData")]
+ [InlineData("Server", "\0Data")]
+ [InlineData("Server", "Data\r")]
+ [InlineData("Server", "Da\0ta")]
+ [InlineData("Server", "Da\u001Fta")]
+ [InlineData("Unknown-Header", "\r\nData")]
+ [InlineData("Unknown-Header", "\0Data")]
+ [InlineData("Unknown-Header", "Data\0")]
+ [InlineData("Unknown-Header", "Da\nta")]
+ [InlineData("\r\nServer", "Data")]
+ [InlineData("Server\r", "Data")]
+ [InlineData("Ser\0ver", "Data")]
+ [InlineData("Server\r\n", "Data")]
+ [InlineData("\u001FServer", "Data")]
+ [InlineData("Unknown-Header\r\n", "Data")]
+ [InlineData("\0Unknown-Header", "Data")]
+ [InlineData("Unknown\r-Header", "Data")]
+ [InlineData("Unk\nown-Header", "Data")]
+ public async Task AddingControlCharactersToHeadersThrows(string key, string value)
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+
+ var responseHeaders = context.Response.Headers;
+
+ Assert.Throws<InvalidOperationException>(() => {
+ responseHeaders[key] = value;
+ });
+
+ Assert.Throws<InvalidOperationException>(() => {
+ responseHeaders[key] = new StringValues(new[] { "valid", value });
+ });
+
+ Assert.Throws<InvalidOperationException>(() => {
+ ((IDictionary<string, StringValues>)responseHeaders)[key] = value;
+ });
+
+ Assert.Throws<InvalidOperationException>(() => {
+ var kvp = new KeyValuePair<string, StringValues>(key, value);
+ ((ICollection<KeyValuePair<string, StringValues>>)responseHeaders).Add(kvp);
+ });
+
+ Assert.Throws<InvalidOperationException>(() => {
+ var kvp = new KeyValuePair<string, StringValues>(key, value);
+ ((IDictionary<string, StringValues>)responseHeaders).Add(key, value);
+ });
+
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ }
+ }
+
+ private async Task<HttpResponseMessage> SendRequestAsync(string uri, bool usehttp11 = true, bool sendKeepAlive = false)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, uri);
+ if (!usehttp11)
+ {
+ request.Version = new Version(1, 0);
+ }
+ if (sendKeepAlive)
+ {
+ request.Headers.Add("Connection", "Keep-Alive");
+ }
+ return await _client.SendAsync(request);
+ }
+
+ private async Task<HttpResponseMessage> SendHeadRequestAsync(string uri, bool usehttp11 = true)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Head, uri);
+ if (!usehttp11)
+ {
+ request.Version = new Version(1, 0);
+ }
+ return await _client.SendAsync(request);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseSendFileTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseSendFileTests.cs
new file mode 100644
index 0000000000..990af071e7
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseSendFileTests.cs
@@ -0,0 +1,593 @@
+// 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.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.Listener
+{
+ public class ResponseSendFileTests
+ {
+ private readonly string AbsoluteFilePath;
+ private readonly string RelativeFilePath;
+ private readonly long FileLength;
+
+ public ResponseSendFileTests()
+ {
+ AbsoluteFilePath = Directory.GetFiles(Directory.GetCurrentDirectory()).First();
+ RelativeFilePath = Path.GetFileName(AbsoluteFilePath);
+ FileLength = new FileInfo(AbsoluteFilePath).Length;
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_MissingFile_Throws()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ await Assert.ThrowsAsync<FileNotFoundException>(() =>
+ context.Response.SendFileAsync("Missing.txt", 0, null, CancellationToken.None));
+ context.Dispose();
+
+ var response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_NoHeaders_DefaultsToChunked()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
+ Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_RelativeFile_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ await context.Response.SendFileAsync(RelativeFilePath, 0, null, CancellationToken.None);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
+ Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_Unspecified_Chunked()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value);
+ Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_MultipleWrites_Chunked()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value);
+ Assert.Equal(FileLength * 2, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_HalfOfFile_Chunked()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, FileLength / 2, CancellationToken.None);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value);
+ Assert.Equal(FileLength / 2, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_OffsetOutOfRange_Throws()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
+ () => context.Response.SendFileAsync(AbsoluteFilePath, 1234567, null, CancellationToken.None));
+ context.Dispose();
+
+ var response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_CountOutOfRange_Throws()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
+ () => context.Response.SendFileAsync(AbsoluteFilePath, 0, 1234567, CancellationToken.None));
+ context.Dispose();
+
+ var response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_Count0_Chunked()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value);
+ Assert.Empty((await response.Content.ReadAsByteArrayAsync()));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_EmptyFileCountUnspecified_SetsChunkedAndFlushesHeaders()
+ {
+ var emptyFilePath = Path.Combine(Directory.GetCurrentDirectory(), "zz_" + Guid.NewGuid().ToString() + "EmptyTestFile.txt");
+ var emptyFile = File.Create(emptyFilePath, 1024);
+ emptyFile.Dispose();
+
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ await context.Response.SendFileAsync(emptyFilePath, 0, null, CancellationToken.None);
+ Assert.True(context.Response.HasStarted);
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10, CancellationToken.None);
+ context.Dispose();
+ File.Delete(emptyFilePath);
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.HasValue);
+ Assert.Equal(10, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_ContentLength_PassedThrough()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["Content-lenGth"] = FileLength.ToString();
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.Equal(FileLength.ToString(), contentLength.First());
+ Assert.Null(response.Headers.TransferEncodingChunked);
+ Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_ContentLengthSpecific_PassedThrough()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["Content-lenGth"] = "10";
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, 10, CancellationToken.None);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.Equal("10", contentLength.First());
+ Assert.Null(response.Headers.TransferEncodingChunked);
+ Assert.Equal(10, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_ContentLength0_PassedThrough()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.Headers["Content-lenGth"] = "0";
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.Equal("0", contentLength.First());
+ Assert.Null(response.Headers.TransferEncodingChunked);
+ Assert.Empty((await response.Content.ReadAsByteArrayAsync()));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_WithActiveCancellationToken_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cts = new CancellationTokenSource();
+ // First write sends headers
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(FileLength * 2, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_WithTimerCancellationToken_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cts = new CancellationTokenSource();
+ cts.CancelAfter(TimeSpan.FromSeconds(10));
+ // First write sends headers
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(FileLength * 2, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFileWriteExceptions_FirstCallWithCanceledCancellationToken_CancelsAndAborts()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ server.Options.ThrowWriteExceptions = true;
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+ // First write sends headers
+ var writeTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
+ Assert.True(writeTask.IsCanceled);
+ context.Dispose();
+#if NET461
+ // .NET HttpClient automatically retries a request if it does not get a response.
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ cts = new CancellationTokenSource();
+ cts.Cancel();
+ // First write sends headers
+ writeTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
+ Assert.True(writeTask.IsCanceled);
+ context.Dispose();
+#elif NETCOREAPP2_0 || NETCOREAPP2_1
+#else
+#error Target framework needs to be updated
+#endif
+ await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_FirstSendWithCanceledCancellationToken_CancelsAndAborts()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+ // First write sends headers
+ var writeTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
+ Assert.True(writeTask.IsCanceled);
+ context.Dispose();
+#if NET461
+ // .NET HttpClient automatically retries a request if it does not get a response.
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ cts = new CancellationTokenSource();
+ cts.Cancel();
+ // First write sends headers
+ writeTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
+ Assert.True(writeTask.IsCanceled);
+ context.Dispose();
+#elif NETCOREAPP2_0 || NETCOREAPP2_1
+#else
+#error Target framework needs to be updated
+#endif
+ await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFileExceptions_SecondSendWithCanceledCancellationToken_CancelsAndAborts()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ server.Options.ThrowWriteExceptions = true;
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cts = new CancellationTokenSource();
+ // First write sends headers
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
+ cts.Cancel();
+ var writeTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
+ Assert.True(writeTask.IsCanceled);
+ context.Dispose();
+
+ await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_SecondSendWithCanceledCancellationToken_CancelsAndAborts()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var cts = new CancellationTokenSource();
+ // First write sends headers
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
+ cts.Cancel();
+ var writeTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, cts.Token);
+ Assert.True(writeTask.IsCanceled);
+ context.Dispose();
+
+ await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFileExceptions_ClientDisconnectsBeforeFirstSend_SendThrows()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ server.Options.ThrowWriteExceptions = true;
+ var cts = new CancellationTokenSource();
+ var responseTask = SendRequestAsync(address, cts.Token);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+
+ // First write sends headers
+ cts.Cancel();
+ await Assert.ThrowsAnyAsync<OperationCanceledException>(() => responseTask);
+
+ Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
+ await Assert.ThrowsAsync<IOException>(async () =>
+ {
+ // It can take several tries before Send notices the disconnect.
+ for (int i = 0; i < Utilities.WriteRetryLimit; i++)
+ {
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+ }
+ });
+
+ await Assert.ThrowsAsync<ObjectDisposedException>(() =>
+ context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None));
+
+ context.Dispose();
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_ClientDisconnectsBeforeFirstSend_SendCompletesSilently()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var cts = new CancellationTokenSource();
+ var responseTask = SendRequestAsync(address, cts.Token);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ // First write sends headers
+ cts.Cancel();
+ await Assert.ThrowsAnyAsync<OperationCanceledException>(() => responseTask);
+ Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
+ // It can take several tries before Send notices the disconnect.
+ for (int i = 0; i < Utilities.WriteRetryLimit; i++)
+ {
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+ }
+ context.Dispose();
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFileExceptions_ClientDisconnectsBeforeSecondSend_SendThrows()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ server.Options.ThrowWriteExceptions = true;
+ RequestContext context;
+ using (var client = new HttpClient())
+ {
+ var responseTask = client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ // First write sends headers
+ var sendFileTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+
+ var response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ // Drain data from the connection so that SendFileAsync can complete.
+ var bufferTask = response.Content.LoadIntoBufferAsync();
+
+ await sendFileTask;
+ response.Dispose();
+ }
+
+ Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
+ await Assert.ThrowsAsync<IOException>(async () =>
+ {
+ // It can take several tries before Write notices the disconnect.
+ for (int i = 0; i < Utilities.WriteRetryLimit; i++)
+ {
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+ }
+ });
+ context.Dispose();
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_ClientDisconnectsBeforeSecondSend_SendCompletesSilently()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ RequestContext context;
+ using (var client = new HttpClient())
+ {
+ var responseTask = client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ // First write sends headers
+ var sendFileTask = context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+
+ var response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ // Drain data from the connection so that SendFileAsync can complete.
+ var bufferTask = response.Content.LoadIntoBufferAsync();
+
+ await sendFileTask;
+ response.Dispose();
+ }
+
+ Assert.True(context.DisconnectToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5)));
+ // It can take several tries before Write notices the disconnect.
+ for (int i = 0; i < Utilities.WriteRetryLimit; i++)
+ {
+ await context.Response.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+ }
+ context.Dispose();
+ }
+ }
+
+ private async Task<HttpResponseMessage> SendRequestAsync(string uri, CancellationToken cancellationToken = new CancellationToken())
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetAsync(uri, cancellationToken);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseTests.cs
new file mode 100644
index 0000000000..eacd671453
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ResponseTests.cs
@@ -0,0 +1,137 @@
+// 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.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.Listener
+{
+ public class ResponseTests
+ {
+ [ConditionalFact]
+ public async Task Response_ServerSendsDefaultResponse_ServerProvidesStatusCodeAndReasonPhrase()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.Equal(200, context.Response.StatusCode);
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("OK", response.ReasonPhrase);
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Response_ServerSendsSpecificStatus_ServerProvidesReasonPhrase()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.StatusCode = 201;
+ // TODO: env["owin.ResponseProtocol"] = "HTTP/1.0"; // Http.Sys ignores this value
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ Assert.Equal(201, (int)response.StatusCode);
+ Assert.Equal("Created", response.ReasonPhrase);
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Response_ServerSendsSpecificStatusAndReasonPhrase_PassedThrough()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.StatusCode = 201;
+ context.Response.ReasonPhrase = "CustomReasonPhrase";
+ // TODO: env["owin.ResponseProtocol"] = "HTTP/1.0"; // Http.Sys ignores this value
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ Assert.Equal(201, (int)response.StatusCode);
+ Assert.Equal("CustomReasonPhrase", response.ReasonPhrase);
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Response_ServerSendsCustomStatus_NoReasonPhrase()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.StatusCode = 901;
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ Assert.Equal(901, (int)response.StatusCode);
+ Assert.Equal(string.Empty, response.ReasonPhrase);
+ Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Response_100_Throws()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.Throws<ArgumentOutOfRangeException>(() => { context.Response.StatusCode = 100; });
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Response_0_Throws()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.Throws<ArgumentOutOfRangeException>(() => { context.Response.StatusCode = 0; });
+ context.Dispose();
+
+ HttpResponseMessage response = await responseTask;
+ }
+ }
+
+ private async Task<HttpResponseMessage> SendRequestAsync(string uri)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetAsync(uri);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ServerTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ServerTests.cs
new file mode 100644
index 0000000000..e70e3de1ac
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/ServerTests.cs
@@ -0,0 +1,345 @@
+// 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 System.Net;
+using System.Net.Http;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.Listener
+{
+ public class ServerTests
+ {
+ [ConditionalFact]
+ public async Task Server_200OK_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_SendHelloWorld_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ Task<string> responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Response.ContentLength = 11;
+ var writer = new StreamWriter(context.Response.Body);
+ await writer.WriteAsync("Hello World");
+ await writer.FlushAsync();
+
+ string response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_EchoHelloWorld_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address, "Hello World");
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var input = await new StreamReader(context.Request.Body).ReadToEndAsync();
+ Assert.Equal("Hello World", input);
+ context.Response.ContentLength = 11;
+ var writer = new StreamWriter(context.Response.Body);
+ await writer.WriteAsync("Hello World");
+ await writer.FlushAsync();
+
+ var response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_ClientDisconnects_CallCanceled()
+ {
+ var interval = TimeSpan.FromSeconds(1);
+ var canceled = new ManualResetEvent(false);
+
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ using (var client = new HttpClient())
+ {
+ var responseTask = client.GetAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var ct = context.DisconnectToken;
+ Assert.True(ct.CanBeCanceled, "CanBeCanceled");
+ Assert.False(ct.IsCancellationRequested, "IsCancellationRequested");
+ ct.Register(() => canceled.Set());
+
+ client.CancelPendingRequests();
+
+ Assert.True(canceled.WaitOne(interval), "canceled");
+ Assert.True(ct.IsCancellationRequested, "IsCancellationRequested");
+
+ await Assert.ThrowsAnyAsync<OperationCanceledException>(() => responseTask);
+
+ context.Dispose();
+ }
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_TokenRegisteredAfterClientDisconnects_CallCanceled()
+ {
+ var interval = TimeSpan.FromSeconds(1);
+ var canceled = new ManualResetEvent(false);
+
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ using (var client = new HttpClient())
+ {
+ var responseTask = client.GetAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+
+ client.CancelPendingRequests();
+ await Assert.ThrowsAnyAsync<OperationCanceledException>(() => responseTask);
+
+ var ct = context.DisconnectToken;
+ Assert.True(ct.CanBeCanceled, "CanBeCanceled");
+ ct.Register(() => canceled.Set());
+ Assert.True(ct.WaitHandle.WaitOne(interval));
+ Assert.True(ct.IsCancellationRequested, "IsCancellationRequested");
+
+ Assert.True(canceled.WaitOne(interval), "canceled");
+
+ context.Dispose();
+ }
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_TokenRegisteredAfterResponseSent_Success()
+ {
+ var interval = TimeSpan.FromSeconds(1);
+ var canceled = new ManualResetEvent(false);
+
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ using (var client = new HttpClient())
+ {
+ var responseTask = client.GetAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+
+ var response = await responseTask;
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
+
+ var ct = context.DisconnectToken;
+ Assert.False(ct.CanBeCanceled, "CanBeCanceled");
+ ct.Register(() => canceled.Set());
+ Assert.False(ct.WaitHandle.WaitOne(interval));
+ Assert.False(ct.IsCancellationRequested, "IsCancellationRequested");
+
+ Assert.False(canceled.WaitOne(interval), "canceled");
+ }
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_Abort_CallCanceled()
+ {
+ var interval = TimeSpan.FromSeconds(1);
+ var canceled = new ManualResetEvent(false);
+
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var ct = context.DisconnectToken;
+ Assert.True(ct.CanBeCanceled, "CanBeCanceled");
+ Assert.False(ct.IsCancellationRequested, "IsCancellationRequested");
+ ct.Register(() => canceled.Set());
+ context.Abort();
+ Assert.True(canceled.WaitOne(interval), "Aborted");
+ Assert.True(ct.IsCancellationRequested, "IsCancellationRequested");
+#if NET461
+ // HttpClient re-tries the request because it doesn't know if the request was received.
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Abort();
+#elif NETCOREAPP2_0 || NETCOREAPP2_1
+#else
+#error Target framework needs to be updated
+#endif
+ await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_ConnectionCloseHeader_CancellationTokenFires()
+ {
+ var interval = TimeSpan.FromSeconds(1);
+ var canceled = new ManualResetEvent(false);
+
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ var ct = context.DisconnectToken;
+ Assert.True(ct.CanBeCanceled, "CanBeCanceled");
+ Assert.False(ct.IsCancellationRequested, "IsCancellationRequested");
+ ct.Register(() => canceled.Set());
+
+ context.Response.Headers["Connection"] = "close";
+
+ context.Response.ContentLength = 11;
+ var writer = new StreamWriter(context.Response.Body);
+ await writer.WriteAsync("Hello World");
+ await writer.FlushAsync();
+
+ Assert.True(canceled.WaitOne(interval), "Disconnected");
+ Assert.True(ct.IsCancellationRequested, "IsCancellationRequested");
+
+ var response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_SetQueueLimit_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ server.Options.RequestQueueLimit = 1001;
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_SetRejectionVerbosityLevel_Success()
+ {
+ using (var server = Utilities.CreateHttpServer(out string address))
+ {
+ server.Options.Http503Verbosity = Http503VerbosityLevel.Limited;
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_HotAddPrefix_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.Equal(string.Empty, context.Request.PathBase);
+ Assert.Equal("/", context.Request.Path);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(string.Empty, response);
+
+ address += "pathbase/";
+ server.Options.UrlPrefixes.Add(address);
+
+ responseTask = SendRequestAsync(address);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.Equal("/pathbase", context.Request.PathBase);
+ Assert.Equal("/", context.Request.Path);
+ context.Dispose();
+
+ response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_HotRemovePrefix_Success()
+ {
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address))
+ {
+ address += "pathbase/";
+ server.Options.UrlPrefixes.Add(address);
+ var responseTask = SendRequestAsync(address);
+
+ var context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.Equal("/pathbase", context.Request.PathBase);
+ Assert.Equal("/", context.Request.Path);
+ context.Dispose();
+
+ var response = await responseTask;
+ Assert.Equal(string.Empty, response);
+
+ Assert.True(server.Options.UrlPrefixes.Remove(address));
+
+ responseTask = SendRequestAsync(address);
+
+ context = await server.AcceptAsync(Utilities.DefaultTimeout);
+ Assert.Equal(string.Empty, context.Request.PathBase);
+ Assert.Equal("/pathbase/", context.Request.Path);
+ context.Dispose();
+
+ response = await responseTask;
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ private async Task<string> SendRequestAsync(string uri)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetStringAsync(uri);
+ }
+ }
+
+ private async Task<string> SendRequestAsync(string uri, string upload)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ HttpResponseMessage response = await client.PostAsync(uri, new StringContent(upload));
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadAsStringAsync();
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/Utilities.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/Utilities.cs
new file mode 100644
index 0000000000..b3fb1b1c8c
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Listener/Utilities.cs
@@ -0,0 +1,112 @@
+// 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.Threading.Tasks;
+using Microsoft.Extensions.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.Listener
+{
+ internal static class Utilities
+ {
+ internal static readonly int WriteRetryLimit = 1000;
+
+ // When tests projects are run in parallel, overlapping port ranges can cause a race condition when looking for free
+ // ports during dynamic port allocation.
+ private const int BasePort = 8001;
+ private const int MaxPort = 11000;
+ private static int NextPort = BasePort;
+ private static object PortLock = new object();
+
+ internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15);
+ // Minimum support for Windows 7 is assumed.
+ internal static readonly bool IsWin8orLater;
+
+ static Utilities()
+ {
+ var win8Version = new Version(6, 2);
+ IsWin8orLater = (Environment.OSVersion.Version >= win8Version);
+ }
+
+ internal static HttpSysListener CreateHttpAuthServer(AuthenticationSchemes authScheme, bool allowAnonymos, out string baseAddress)
+ {
+ var listener = CreateHttpServer(out baseAddress);
+ listener.Options.Authentication.Schemes = authScheme;
+ listener.Options.Authentication.AllowAnonymous = allowAnonymos;
+ return listener;
+ }
+
+ internal static HttpSysListener CreateHttpServer(out string baseAddress)
+ {
+ string root;
+ return CreateDynamicHttpServer(string.Empty, out root, out baseAddress);
+ }
+
+ internal static HttpSysListener CreateHttpServerReturnRoot(string path, out string root)
+ {
+ string baseAddress;
+ return CreateDynamicHttpServer(path, out root, out baseAddress);
+ }
+
+ internal static HttpSysListener CreateDynamicHttpServer(string basePath, out string root, out string baseAddress)
+ {
+ lock (PortLock)
+ {
+ while (NextPort < MaxPort)
+ {
+ var port = NextPort++;
+ var prefix = UrlPrefix.Create("http", "localhost", port, basePath);
+ root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
+ baseAddress = prefix.ToString();
+ var listener = new HttpSysListener(new HttpSysOptions(), new LoggerFactory());
+ listener.Options.UrlPrefixes.Add(prefix);
+ try
+ {
+ listener.Start();
+ return listener;
+ }
+ catch (HttpSysException)
+ {
+ listener.Dispose();
+ }
+ }
+ NextPort = BasePort;
+ }
+ throw new Exception("Failed to locate a free port.");
+ }
+
+ internal static HttpSysListener CreateHttpsServer()
+ {
+ return CreateServer("https", "localhost", 9090, string.Empty);
+ }
+
+ internal static HttpSysListener CreateServer(string scheme, string host, int port, string path)
+ {
+ var listener = new HttpSysListener(new HttpSysOptions(), new LoggerFactory());
+ listener.Options.UrlPrefixes.Add(UrlPrefix.Create(scheme, host, port, path));
+ listener.Start();
+ return listener;
+ }
+
+ /// <summary>
+ /// AcceptAsync extension with timeout. This extension should be used in all tests to prevent
+ /// unexpected hangs when a request does not arrive.
+ /// </summary>
+ internal static async Task<RequestContext> AcceptAsync(this HttpSysListener server, TimeSpan timeout)
+ {
+ var acceptTask = server.AcceptAsync();
+ var completedTask = await Task.WhenAny(acceptTask, Task.Delay(timeout));
+
+ if (completedTask == acceptTask)
+ {
+ return await acceptTask;
+ }
+ else
+ {
+ server.Dispose();
+ throw new TimeoutException("AcceptAsync has timed out.");
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/MessagePumpTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/MessagePumpTests.cs
new file mode 100644
index 0000000000..5fc93e69e7
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/MessagePumpTests.cs
@@ -0,0 +1,122 @@
+// 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.Threading;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class MessagePumpTests
+ {
+ [ConditionalFact]
+ public void OverridingDirectConfigurationWithIServerAddressesFeatureSucceeds()
+ {
+ var serverAddress = "http://localhost:11001/";
+ var overrideAddress = "http://localhost:11002/";
+
+ using (var server = Utilities.CreatePump())
+ {
+ var serverAddressesFeature = server.Features.Get<IServerAddressesFeature>();
+ serverAddressesFeature.Addresses.Add(overrideAddress);
+ serverAddressesFeature.PreferHostingUrls = true;
+ server.Listener.Options.UrlPrefixes.Add(serverAddress);
+
+ server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
+
+ Assert.Equal(overrideAddress, serverAddressesFeature.Addresses.Single());
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData("http://localhost:11001/")]
+ [InlineData("invalid address")]
+ [InlineData("")]
+ [InlineData(null)]
+ public void DoesNotOverrideDirectConfigurationWithIServerAddressesFeature_IfPreferHostinUrlsFalse(string overrideAddress)
+ {
+ var serverAddress = "http://localhost:11002/";
+
+ using (var server = Utilities.CreatePump())
+ {
+ var serverAddressesFeature = server.Features.Get<IServerAddressesFeature>();
+ serverAddressesFeature.Addresses.Add(overrideAddress);
+ server.Listener.Options.UrlPrefixes.Add(serverAddress);
+
+ server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
+
+ Assert.Equal(serverAddress, serverAddressesFeature.Addresses.Single());
+ }
+ }
+
+ [ConditionalFact]
+ public void DoesNotOverrideDirectConfigurationWithIServerAddressesFeature_IfAddressesIsEmpty()
+ {
+ var serverAddress = "http://localhost:11002/";
+
+ using (var server = Utilities.CreatePump())
+ {
+ var serverAddressesFeature = server.Features.Get<IServerAddressesFeature>();
+ serverAddressesFeature.PreferHostingUrls = true;
+ server.Listener.Options.UrlPrefixes.Add(serverAddress);
+
+ server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
+
+ Assert.Equal(serverAddress, serverAddressesFeature.Addresses.Single());
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData("http://localhost:11001/")]
+ [InlineData("invalid address")]
+ [InlineData("")]
+ [InlineData(null)]
+ public void OverridingIServerAdressesFeatureWithDirectConfiguration_WarnsOnStart(string serverAddress)
+ {
+ var overrideAddress = "http://localhost:11002/";
+
+ using (var server = Utilities.CreatePump())
+ {
+ var serverAddressesFeature = server.Features.Get<IServerAddressesFeature>();
+ serverAddressesFeature.Addresses.Add(serverAddress);
+ server.Listener.Options.UrlPrefixes.Add(overrideAddress);
+
+ server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
+
+ Assert.Equal(overrideAddress, serverAddressesFeature.Addresses.Single());
+ }
+ }
+
+ [ConditionalFact]
+ public void UseIServerAdressesFeature_WhenNoDirectConfiguration()
+ {
+ var serverAddress = "http://localhost:11001/";
+
+ using (var server = Utilities.CreatePump())
+ {
+ var serverAddressesFeature = server.Features.Get<IServerAddressesFeature>();
+ serverAddressesFeature.Addresses.Add(serverAddress);
+
+ server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
+ }
+ }
+
+ [ConditionalFact]
+ public void UseDefaultAddress_WhenNoServerAddressAndNoDirectConfiguration()
+ {
+ using (var server = Utilities.CreatePump())
+ {
+ server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
+
+ Assert.Equal(Constants.DefaultServerAddress, server.Features.Get<IServerAddressesFeature>().Addresses.Single());
+ }
+ }
+
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj
new file mode 100644
index 0000000000..dd4dc97dbc
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Server.HttpSys\Microsoft.AspNetCore.Server.HttpSys.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="System.Net.Http.WinHttpHandler" Version="$(SystemNetHttpWinHttpHandlerPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/OSDontSkipConditionAttribute.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/OSDontSkipConditionAttribute.cs
new file mode 100644
index 0000000000..c657240a3e
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/OSDontSkipConditionAttribute.cs
@@ -0,0 +1,99 @@
+// 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.Runtime.InteropServices;
+
+namespace Microsoft.AspNetCore.Testing.xunit
+{
+ // Skip except on a specific OS and version
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)]
+ public class OSDontSkipConditionAttribute : Attribute, ITestCondition
+ {
+ private readonly OperatingSystems _includedOperatingSystem;
+ private readonly IEnumerable<string> _includedVersions;
+ private readonly OperatingSystems _osPlatform;
+ private readonly string _osVersion;
+
+ public OSDontSkipConditionAttribute(OperatingSystems operatingSystem, params string[] versions) :
+ this(
+ operatingSystem,
+ GetCurrentOS(),
+ GetCurrentOSVersion(),
+ versions)
+ {
+ }
+
+ // to enable unit testing
+ internal OSDontSkipConditionAttribute(
+ OperatingSystems operatingSystem, OperatingSystems osPlatform, string osVersion, params string[] versions)
+ {
+ _includedOperatingSystem = operatingSystem;
+ _includedVersions = versions ?? Enumerable.Empty<string>();
+ _osPlatform = osPlatform;
+ _osVersion = osVersion;
+ }
+
+ public bool IsMet
+ {
+ get
+ {
+ var currentOSInfo = new OSInfo()
+ {
+ OperatingSystem = _osPlatform,
+ Version = _osVersion,
+ };
+
+ var skip = (_includedOperatingSystem & currentOSInfo.OperatingSystem) != currentOSInfo.OperatingSystem;
+ if (!skip && _includedVersions.Any())
+ {
+ skip = !_includedVersions.Any(inc => _osVersion.StartsWith(inc, StringComparison.OrdinalIgnoreCase));
+ }
+
+ // Since a test would be excuted only if 'IsMet' is true, return false if we want to skip
+ return !skip;
+ }
+ }
+
+ public string SkipReason { get; set; } = "Test cannot run on this operating system.";
+
+ static private OperatingSystems GetCurrentOS()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return OperatingSystems.Windows;
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ return OperatingSystems.Linux;
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ return OperatingSystems.MacOSX;
+ }
+ throw new PlatformNotSupportedException();
+ }
+
+ static private string GetCurrentOSVersion()
+ {
+ // currently not used on other OS's
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return Environment.OSVersion.Version.ToString();
+ }
+ else
+ {
+ return string.Empty;
+ }
+ }
+
+ private class OSInfo
+ {
+ public OperatingSystems OperatingSystem { get; set; }
+
+ public string Version { get; set; }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/OpaqueUpgradeTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/OpaqueUpgradeTests.cs
new file mode 100644
index 0000000000..609bc59fdd
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/OpaqueUpgradeTests.cs
@@ -0,0 +1,367 @@
+// 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 System.Net.Http;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class OpaqueUpgradeTests
+ {
+ [ConditionalFact]
+ [OSDontSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
+ public async Task OpaqueUpgrade_DownLevel_FeatureIsAbsent()
+ {
+ using (Utilities.CreateHttpServer(out var address, httpContext =>
+ {
+ try
+ {
+ var opaqueFeature = httpContext.Features.Get<IHttpUpgradeFeature>();
+ Assert.Null(opaqueFeature);
+ }
+ catch (Exception ex)
+ {
+ return httpContext.Response.WriteAsync(ex.ToString());
+ }
+ return Task.FromResult(0);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
+ Assert.Equal(0, response.Content.Headers.ContentLength);
+ Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
+ public async Task OpaqueUpgrade_SupportKeys_Present()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ try
+ {
+ var opaqueFeature = httpContext.Features.Get<IHttpUpgradeFeature>();
+ Assert.NotNull(opaqueFeature);
+ }
+ catch (Exception ex)
+ {
+ return httpContext.Response.WriteAsync(ex.ToString());
+ }
+ return Task.FromResult(0);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
+ Assert.Equal(0, response.Content.Headers.ContentLength);
+ Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
+ public async Task OpaqueUpgrade_AfterHeadersSent_Throws()
+ {
+ bool? upgradeThrew = null;
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ await httpContext.Response.WriteAsync("Hello World");
+ await httpContext.Response.Body.FlushAsync();
+ try
+ {
+ var opaqueFeature = httpContext.Features.Get<IHttpUpgradeFeature>();
+ Assert.NotNull(opaqueFeature);
+ await opaqueFeature.UpgradeAsync();
+ upgradeThrew = false;
+ }
+ catch (InvalidOperationException)
+ {
+ upgradeThrew = true;
+ }
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
+ Assert.True(upgradeThrew.Value);
+ }
+ }
+
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
+ public async Task OpaqueUpgrade_GetUpgrade_Success()
+ {
+ ManualResetEvent waitHandle = new ManualResetEvent(false);
+ bool? upgraded = null;
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ httpContext.Response.Headers["Upgrade"] = "websocket"; // Win8.1 blocks anything but WebSockets
+ var opaqueFeature = httpContext.Features.Get<IHttpUpgradeFeature>();
+ Assert.NotNull(opaqueFeature);
+ Assert.True(opaqueFeature.IsUpgradableRequest);
+ await opaqueFeature.UpgradeAsync();
+ upgraded = true;
+ waitHandle.Set();
+ }))
+ {
+ using (Stream stream = await SendOpaqueRequestAsync("GET", address))
+ {
+ Assert.True(waitHandle.WaitOne(TimeSpan.FromSeconds(1)), "Timed out");
+ Assert.True(upgraded.HasValue, "Upgraded not set");
+ Assert.True(upgraded.Value, "Upgrade failed");
+ }
+ }
+ }
+
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
+ public async Task OpaqueUpgrade_GetUpgrade_NotAffectedByMaxRequestBodyLimit()
+ {
+ ManualResetEvent waitHandle = new ManualResetEvent(false);
+ bool? upgraded = null;
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, async httpContext =>
+ {
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Null(feature.MaxRequestBodySize); // GET/Upgrade requests don't actually have an entity body, so they can't set the limit.
+
+ httpContext.Response.Headers["Upgrade"] = "websocket"; // Win8.1 blocks anything but WebSockets
+ var opaqueFeature = httpContext.Features.Get<IHttpUpgradeFeature>();
+ Assert.NotNull(opaqueFeature);
+ Assert.True(opaqueFeature.IsUpgradableRequest);
+ var stream = await opaqueFeature.UpgradeAsync();
+ Assert.True(feature.IsReadOnly);
+ Assert.Null(feature.MaxRequestBodySize);
+ Assert.Throws<InvalidOperationException>(() => feature.MaxRequestBodySize = 12);
+ Assert.Equal(15, await stream.ReadAsync(new byte[15], 0, 15));
+ upgraded = true;
+ waitHandle.Set();
+ }))
+ {
+ using (Stream stream = await SendOpaqueRequestAsync("GET", address))
+ {
+ stream.Write(new byte[15], 0, 15);
+ Assert.True(waitHandle.WaitOne(TimeSpan.FromSeconds(10)), "Timed out");
+ Assert.True(upgraded.HasValue, "Upgraded not set");
+ Assert.True(upgraded.Value, "Upgrade failed");
+ }
+ }
+ }
+
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
+ public async Task OpaqueUpgrade_WithOnStarting_CallbackCalled()
+ {
+ var callbackCalled = false;
+ var waitHandle = new ManualResetEvent(false);
+ bool? upgraded = null;
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ httpContext.Response.OnStarting(_ =>
+ {
+ callbackCalled = true;
+ return Task.FromResult(0);
+ }, null);
+ httpContext.Response.Headers["Upgrade"] = "websocket"; // Win8.1 blocks anything but WebSockets
+ var opaqueFeature = httpContext.Features.Get<IHttpUpgradeFeature>();
+ Assert.NotNull(opaqueFeature);
+ Assert.True(opaqueFeature.IsUpgradableRequest);
+ await opaqueFeature.UpgradeAsync();
+ upgraded = true;
+ waitHandle.Set();
+ }))
+ {
+ using (Stream stream = await SendOpaqueRequestAsync("GET", address))
+ {
+ Assert.True(waitHandle.WaitOne(TimeSpan.FromSeconds(1)), "Timed out");
+ Assert.True(upgraded.HasValue, "Upgraded not set");
+ Assert.True(upgraded.Value, "Upgrade failed");
+ Assert.True(callbackCalled, "Callback not called");
+ }
+ }
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
+ // See HTTP_VERB for known verbs
+ [InlineData("UNKNOWN", null)]
+ [InlineData("INVALID", null)]
+ [InlineData("OPTIONS", null)]
+ [InlineData("GET", null)]
+ [InlineData("HEAD", null)]
+ [InlineData("DELETE", null)]
+ [InlineData("TRACE", null)]
+ [InlineData("CONNECT", null)]
+ [InlineData("TRACK", null)]
+ [InlineData("MOVE", null)]
+ [InlineData("COPY", null)]
+ [InlineData("PROPFIND", null)]
+ [InlineData("PROPPATCH", null)]
+ [InlineData("MKCOL", null)]
+ [InlineData("LOCK", null)]
+ [InlineData("UNLOCK", null)]
+ [InlineData("SEARCH", null)]
+ [InlineData("CUSTOMVERB", null)]
+ [InlineData("PATCH", null)]
+ [InlineData("POST", "Content-Length: 0")]
+ [InlineData("PUT", "Content-Length: 0")]
+ public async Task OpaqueUpgrade_VariousMethodsUpgradeSendAndReceive_Success(string method, string extraHeader)
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ try
+ {
+ httpContext.Response.Headers["Upgrade"] = "websocket"; // Win8.1 blocks anything but WebSockets
+ var opaqueFeature = httpContext.Features.Get<IHttpUpgradeFeature>();
+ Assert.NotNull(opaqueFeature);
+ Assert.True(opaqueFeature.IsUpgradableRequest);
+ var opaqueStream = await opaqueFeature.UpgradeAsync();
+
+ byte[] buffer = new byte[100];
+ int read = await opaqueStream.ReadAsync(buffer, 0, buffer.Length);
+
+ await opaqueStream.WriteAsync(buffer, 0, read);
+ }
+ catch (Exception ex)
+ {
+ await httpContext.Response.WriteAsync(ex.ToString());
+ }
+ }))
+ {
+ using (Stream stream = await SendOpaqueRequestAsync(method, address, extraHeader))
+ {
+ byte[] data = new byte[100];
+ await stream.WriteAsync(data, 0, 49);
+ int read = await stream.ReadAsync(data, 0, data.Length);
+ Assert.Equal(49, read);
+ }
+ }
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win2008R2)]
+ // Http.Sys returns a 411 Length Required if PUT or POST does not specify content-length or chunked.
+ [InlineData("POST", "Content-Length: 10")]
+ [InlineData("POST", "Transfer-Encoding: chunked")]
+ [InlineData("PUT", "Content-Length: 10")]
+ [InlineData("PUT", "Transfer-Encoding: chunked")]
+ [InlineData("CUSTOMVERB", "Content-Length: 10")]
+ [InlineData("CUSTOMVERB", "Transfer-Encoding: chunked")]
+ public async Task OpaqueUpgrade_InvalidMethodUpgrade_Disconnected(string method, string extraHeader)
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ try
+ {
+ var opaqueFeature = httpContext.Features.Get<IHttpUpgradeFeature>();
+ Assert.NotNull(opaqueFeature);
+ Assert.False(opaqueFeature.IsUpgradableRequest);
+ }
+ catch (Exception ex)
+ {
+ await httpContext.Response.WriteAsync(ex.ToString());
+ }
+ }))
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(async () => await SendOpaqueRequestAsync(method, address, extraHeader));
+ }
+ }
+
+ private async Task<HttpResponseMessage> SendRequestAsync(string uri)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetAsync(uri);
+ }
+ }
+
+ // Returns a bidirectional opaque stream or throws if the upgrade fails
+ private async Task<Stream> SendOpaqueRequestAsync(string method, string address, string extraHeader = null)
+ {
+ // Connect with a socket
+ Uri uri = new Uri(address);
+ TcpClient client = new TcpClient();
+
+ try
+ {
+ await client.ConnectAsync(uri.Host, uri.Port);
+ NetworkStream stream = client.GetStream();
+
+ // Send an HTTP GET request
+ byte[] requestBytes = BuildGetRequest(method, uri, extraHeader);
+ await stream.WriteAsync(requestBytes, 0, requestBytes.Length);
+
+ // Read the response headers, fail if it's not a 101
+ await ParseResponseAsync(stream);
+
+ // Return the opaque network stream
+ return stream;
+ }
+ catch (Exception)
+ {
+ ((IDisposable)client).Dispose();
+ throw;
+ }
+ }
+
+ private byte[] BuildGetRequest(string method, Uri uri, string extraHeader)
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.Append(method);
+ builder.Append(" ");
+ builder.Append(uri.PathAndQuery);
+ builder.Append(" HTTP/1.1");
+ builder.AppendLine();
+
+ builder.Append("Host: ");
+ builder.Append(uri.Host);
+ builder.Append(':');
+ builder.Append(uri.Port);
+ builder.AppendLine();
+
+ if (!string.IsNullOrEmpty(extraHeader))
+ {
+ builder.AppendLine(extraHeader);
+ }
+
+ builder.AppendLine();
+ return Encoding.ASCII.GetBytes(builder.ToString());
+ }
+
+ // Read the response headers, fail if it's not a 101
+ private async Task ParseResponseAsync(NetworkStream stream)
+ {
+ StreamReader reader = new StreamReader(stream);
+ string statusLine = await reader.ReadLineAsync();
+ string[] parts = statusLine.Split(' ');
+ if (int.Parse(parts[1]) != 101)
+ {
+ throw new InvalidOperationException("The response status code was incorrect: " + statusLine);
+ }
+
+ // Scan to the end of the headers
+ while (!string.IsNullOrEmpty(reader.ReadLine()))
+ {
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Properties/AssemblyInfo.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..0d585d3063
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Properties/AssemblyInfo.cs
@@ -0,0 +1,9 @@
+// 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.Testing.xunit;
+using Xunit;
+
+[assembly: OSSkipCondition(OperatingSystems.MacOSX)]
+[assembly: OSSkipCondition(OperatingSystems.Linux)]
+[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestBodyLimitTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestBodyLimitTests.cs
new file mode 100644
index 0000000000..ae203ceab1
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestBodyLimitTests.cs
@@ -0,0 +1,428 @@
+// 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 System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class RequestBodyLimitTests
+ {
+ [ConditionalFact]
+ public async Task ContentLengthEqualsLimit_ReadSync_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, httpContext =>
+ {
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Equal(11, httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ int read = httpContext.Request.Body.Read(input, 0, input.Length);
+ httpContext.Response.ContentLength = read;
+ httpContext.Response.Body.Write(input, 0, read);
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World");
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ContentLengthEqualsLimit_ReadAsync_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, async httpContext =>
+ {
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Equal(11, httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
+ httpContext.Response.ContentLength = read;
+ await httpContext.Response.Body.WriteAsync(input, 0, read);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World");
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ContentLengthEqualsLimit_ReadBeginEnd_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, httpContext =>
+ {
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Equal(11, httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ int read = httpContext.Request.Body.EndRead(httpContext.Request.Body.BeginRead(input, 0, input.Length, null, null));
+ httpContext.Response.ContentLength = read;
+ httpContext.Response.Body.EndWrite(httpContext.Response.Body.BeginWrite(input, 0, read, null, null));
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World");
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ChunkedEqualsLimit_ReadSync_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, httpContext =>
+ {
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Null(httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ int read = httpContext.Request.Body.Read(input, 0, input.Length);
+ httpContext.Response.ContentLength = read;
+ httpContext.Response.Body.Write(input, 0, read);
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World", chunked: true);
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ChunkedEqualsLimit_ReadAsync_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, async httpContext =>
+ {
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Null(httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
+ httpContext.Response.ContentLength = read;
+ await httpContext.Response.Body.WriteAsync(input, 0, read);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World", chunked: true);
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ChunkedEqualsLimit_ReadBeginEnd_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, httpContext =>
+ {
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Null(httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ int read = httpContext.Request.Body.EndRead(httpContext.Request.Body.BeginRead(input, 0, input.Length, null, null));
+ httpContext.Response.ContentLength = read;
+ httpContext.Response.Body.EndWrite(httpContext.Response.Body.BeginWrite(input, 0, read, null, null));
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World", chunked: true);
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ContentLengthExceedsLimit_ReadSync_ThrowsImmediately()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, httpContext =>
+ {
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Equal(11, httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ var ex = Assert.Throws<IOException>(() => httpContext.Request.Body.Read(input, 0, input.Length));
+ Assert.Equal("The request's Content-Length 11 is larger than the request body size limit 10.", ex.Message);
+ ex = Assert.Throws<IOException>(() => httpContext.Request.Body.Read(input, 0, input.Length));
+ Assert.Equal("The request's Content-Length 11 is larger than the request body size limit 10.", ex.Message);
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World");
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ContentLengthExceedsLimit_ReadAsync_ThrowsImmediately()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, httpContext =>
+ {
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Equal(11, httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ var ex = Assert.Throws<IOException>(() => { var t = httpContext.Request.Body.ReadAsync(input, 0, input.Length); });
+ Assert.Equal("The request's Content-Length 11 is larger than the request body size limit 10.", ex.Message);
+ ex = Assert.Throws<IOException>(() => { var t = httpContext.Request.Body.ReadAsync(input, 0, input.Length); });
+ Assert.Equal("The request's Content-Length 11 is larger than the request body size limit 10.", ex.Message);
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World");
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ContentLengthExceedsLimit_ReadBeginEnd_ThrowsImmediately()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, httpContext =>
+ {
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Equal(11, httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ var ex = Assert.Throws<IOException>(() => httpContext.Request.Body.BeginRead(input, 0, input.Length, null, null));
+ Assert.Equal("The request's Content-Length 11 is larger than the request body size limit 10.", ex.Message);
+ ex = Assert.Throws<IOException>(() => httpContext.Request.Body.BeginRead(input, 0, input.Length, null, null));
+ Assert.Equal("The request's Content-Length 11 is larger than the request body size limit 10.", ex.Message);
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World");
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ChunkedExceedsLimit_ReadSync_ThrowsAtLimit()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, httpContext =>
+ {
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Null(httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ var ex = Assert.Throws<IOException>(() => httpContext.Request.Body.Read(input, 0, input.Length));
+ Assert.Equal("The total number of bytes read 11 has exceeded the request body size limit 10.", ex.Message);
+ ex = Assert.Throws<IOException>(() => httpContext.Request.Body.Read(input, 0, input.Length));
+ Assert.Equal("The total number of bytes read 11 has exceeded the request body size limit 10.", ex.Message);
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World", chunked: true);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ChunkedExceedsLimit_ReadAsync_ThrowsAtLimit()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, async httpContext =>
+ {
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Null(httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ var ex = await Assert.ThrowsAsync<IOException>(() => httpContext.Request.Body.ReadAsync(input, 0, input.Length));
+ Assert.Equal("The total number of bytes read 11 has exceeded the request body size limit 10.", ex.Message);
+ ex = await Assert.ThrowsAsync<IOException>(() => httpContext.Request.Body.ReadAsync(input, 0, input.Length));
+ Assert.Equal("The total number of bytes read 11 has exceeded the request body size limit 10.", ex.Message);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World", chunked: true);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ChunkedExceedsLimit_ReadBeginEnd_ThrowsAtLimit()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, httpContext =>
+ {
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Null(httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ var body = httpContext.Request.Body;
+ var ex = Assert.Throws<IOException>(() => body.EndRead(body.BeginRead(input, 0, input.Length, null, null)));
+ Assert.Equal("The total number of bytes read 11 has exceeded the request body size limit 10.", ex.Message);
+ ex = Assert.Throws<IOException>(() => body.EndRead(body.BeginRead(input, 0, input.Length, null, null)));
+ Assert.Equal("The total number of bytes read 11 has exceeded the request body size limit 10.", ex.Message);
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World", chunked: true);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Chunked_ReadSyncPartialBodyUnderLimit_ThrowsAfterLimit()
+ {
+ var content = new StaggardContent();
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, httpContext =>
+ {
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Null(httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ int read = httpContext.Request.Body.Read(input, 0, input.Length);
+ Assert.Equal(10, read);
+ content.Block.Release();
+ var ex = Assert.Throws<IOException>(() => httpContext.Request.Body.Read(input, 0, input.Length));
+ Assert.Equal("The total number of bytes read 20 has exceeded the request body size limit 10.", ex.Message);
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(address, content, chunked: true);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Chunked_ReadAsyncPartialBodyUnderLimit_ThrowsAfterLimit()
+ {
+ var content = new StaggardContent();
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 10, async httpContext =>
+ {
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Null(httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
+ Assert.Equal(10, read);
+ content.Block.Release();
+ var ex = await Assert.ThrowsAsync<IOException>(() => httpContext.Request.Body.ReadAsync(input, 0, input.Length));
+ Assert.Equal("The total number of bytes read 20 has exceeded the request body size limit 10.", ex.Message);
+ }))
+ {
+ string response = await SendRequestAsync(address, content, chunked: true);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task AdjustLimitPerRequest_ContentLength_ReadAsync_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, async httpContext =>
+ {
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Equal(11, feature.MaxRequestBodySize);
+ feature.MaxRequestBodySize = 12;
+ Assert.Equal(12, httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
+ Assert.True(feature.IsReadOnly);
+ httpContext.Response.ContentLength = read;
+ await httpContext.Response.Body.WriteAsync(input, 0, read);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World!");
+ Assert.Equal("Hello World!", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task AdjustLimitPerRequest_Chunked_ReadAsync_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, options => options.MaxRequestBodySize = 11, async httpContext =>
+ {
+ var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
+ Assert.NotNull(feature);
+ Assert.False(feature.IsReadOnly);
+ Assert.Equal(11, feature.MaxRequestBodySize);
+ feature.MaxRequestBodySize = 12;
+ Assert.Null(httpContext.Request.ContentLength);
+ byte[] input = new byte[100];
+ int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
+ Assert.True(feature.IsReadOnly);
+ httpContext.Response.ContentLength = read;
+ await httpContext.Response.Body.WriteAsync(input, 0, read);
+ }))
+ {
+ var response = await SendRequestAsync(address, "Hello World!", chunked: true);
+ Assert.Equal("Hello World!", response);
+ }
+ }
+
+ private Task<string> SendRequestAsync(string uri, string upload, bool chunked = false)
+ {
+ return SendRequestAsync(uri, new StringContent(upload), chunked);
+ }
+
+ private async Task<string> SendRequestAsync(string uri, HttpContent content, bool chunked = false)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ client.DefaultRequestHeaders.TransferEncodingChunked = chunked;
+ HttpResponseMessage response = await client.PostAsync(uri, content);
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadAsStringAsync();
+ }
+ }
+
+ private class StaggardContent : HttpContent
+ {
+ public StaggardContent()
+ {
+ Block = new SemaphoreSlim(0, 1);
+ }
+
+ public SemaphoreSlim Block { get; private set; }
+
+ protected async override Task SerializeToStreamAsync(Stream stream, TransportContext context)
+ {
+ await stream.WriteAsync(new byte[10], 0, 10);
+ await stream.FlushAsync();
+ Assert.True(await Block.WaitAsync(TimeSpan.FromSeconds(10)));
+ await stream.WriteAsync(new byte[10], 0, 10);
+ }
+
+ protected override bool TryComputeLength(out long length)
+ {
+ length = 10;
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestBodyTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestBodyTests.cs
new file mode 100644
index 0000000000..b7a18867d9
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestBodyTests.cs
@@ -0,0 +1,247 @@
+// 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 System.Net;
+using System.Net.Http;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class RequestBodyTests
+ {
+ [ConditionalFact]
+ public async Task RequestBody_ReadSync_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ byte[] input = new byte[100];
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ int read = httpContext.Request.Body.Read(input, 0, input.Length);
+ httpContext.Response.ContentLength = read;
+ httpContext.Response.Body.Write(input, 0, read);
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(address, "Hello World");
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadAsync_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ byte[] input = new byte[100];
+ int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
+ httpContext.Response.ContentLength = read;
+ await httpContext.Response.Body.WriteAsync(input, 0, read);
+ }))
+ {
+ string response = await SendRequestAsync(address, "Hello World");
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadBeginEnd_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ byte[] input = new byte[100];
+ int read = httpContext.Request.Body.EndRead(httpContext.Request.Body.BeginRead(input, 0, input.Length, null, null));
+ httpContext.Response.ContentLength = read;
+ httpContext.Response.Body.EndWrite(httpContext.Response.Body.BeginWrite(input, 0, read, null, null));
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(address, "Hello World");
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_InvalidBuffer_ArgumentException()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ byte[] input = new byte[100];
+ Assert.Throws<ArgumentNullException>("buffer", () => httpContext.Request.Body.Read(null, 0, 1));
+ Assert.Throws<ArgumentOutOfRangeException>("offset", () => httpContext.Request.Body.Read(input, -1, 1));
+ Assert.Throws<ArgumentOutOfRangeException>("offset", () => httpContext.Request.Body.Read(input, input.Length + 1, 1));
+ Assert.Throws<ArgumentOutOfRangeException>("size", () => httpContext.Request.Body.Read(input, 10, -1));
+ Assert.Throws<ArgumentOutOfRangeException>("size", () => httpContext.Request.Body.Read(input, 0, 0));
+ Assert.Throws<ArgumentOutOfRangeException>("size", () => httpContext.Request.Body.Read(input, 1, input.Length));
+ Assert.Throws<ArgumentOutOfRangeException>("size", () => httpContext.Request.Body.Read(input, 0, input.Length + 1));
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(address, "Hello World");
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadSyncPartialBody_Success()
+ {
+ StaggardContent content = new StaggardContent();
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ byte[] input = new byte[10];
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ int read = httpContext.Request.Body.Read(input, 0, input.Length);
+ Assert.Equal(5, read);
+ content.Block.Release();
+ read = httpContext.Request.Body.Read(input, 0, input.Length);
+ Assert.Equal(5, read);
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(address, content);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_ReadAsyncPartialBody_Success()
+ {
+ StaggardContent content = new StaggardContent();
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ byte[] input = new byte[10];
+ int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
+ Assert.Equal(5, read);
+ content.Block.Release();
+ read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
+ Assert.Equal(5, read);
+ }))
+ {
+ string response = await SendRequestAsync(address, content);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestBody_PostWithImidateBody_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ byte[] input = new byte[11];
+ int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
+ Assert.Equal(10, read);
+ read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
+ Assert.Equal(0, read);
+ httpContext.Response.ContentLength = 10;
+ await httpContext.Response.Body.WriteAsync(input, 0, 10);
+ }))
+ {
+ string response = await SendSocketRequestAsync(address);
+ string[] lines = response.Split('\r', '\n');
+ Assert.Equal(13, lines.Length);
+ Assert.Equal("HTTP/1.1 200 OK", lines[0]);
+ Assert.Equal("0123456789", lines[12]);
+ }
+ }
+
+ private Task<string> SendRequestAsync(string uri, string upload)
+ {
+ return SendRequestAsync(uri, new StringContent(upload));
+ }
+
+ private async Task<string> SendRequestAsync(string uri, HttpContent content)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ HttpResponseMessage response = await client.PostAsync(uri, content);
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadAsStringAsync();
+ }
+ }
+
+ private async Task<string> SendSocketRequestAsync(string address)
+ {
+ // Connect with a socket
+ Uri uri = new Uri(address);
+ TcpClient client = new TcpClient();
+
+ try
+ {
+ await client.ConnectAsync(uri.Host, uri.Port);
+ NetworkStream stream = client.GetStream();
+
+ // Send an HTTP GET request
+ byte[] requestBytes = BuildPostRequest(uri);
+ await stream.WriteAsync(requestBytes, 0, requestBytes.Length);
+ StreamReader reader = new StreamReader(stream);
+ return await reader.ReadToEndAsync();
+ }
+ catch (Exception)
+ {
+ ((IDisposable)client).Dispose();
+ throw;
+ }
+ }
+
+ private byte[] BuildPostRequest(Uri uri)
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.Append("POST");
+ builder.Append(" ");
+ builder.Append(uri.PathAndQuery);
+ builder.Append(" HTTP/1.1");
+ builder.AppendLine();
+
+ builder.Append("Host: ");
+ builder.Append(uri.Host);
+ builder.Append(':');
+ builder.Append(uri.Port);
+ builder.AppendLine();
+
+ builder.AppendLine("Connection: close");
+ builder.AppendLine("Content-Length: 10");
+ builder.AppendLine();
+ builder.Append("0123456789");
+ return Encoding.ASCII.GetBytes(builder.ToString());
+ }
+
+ private class StaggardContent : HttpContent
+ {
+ public StaggardContent()
+ {
+ Block = new SemaphoreSlim(0, 1);
+ }
+
+ public SemaphoreSlim Block { get; private set; }
+
+ protected async override Task SerializeToStreamAsync(Stream stream, TransportContext context)
+ {
+ await stream.WriteAsync(new byte[5], 0, 5);
+ await stream.FlushAsync();
+ Assert.True(await Block.WaitAsync(TimeSpan.FromSeconds(10)));
+ await stream.WriteAsync(new byte[5], 0, 5);
+ }
+
+ protected override bool TryComputeLength(out long length)
+ {
+ length = 10;
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestHeaderTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestHeaderTests.cs
new file mode 100644
index 0000000000..39e237eb5e
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestHeaderTests.cs
@@ -0,0 +1,98 @@
+// 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.Net.Http;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class RequestHeaderTests
+ {
+ [ConditionalFact]
+ public async Task RequestHeaders_ClientSendsDefaultHeaders_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var requestHeaders = httpContext.Request.Headers;
+ // NOTE: The System.Net client only sends the Connection: keep-alive header on the first connection per service-point.
+ // Assert.Equal(2, requestHeaders.Count);
+ // Assert.Equal("Keep-Alive", requestHeaders.Get("Connection"));
+ Assert.False(StringValues.IsNullOrEmpty(requestHeaders["Host"]));
+ Assert.True(StringValues.IsNullOrEmpty(requestHeaders["Accept"]));
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(address);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task RequestHeaders_ClientSendsCustomHeaders_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var requestHeaders = httpContext.Request.Headers;
+ Assert.Equal(4, requestHeaders.Count);
+ Assert.False(StringValues.IsNullOrEmpty(requestHeaders["Host"]));
+ Assert.Equal("close", requestHeaders["Connection"]);
+ // Apparently Http.Sys squashes request headers together.
+ Assert.Single(requestHeaders["Custom-Header"]);
+ Assert.Equal("custom1, and custom2, custom3", requestHeaders["Custom-Header"]);
+ Assert.Single(requestHeaders["Spacer-Header"]);
+ Assert.Equal("spacervalue, spacervalue", requestHeaders["Spacer-Header"]);
+ return Task.FromResult(0);
+ }))
+ {
+ string[] customValues = new string[] { "custom1, and custom2", "custom3" };
+
+ await SendRequestAsync(address, "Custom-Header", customValues);
+ }
+ }
+
+ private async Task<string> SendRequestAsync(string uri)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetStringAsync(uri);
+ }
+ }
+
+ private async Task SendRequestAsync(string address, string customHeader, string[] customValues)
+ {
+ var uri = new Uri(address);
+ StringBuilder builder = new StringBuilder();
+ builder.AppendLine("GET / HTTP/1.1");
+ builder.AppendLine("Connection: close");
+ builder.Append("HOST: ");
+ builder.AppendLine(uri.Authority);
+ foreach (string value in customValues)
+ {
+ builder.Append(customHeader);
+ builder.Append(": ");
+ builder.AppendLine(value);
+ builder.AppendLine("Spacer-Header: spacervalue");
+ }
+ builder.AppendLine();
+
+ byte[] request = Encoding.ASCII.GetBytes(builder.ToString());
+
+ Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
+ socket.Connect(uri.Host, uri.Port);
+
+ socket.Send(request);
+
+ byte[] response = new byte[1024 * 5];
+ await Task.Run(() => socket.Receive(response));
+ socket.Dispose();
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestTests.cs
new file mode 100644
index 0000000000..837c53340a
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/RequestTests.cs
@@ -0,0 +1,388 @@
+// 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 System.Net;
+using System.Net.Http;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class RequestTests
+ {
+ [ConditionalFact]
+ public async Task Request_SimpleGet_ExpectedFieldsSet()
+ {
+ string root;
+ using (Utilities.CreateHttpServerReturnRoot("/basepath", out root, httpContext =>
+ {
+ try
+ {
+ var requestInfo = httpContext.Features.Get<IHttpRequestFeature>();
+
+ // Request Keys
+ Assert.Equal("GET", requestInfo.Method);
+ Assert.Equal(Stream.Null, requestInfo.Body);
+ Assert.NotNull(requestInfo.Headers);
+ Assert.Equal("http", requestInfo.Scheme);
+ Assert.Equal("/basepath", requestInfo.PathBase);
+ Assert.Equal("/SomePath", requestInfo.Path);
+ Assert.Equal("?SomeQuery", requestInfo.QueryString);
+ Assert.Equal("/basepath/SomePath?SomeQuery", requestInfo.RawTarget);
+ Assert.Equal("HTTP/1.1", requestInfo.Protocol);
+
+ var connectionInfo = httpContext.Features.Get<IHttpConnectionFeature>();
+ Assert.Equal("::1", connectionInfo.RemoteIpAddress.ToString());
+ Assert.NotEqual(0, connectionInfo.RemotePort);
+ Assert.Equal("::1", connectionInfo.LocalIpAddress.ToString());
+ Assert.NotEqual(0, connectionInfo.LocalPort);
+ Assert.NotNull(connectionInfo.ConnectionId);
+
+ // Trace identifier
+ var requestIdentifierFeature = httpContext.Features.Get<IHttpRequestIdentifierFeature>();
+ Assert.NotNull(requestIdentifierFeature);
+ Assert.NotNull(requestIdentifierFeature.TraceIdentifier);
+
+ // Note: Response keys are validated in the ResponseTests
+ }
+ catch (Exception ex)
+ {
+ byte[] body = Encoding.ASCII.GetBytes(ex.ToString());
+ httpContext.Response.Body.Write(body, 0, body.Length);
+ }
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(root + "/basepath/SomePath?SomeQuery");
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Request_FieldsCanBeSet_Set()
+ {
+ string root;
+ using (Utilities.CreateHttpServerReturnRoot("/basepath", out root, httpContext =>
+ {
+ try
+ {
+ var requestInfo = httpContext.Features.Get<IHttpRequestFeature>();
+
+ // Request Keys
+ requestInfo.Method = "TEST";
+ Assert.Equal("TEST", requestInfo.Method);
+ requestInfo.Body = new MemoryStream();
+ Assert.IsType<MemoryStream>(requestInfo.Body);
+ var customHeaders = new HeaderCollection();
+ requestInfo.Headers = customHeaders;
+ Assert.Same(customHeaders, requestInfo.Headers);
+ requestInfo.Scheme = "abcd";
+ Assert.Equal("abcd", requestInfo.Scheme);
+ requestInfo.PathBase = "/customized/Base";
+ Assert.Equal("/customized/Base", requestInfo.PathBase);
+ requestInfo.Path = "/customized/Path";
+ Assert.Equal("/customized/Path", requestInfo.Path);
+ requestInfo.QueryString = "?customizedQuery";
+ Assert.Equal("?customizedQuery", requestInfo.QueryString);
+ requestInfo.RawTarget = "/customized/raw?Target";
+ Assert.Equal("/customized/raw?Target", requestInfo.RawTarget);
+ requestInfo.Protocol = "Custom/2.0";
+ Assert.Equal("Custom/2.0", requestInfo.Protocol);
+
+ var connectionInfo = httpContext.Features.Get<IHttpConnectionFeature>();
+ connectionInfo.RemoteIpAddress = IPAddress.Broadcast;
+ Assert.Equal(IPAddress.Broadcast, connectionInfo.RemoteIpAddress);
+ connectionInfo.RemotePort = 12345;
+ Assert.Equal(12345, connectionInfo.RemotePort);
+ connectionInfo.LocalIpAddress = IPAddress.Any;
+ Assert.Equal(IPAddress.Any, connectionInfo.LocalIpAddress);
+ connectionInfo.LocalPort = 54321;
+ Assert.Equal(54321, connectionInfo.LocalPort);
+ connectionInfo.ConnectionId = "CustomId";
+ Assert.Equal("CustomId", connectionInfo.ConnectionId);
+
+ // Trace identifier
+ var requestIdentifierFeature = httpContext.Features.Get<IHttpRequestIdentifierFeature>();
+ Assert.NotNull(requestIdentifierFeature);
+ requestIdentifierFeature.TraceIdentifier = "customTrace";
+ Assert.Equal("customTrace", requestIdentifierFeature.TraceIdentifier);
+
+ // Note: Response keys are validated in the ResponseTests
+ }
+ catch (Exception ex)
+ {
+ byte[] body = Encoding.ASCII.GetBytes(ex.ToString());
+ httpContext.Response.Body.Write(body, 0, body.Length);
+ }
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(root + "/basepath/SomePath?SomeQuery");
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Request_FieldsCanBeSetToNull_Set()
+ {
+ string root;
+ using (Utilities.CreateHttpServerReturnRoot("/basepath", out root, httpContext =>
+ {
+ try
+ {
+ var requestInfo = httpContext.Features.Get<IHttpRequestFeature>();
+
+ // Request Keys
+ requestInfo.Method = null;
+ Assert.Null(requestInfo.Method);
+ requestInfo.Body = null;
+ Assert.Null(requestInfo.Body);
+ requestInfo.Headers = null;
+ Assert.Null(requestInfo.Headers);
+ requestInfo.Scheme = null;
+ Assert.Null(requestInfo.Scheme);
+ requestInfo.PathBase = null;
+ Assert.Null(requestInfo.PathBase);
+ requestInfo.Path = null;
+ Assert.Null(requestInfo.Path);
+ requestInfo.QueryString = null;
+ Assert.Null(requestInfo.QueryString);
+ requestInfo.RawTarget = null;
+ Assert.Null(requestInfo.RawTarget);
+ requestInfo.Protocol = null;
+ Assert.Null(requestInfo.Protocol);
+
+ var connectionInfo = httpContext.Features.Get<IHttpConnectionFeature>();
+ connectionInfo.RemoteIpAddress = null;
+ Assert.Null(connectionInfo.RemoteIpAddress);
+ connectionInfo.RemotePort = -1;
+ Assert.Equal(-1, connectionInfo.RemotePort);
+ connectionInfo.LocalIpAddress = null;
+ Assert.Null(connectionInfo.LocalIpAddress);
+ connectionInfo.LocalPort = -1;
+ Assert.Equal(-1, connectionInfo.LocalPort);
+ connectionInfo.ConnectionId = null;
+ Assert.Null(connectionInfo.ConnectionId);
+
+ // Trace identifier
+ var requestIdentifierFeature = httpContext.Features.Get<IHttpRequestIdentifierFeature>();
+ Assert.NotNull(requestIdentifierFeature);
+ requestIdentifierFeature.TraceIdentifier = null;
+ Assert.Null(requestIdentifierFeature.TraceIdentifier);
+
+ // Note: Response keys are validated in the ResponseTests
+ }
+ catch (Exception ex)
+ {
+ byte[] body = Encoding.ASCII.GetBytes(ex.ToString());
+ httpContext.Response.Body.Write(body, 0, body.Length);
+ }
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(root + "/basepath/SomePath?SomeQuery");
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData("/", "/", "", "/")]
+ [InlineData("/basepath/", "/basepath", "/basepath", "")]
+ [InlineData("/basepath/", "/basepath/", "/basepath", "/")]
+ [InlineData("/basepath/", "/basepath/subpath", "/basepath", "/subpath")]
+ [InlineData("/base path/", "/base%20path/sub%20path", "/base path", "/sub path")]
+ [InlineData("/base葉path/", "/base%E8%91%89path/sub%E8%91%89path", "/base葉path", "/sub葉path")]
+ [InlineData("/basepath/", "/basepath/sub%2Fpath", "/basepath", "/sub%2Fpath")]
+ public async Task Request_PathSplitting(string pathBase, string requestPath, string expectedPathBase, string expectedPath)
+ {
+ string root;
+ using (Utilities.CreateHttpServerReturnRoot(pathBase, out root, httpContext =>
+ {
+ try
+ {
+ var requestInfo = httpContext.Features.Get<IHttpRequestFeature>();
+ var connectionInfo = httpContext.Features.Get<IHttpConnectionFeature>();
+ var requestIdentifierFeature = httpContext.Features.Get<IHttpRequestIdentifierFeature>();
+
+ // Request Keys
+ Assert.Equal("http", requestInfo.Scheme);
+ Assert.Equal(expectedPath, requestInfo.Path);
+ Assert.Equal(expectedPathBase, requestInfo.PathBase);
+ Assert.Equal(string.Empty, requestInfo.QueryString);
+ Assert.Equal(requestPath, requestInfo.RawTarget);
+
+ // Trace identifier
+ Assert.NotNull(requestIdentifierFeature);
+ Assert.NotNull(requestIdentifierFeature.TraceIdentifier);
+ }
+ catch (Exception ex)
+ {
+ byte[] body = Encoding.ASCII.GetBytes(ex.ToString());
+ httpContext.Response.Body.Write(body, 0, body.Length);
+ }
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(root + requestPath);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Request_DoubleEscapingAllowed()
+ {
+ string root;
+ using (var server = Utilities.CreateHttpServerReturnRoot("/", out root, httpContext =>
+ {
+ var requestInfo = httpContext.Features.Get<IHttpRequestFeature>();
+ Assert.Equal("/%2F", requestInfo.Path);
+ Assert.Equal("/%252F", requestInfo.RawTarget);
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendSocketRequestAsync(root, "/%252F");
+ var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 "
+ Assert.Equal("200", responseStatusCode);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Request_FullUriInRequestLine_ParsesPath()
+ {
+ using (var server = Utilities.CreateHttpServerReturnRoot("/", out var root, httpContext =>
+ {
+ var requestInfo = httpContext.Features.Get<IHttpRequestFeature>();
+ Assert.Equal("/", requestInfo.Path);
+ Assert.Equal("", requestInfo.PathBase);
+ return Task.CompletedTask;
+ }))
+ {
+ // Send a HTTP request with the request line:
+ // GET http://localhost:5001 HTTP/1.1
+ var response = await SendSocketRequestAsync(root, root);
+ var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 "
+ Assert.Equal(StatusCodes.Status200OK.ToString(), responseStatusCode);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Request_FullUriInRequestLineWithSlashesInQuery_BlockedByHttpSys()
+ {
+ using (var server = Utilities.CreateHttpServerReturnRoot("/", out var root, httpContext =>
+ {
+ return Task.CompletedTask;
+ }))
+ {
+ // Send a HTTP request with the request line:
+ // GET http://localhost:5001?query=value/1/2 HTTP/1.1
+ // Should return a 400 as it is a client error
+ var response = await SendSocketRequestAsync(root, root + "?query=value/1/2");
+ var responseStatusCode = response.Substring(9); // Skip "HTTP/1.1 "
+ Assert.Equal(StatusCodes.Status400BadRequest.ToString(), responseStatusCode);
+ }
+ }
+
+ [ConditionalTheory]
+ // The test server defines these prefixes: "/", "/11", "/2/3", "/2", "/11/2"
+ [InlineData("/", "", "/")]
+ [InlineData("/random", "", "/random")]
+ [InlineData("/11", "/11", "")]
+ [InlineData("/11/", "/11", "/")]
+ [InlineData("/11/random", "/11", "/random")]
+ [InlineData("/2", "/2", "")]
+ [InlineData("/2/", "/2", "/")]
+ [InlineData("/2/random", "/2", "/random")]
+ [InlineData("/2/3", "/2/3", "")]
+ [InlineData("/2/3/", "/2/3", "/")]
+ [InlineData("/2/3/random", "/2/3", "/random")]
+ public async Task Request_MultiplePrefixes(string requestPath, string expectedPathBase, string expectedPath)
+ {
+ string root;
+ using (CreateServer(out root, httpContext =>
+ {
+ var requestInfo = httpContext.Features.Get<IHttpRequestFeature>();
+ var requestIdentifierFeature = httpContext.Features.Get<IHttpRequestIdentifierFeature>();
+ try
+ {
+ Assert.Equal(expectedPath, requestInfo.Path);
+ Assert.Equal(expectedPathBase, requestInfo.PathBase);
+ Assert.Equal(requestPath, requestInfo.RawTarget);
+
+ // Trace identifier
+ Assert.NotNull(requestIdentifierFeature);
+ Assert.NotNull(requestIdentifierFeature.TraceIdentifier);
+ }
+ catch (Exception ex)
+ {
+ byte[] body = Encoding.ASCII.GetBytes(ex.ToString());
+ httpContext.Response.Body.Write(body, 0, body.Length);
+ }
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(root + requestPath);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ private IServer CreateServer(out string root, RequestDelegate app)
+ {
+ // TODO: We're just doing this to get a dynamic port. This can be removed later when we add support for hot-adding prefixes.
+ var dynamicServer = Utilities.CreateHttpServerReturnRoot("/", out root, app);
+ dynamicServer.Dispose();
+ var rootUri = new Uri(root);
+ var server = Utilities.CreatePump();
+
+ foreach (string path in new[] { "/", "/11", "/2/3", "/2", "/11/2" })
+ {
+ server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(rootUri.Scheme, rootUri.Host, rootUri.Port, path));
+ }
+
+ server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
+ return server;
+ }
+
+ private async Task<string> SendRequestAsync(string uri)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetStringAsync(uri);
+ }
+ }
+
+ private async Task<string> SendSocketRequestAsync(string address, string path)
+ {
+ var uri = new Uri(address);
+ StringBuilder builder = new StringBuilder();
+ builder.AppendLine("GET " + path + " HTTP/1.1");
+ builder.AppendLine("Connection: close");
+ builder.Append("HOST: ");
+ builder.AppendLine(uri.Authority);
+ builder.AppendLine();
+
+ byte[] request = Encoding.ASCII.GetBytes(builder.ToString());
+
+ using (var socket = new Socket(SocketType.Stream, ProtocolType.Tcp))
+ {
+ socket.Connect(uri.Host, uri.Port);
+ socket.Send(request);
+ var response = new byte[12];
+ await Task.Run(() => socket.Receive(response));
+ return Encoding.ASCII.GetString(response);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseBodyTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseBodyTests.cs
new file mode 100644
index 0000000000..a42071b342
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseBodyTests.cs
@@ -0,0 +1,289 @@
+// 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.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class ResponseBodyTests
+ {
+ [ConditionalFact]
+ public async Task ResponseBody_WriteNoHeaders_SetsChunked()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ httpContext.Response.Body.Write(new byte[10], 0, 10);
+ return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
+ Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteNoHeadersAndFlush_DefaultsToChunked()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ httpContext.Response.Body.Write(new byte[10], 0, 10);
+ await httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
+ await httpContext.Response.Body.FlushAsync();
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
+ Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteChunked_ManuallyChunked()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ httpContext.Response.Headers["transfeR-Encoding"] = "CHunked";
+ Stream stream = httpContext.Response.Body;
+ var responseBytes = Encoding.ASCII.GetBytes("10\r\nManually Chunked\r\n0\r\n\r\n");
+ await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
+ Assert.Equal("Manually Chunked", await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteContentLength_PassedThrough()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ httpContext.Response.Headers["Content-lenGth"] = " 30 ";
+ Stream stream = httpContext.Response.Body;
+ stream.EndWrite(stream.BeginWrite(new byte[10], 0, 10, null, null));
+ stream.Write(new byte[10], 0, 10);
+ await stream.WriteAsync(new byte[10], 0, 10);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ IEnumerable<string> contentLength;
+ Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.Equal("30", contentLength.First());
+ Assert.Null(response.Headers.TransferEncodingChunked);
+ Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteContentLengthNoneWritten_Throws()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.Headers["Content-lenGth"] = " 20 ";
+ return Task.FromResult(0);
+ }))
+ {
+ await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteContentLengthNotEnoughWritten_Throws()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.Headers["Content-lenGth"] = " 20 ";
+ return httpContext.Response.Body.WriteAsync(new byte[5], 0, 5);
+ }))
+ {
+ await Assert.ThrowsAsync<HttpRequestException>(async () => await SendRequestAsync(address));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteContentLengthTooMuchWritten_Throws()
+ {
+ var completed = false;
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ httpContext.Response.Headers["Content-lenGth"] = " 10 ";
+ await httpContext.Response.Body.WriteAsync(new byte[5], 0, 5);
+ await Assert.ThrowsAsync<InvalidOperationException>(() =>
+ httpContext.Response.Body.WriteAsync(new byte[6], 0, 6));
+ completed = true;
+ }))
+ {
+ await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
+ Assert.True(completed);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteContentLengthExtraWritten_Throws()
+ {
+ var waitHandle = new ManualResetEvent(false);
+ bool? appThrew = null;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ try
+ {
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ httpContext.Response.Headers["Content-lenGth"] = " 10 ";
+ httpContext.Response.Body.Write(new byte[10], 0, 10);
+ httpContext.Response.Body.Write(new byte[9], 0, 9);
+ appThrew = false;
+ }
+ catch (Exception)
+ {
+ appThrew = true;
+ }
+ waitHandle.Set();
+ return Task.FromResult(0);
+ }))
+ {
+ // The full response is received.
+ HttpResponseMessage response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ IEnumerable<string> contentLength;
+ Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.Equal("10", contentLength.First());
+ Assert.Null(response.Headers.TransferEncodingChunked);
+ Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
+
+ Assert.True(waitHandle.WaitOne(100));
+ Assert.True(appThrew.HasValue, "appThrew.HasValue");
+ Assert.True(appThrew.Value, "appThrew.Value");
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_Write_TriggersOnStarting()
+ {
+ var onStartingCalled = false;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ httpContext.Response.OnStarting(state =>
+ {
+ onStartingCalled = true;
+ Assert.Same(state, httpContext);
+ return Task.FromResult(0);
+ }, httpContext);
+ httpContext.Response.Body.Write(new byte[10], 0, 10);
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.True(onStartingCalled);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
+ Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_BeginWrite_TriggersOnStarting()
+ {
+ var onStartingCalled = false;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.OnStarting(state =>
+ {
+ onStartingCalled = true;
+ Assert.Same(state, httpContext);
+ return Task.FromResult(0);
+ }, httpContext);
+ httpContext.Response.Body.EndWrite(httpContext.Response.Body.BeginWrite(new byte[10], 0, 10, null, null));
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.True(onStartingCalled);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
+ Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseBody_WriteAsync_TriggersOnStarting()
+ {
+ var onStartingCalled = false;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.OnStarting(state =>
+ {
+ onStartingCalled = true;
+ Assert.Same(state, httpContext);
+ return Task.FromResult(0);
+ }, httpContext);
+ return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.True(onStartingCalled);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
+ Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
+ }
+ }
+
+ private async Task<HttpResponseMessage> SendRequestAsync(string uri)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetAsync(uri);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseCachingTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseCachingTests.cs
new file mode 100644
index 0000000000..6a2fe8c57d
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseCachingTests.cs
@@ -0,0 +1,225 @@
+// 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.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests
+{
+ public class ResponseCachingTests
+ {
+ [ConditionalFact]
+ public async Task Caching_NoCacheControl_NotCached()
+ {
+ var requestCount = 1;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
+ httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
+ httpContext.Response.ContentLength = 10;
+ return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
+ }))
+ {
+ Assert.Equal("1", await SendRequestAsync(address));
+ Assert.Equal("2", await SendRequestAsync(address));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_JustPublic_NotCached()
+ {
+ var requestCount = 1;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
+ httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
+ httpContext.Response.Headers["Cache-Control"] = "public";
+ httpContext.Response.ContentLength = 10;
+ return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
+ }))
+ {
+ Assert.Equal("1", await SendRequestAsync(address));
+ Assert.Equal("2", await SendRequestAsync(address));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_MaxAge_Cached()
+ {
+ var requestCount = 1;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
+ httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
+ httpContext.Response.Headers["Cache-Control"] = "public, max-age=10";
+ httpContext.Response.ContentLength = 10;
+ return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
+ }))
+ {
+ Assert.Equal("1", await SendRequestAsync(address));
+ Assert.Equal("1", await SendRequestAsync(address));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_SMaxAge_Cached()
+ {
+ var requestCount = 1;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
+ httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
+ httpContext.Response.Headers["Cache-Control"] = "public, s-maxage=10";
+ httpContext.Response.ContentLength = 10;
+ return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
+ }))
+ {
+ Assert.Equal("1", await SendRequestAsync(address));
+ Assert.Equal("1", await SendRequestAsync(address));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_SMaxAgeAndMaxAge_SMaxAgePreferredCached()
+ {
+ var requestCount = 1;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
+ httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
+ httpContext.Response.Headers["Cache-Control"] = "public, max-age=0, s-maxage=10";
+ httpContext.Response.ContentLength = 10;
+ return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
+ }))
+ {
+ Assert.Equal("1", await SendRequestAsync(address));
+ Assert.Equal("1", await SendRequestAsync(address));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_Expires_Cached()
+ {
+ var requestCount = 1;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
+ httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
+ httpContext.Response.Headers["Cache-Control"] = "public";
+ httpContext.Response.Headers["Expires"] = (DateTime.UtcNow + TimeSpan.FromSeconds(10)).ToString("r");
+ httpContext.Response.ContentLength = 10;
+ return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
+ }))
+ {
+ Assert.Equal("1", await SendRequestAsync(address));
+ Assert.Equal("1", await SendRequestAsync(address));
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData("Set-cookie")]
+ [InlineData("vary")]
+ [InlineData("pragma")]
+ public async Task Caching_DisallowedResponseHeaders_NotCached(string headerName)
+ {
+ var requestCount = 1;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
+ httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
+ httpContext.Response.Headers["Cache-Control"] = "public, max-age=10";
+ httpContext.Response.Headers[headerName] = "headerValue";
+ httpContext.Response.ContentLength = 10;
+ return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
+ }))
+ {
+ Assert.Equal("1", await SendRequestAsync(address));
+ Assert.Equal("2", await SendRequestAsync(address));
+ }
+ }
+
+ [ConditionalTheory]
+ [InlineData("0")]
+ [InlineData("-1")]
+ public async Task Caching_InvalidExpires_NotCached(string expiresValue)
+ {
+ var requestCount = 1;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
+ httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
+ httpContext.Response.Headers["Cache-Control"] = "public";
+ httpContext.Response.Headers["Expires"] = expiresValue;
+ httpContext.Response.ContentLength = 10;
+ return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
+ }))
+ {
+ Assert.Equal("1", await SendRequestAsync(address));
+ Assert.Equal("2", await SendRequestAsync(address));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_ExpiresWithoutPublic_NotCached()
+ {
+ var requestCount = 1;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
+ httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
+ httpContext.Response.Headers["Expires"] = (DateTime.UtcNow + TimeSpan.FromSeconds(10)).ToString("r");
+ httpContext.Response.ContentLength = 10;
+ return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
+ }))
+ {
+ Assert.Equal("1", await SendRequestAsync(address));
+ Assert.Equal("2", await SendRequestAsync(address));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Caching_MaxAgeAndExpires_MaxAgePreferred()
+ {
+ var requestCount = 1;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.ContentType = "some/thing"; // Http.Sys requires content-type for caching
+ httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString();
+ httpContext.Response.Headers["Cache-Control"] = "public, max-age=10";
+ httpContext.Response.Headers["Expires"] = (DateTime.UtcNow - TimeSpan.FromSeconds(10)).ToString("r"); // In the past
+ httpContext.Response.ContentLength = 10;
+ return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
+ }))
+ {
+ Assert.Equal("1", await SendRequestAsync(address));
+ Assert.Equal("1", await SendRequestAsync(address));
+ }
+ }
+
+ private async Task<string> SendRequestAsync(string uri)
+ {
+ using (var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(10) })
+ {
+ var response = await client.GetAsync(uri);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(10, response.Content.Headers.ContentLength);
+ Assert.Equal(new byte[10], await response.Content.ReadAsByteArrayAsync());
+ return response.Headers.GetValues("x-request-count").FirstOrDefault();
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseHeaderTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseHeaderTests.cs
new file mode 100644
index 0000000000..ec93832e18
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseHeaderTests.cs
@@ -0,0 +1,322 @@
+// 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.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class ResponseHeaderTests
+ {
+ [ConditionalFact]
+ public async Task ResponseHeaders_ServerSendsDefaultHeaders_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ return Task.FromResult(0);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(2, response.Headers.Count());
+ Assert.False(response.Headers.TransferEncodingChunked.HasValue);
+ Assert.True(response.Headers.Date.HasValue);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
+ Assert.Single(response.Content.Headers);
+ Assert.Equal(0, response.Content.Headers.ContentLength);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_ServerSendsSingleValueKnownHeaders_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
+ var responseHeaders = responseInfo.Headers;
+ responseHeaders["WWW-Authenticate"] = new string[] { "custom1" };
+ return Task.FromResult(0);
+ }))
+ {
+ // HttpClient would merge the headers no matter what
+ WebRequest request = WebRequest.Create(address);
+ HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync();
+ Assert.Equal(4, response.Headers.Count);
+ Assert.Null(response.Headers["Transfer-Encoding"]);
+ Assert.Equal(0, response.ContentLength);
+ Assert.NotNull(response.Headers["Date"]);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]);
+ Assert.Equal("custom1", response.Headers["WWW-Authenticate"]);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_ServerSendsMultiValueKnownHeaders_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
+ var responseHeaders = responseInfo.Headers;
+ responseHeaders["WWW-Authenticate"] = new string[] { "custom1, and custom2", "custom3" };
+ return Task.FromResult(0);
+ }))
+ {
+ // HttpClient would merge the headers no matter what
+ WebRequest request = WebRequest.Create(address);
+ HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync();
+ Assert.Equal(4, response.Headers.Count);
+ Assert.Null(response.Headers["Transfer-Encoding"]);
+ Assert.Equal(0, response.ContentLength);
+ Assert.NotNull(response.Headers["Date"]);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]);
+#if NETCOREAPP2_0 || NETCOREAPP2_1 // WebHeaderCollection.GetValues() not available in CoreCLR.
+ Assert.Equal("custom1, and custom2, custom3", response.Headers["WWW-Authenticate"]);
+#elif NET461
+ Assert.Equal(new string[] { "custom1, and custom2", "custom3" }, response.Headers.GetValues("WWW-Authenticate"));
+#else
+#error Target framework needs to be updated
+#endif
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_ServerSendsCustomHeaders_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
+ var responseHeaders = responseInfo.Headers;
+ responseHeaders["Custom-Header1"] = new string[] { "custom1, and custom2", "custom3" };
+ return Task.FromResult(0);
+ }))
+ {
+ // HttpClient would merge the headers no matter what
+ WebRequest request = WebRequest.Create(address);
+ HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync();
+ Assert.Equal(4, response.Headers.Count);
+ Assert.Null(response.Headers["Transfer-Encoding"]);
+ Assert.Equal(0, response.ContentLength);
+ Assert.NotNull(response.Headers["Date"]);
+ Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]);
+#if NETCOREAPP2_0 || NETCOREAPP2_1 // WebHeaderCollection.GetValues() not available in CoreCLR.
+ Assert.Equal("custom1, and custom2, custom3", response.Headers["Custom-Header1"]);
+#elif NET461
+ Assert.Equal(new string[] { "custom1, and custom2", "custom3" }, response.Headers.GetValues("Custom-Header1"));
+#else
+#error Target framework needs to be updated
+#endif
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_ServerSendsConnectionClose_Closed()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
+ var responseHeaders = responseInfo.Headers;
+ responseHeaders["Connection"] = new string[] { "Close" };
+ return httpContext.Response.Body.FlushAsync(); // Http.Sys adds the Content-Length: header for us if we don't flush
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ response.EnsureSuccessStatusCode();
+ Assert.True(response.Headers.ConnectionClose.Value);
+ Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
+ Assert.True(response.Headers.TransferEncodingChunked.HasValue);
+ Assert.True(response.Headers.TransferEncodingChunked);
+ IEnumerable<string> values;
+ var result = response.Content.Headers.TryGetValues("Content-Length", out values);
+ Assert.False(result);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_HTTP10Request_Gets11Close()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ return Task.FromResult(0);
+ }))
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, address);
+ request.Version = new Version(1, 0);
+ HttpResponseMessage response = await client.SendAsync(request);
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.True(response.Headers.ConnectionClose.Value);
+ Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
+ Assert.False(response.Headers.TransferEncodingChunked.HasValue);
+ }
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseHeaders_HTTP10RequestWithChunkedHeader_ManualChunking()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
+ var responseHeaders = responseInfo.Headers;
+ responseHeaders["Transfer-Encoding"] = new string[] { "chunked" };
+ var responseBytes = Encoding.ASCII.GetBytes("10\r\nManually Chunked\r\n0\r\n\r\n");
+ return responseInfo.Body.WriteAsync(responseBytes, 0, responseBytes.Length);
+ }))
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, address);
+ request.Version = new Version(1, 0);
+ HttpResponseMessage response = await client.SendAsync(request);
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.True(response.Headers.TransferEncodingChunked.HasValue);
+ Assert.False(response.Content.Headers.Contains("Content-Length"));
+ Assert.True(response.Headers.ConnectionClose.Value);
+ Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
+ Assert.Equal("Manually Chunked", await response.Content.ReadAsStringAsync());
+ }
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Headers_FlushSendsHeaders_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
+ var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
+ var responseHeaders = responseInfo.Headers;
+ responseHeaders.Add("Custom1", new string[] { "value1a", "value1b" });
+ responseHeaders.Add("Custom2", new string[] { "value2a, value2b" });
+ var body = responseInfo.Body;
+ Assert.False(responseInfo.HasStarted);
+ body.Flush();
+ Assert.True(responseInfo.HasStarted);
+ Assert.Throws<InvalidOperationException>(() => responseInfo.StatusCode = 404);
+ Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
+ return Task.FromResult(0);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(5, response.Headers.Count()); // Date, Server, Chunked
+
+ Assert.Equal(2, response.Headers.GetValues("Custom1").Count());
+ Assert.Equal("value1a", response.Headers.GetValues("Custom1").First());
+ Assert.Equal("value1b", response.Headers.GetValues("Custom1").Skip(1).First());
+ Assert.Single(response.Headers.GetValues("Custom2"));
+ Assert.Equal("value2a, value2b", response.Headers.GetValues("Custom2").First());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Headers_FlushAsyncSendsHeaders_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
+ var responseHeaders = responseInfo.Headers;
+ responseHeaders.Add("Custom1", new string[] { "value1a", "value1b" });
+ responseHeaders.Add("Custom2", new string[] { "value2a, value2b" });
+ var body = responseInfo.Body;
+ Assert.False(responseInfo.HasStarted);
+ await body.FlushAsync();
+ Assert.True(responseInfo.HasStarted);
+ Assert.Throws<InvalidOperationException>(() => responseInfo.StatusCode = 404);
+ Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ response.EnsureSuccessStatusCode();
+ Assert.Equal(5, response.Headers.Count()); // Date, Server, Chunked
+
+ Assert.Equal(2, response.Headers.GetValues("Custom1").Count());
+ Assert.Equal("value1a", response.Headers.GetValues("Custom1").First());
+ Assert.Equal("value1b", response.Headers.GetValues("Custom1").Skip(1).First());
+ Assert.Single(response.Headers.GetValues("Custom2"));
+ Assert.Equal("value2a, value2b", response.Headers.GetValues("Custom2").First());
+ }
+ }
+
+ [ConditionalTheory, MemberData(nameof(NullHeaderData))]
+ public async Task Headers_IgnoreNullHeaders(string headerName, StringValues headerValue, StringValues expectedValue)
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var responseHeaders = httpContext.Response.Headers;
+ responseHeaders.Add(headerName, headerValue);
+ return Task.FromResult(0);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ response.EnsureSuccessStatusCode();
+ var headers = response.Headers;
+
+ if (expectedValue.Count == 0)
+ {
+ Assert.False(headers.Contains(headerName));
+ }
+ else
+ {
+ Assert.True(headers.Contains(headerName));
+ Assert.Equal(headers.GetValues(headerName), expectedValue);
+ }
+ }
+ }
+
+ public static TheoryData<string, StringValues, StringValues> NullHeaderData
+ {
+ get
+ {
+ var dataset = new TheoryData<string, StringValues, StringValues>();
+
+ // Unknown headers
+ dataset.Add("NullString", (string)null, (string)null);
+ dataset.Add("EmptyString", "", "");
+ dataset.Add("NullStringArray", new string[] { null }, "");
+ dataset.Add("EmptyStringArray", new string[] { "" }, "");
+ dataset.Add("MixedStringArray", new string[] { null, "" }, new string[] { "", "" });
+ // Known headers
+ dataset.Add("Location", (string)null, (string)null);
+ dataset.Add("Location", "", (string)null);
+ dataset.Add("Location", new string[] { null }, (string)null);
+ dataset.Add("Location", new string[] { "" }, (string)null);
+ dataset.Add("Location", new string[] { "a" }, "a");
+ dataset.Add("Location", new string[] { null, "" }, (string)null);
+ dataset.Add("Location", new string[] { null, "", "a", "b" }, new string[] { "a", "b" });
+
+ return dataset;
+ }
+ }
+
+ private async Task<HttpResponseMessage> SendRequestAsync(string uri)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetAsync(uri);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseSendFileTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseSendFileTests.cs
new file mode 100644
index 0000000000..d3a8c5e5d0
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseSendFileTests.cs
@@ -0,0 +1,359 @@
+// 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.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class ResponseSendFileTests
+ {
+ private readonly string AbsoluteFilePath;
+ private readonly string RelativeFilePath;
+ private readonly long FileLength;
+
+ public ResponseSendFileTests()
+ {
+ AbsoluteFilePath = Directory.GetFiles(Directory.GetCurrentDirectory()).First();
+ RelativeFilePath = Path.GetFileName(AbsoluteFilePath);
+ FileLength = new FileInfo(AbsoluteFilePath).Length;
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_SupportKeys_Present()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ try
+ {
+ /* TODO:
+ IDictionary<string, object> capabilities = httpContext.Get<IDictionary<string, object>>("server.Capabilities");
+ Assert.NotNull(capabilities);
+
+ Assert.Equal("1.0", capabilities.Get<string>("sendfile.Version"));
+
+ IDictionary<string, object> support = capabilities.Get<IDictionary<string, object>>("sendfile.Support");
+ Assert.NotNull(support);
+
+ Assert.Equal("Overlapped", support.Get<string>("sendfile.Concurrency"));
+ */
+
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ Assert.NotNull(sendFile);
+ }
+ catch (Exception ex)
+ {
+ byte[] body = Encoding.UTF8.GetBytes(ex.ToString());
+ httpContext.Response.Body.Write(body, 0, body.Length);
+ }
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> ignored;
+ Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
+ Assert.Equal(0, response.Content.Headers.ContentLength);
+ Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_MissingFile_Throws()
+ {
+ var waitHandle = new ManualResetEvent(false);
+ bool? appThrew = null;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ try
+ {
+ sendFile.SendFileAsync(string.Empty, 0, null, CancellationToken.None).Wait();
+ appThrew = false;
+ }
+ catch (Exception)
+ {
+ appThrew = true;
+ throw;
+ }
+ finally
+ {
+ waitHandle.Set();
+ }
+ return Task.FromResult(0);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ Assert.True(waitHandle.WaitOne(100));
+ Assert.True(appThrew.HasValue, "appThrew.HasValue");
+ Assert.True(appThrew.Value, "appThrew.Value");
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_NoHeaders_DefaultsToChunked()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
+ Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_RelativeFile_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ return sendFile.SendFileAsync(RelativeFilePath, 0, null, CancellationToken.None);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
+ Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_Unspecified_Chunked()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value);
+ Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_MultipleWrites_Chunked()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None).Wait();
+ return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value);
+ Assert.Equal(FileLength * 2, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_HalfOfFile_Chunked()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ return sendFile.SendFileAsync(AbsoluteFilePath, 0, FileLength / 2, CancellationToken.None);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value);
+ Assert.Equal(FileLength / 2, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_OffsetOutOfRange_Throws()
+ {
+ var completed = false;
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
+ sendFile.SendFileAsync(AbsoluteFilePath, 1234567, null, CancellationToken.None));
+ completed = true;
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ response.EnsureSuccessStatusCode();
+ Assert.True(completed);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_CountOutOfRange_Throws()
+ {
+ var completed = false;
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
+ sendFile.SendFileAsync(AbsoluteFilePath, 0, 1234567, CancellationToken.None));
+ completed = true;
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ response.EnsureSuccessStatusCode();
+ Assert.True(completed);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_Count0_Chunked()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ return sendFile.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.Value);
+ Assert.Empty((await response.Content.ReadAsByteArrayAsync()));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_ContentLength_PassedThrough()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ httpContext.Response.Headers["Content-lenGth"] = FileLength.ToString();
+ return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.Equal(FileLength.ToString(), contentLength.First());
+ Assert.Null(response.Headers.TransferEncodingChunked);
+ Assert.Equal(FileLength, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_ContentLengthSpecific_PassedThrough()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ httpContext.Response.Headers["Content-lenGth"] = "10";
+ return sendFile.SendFileAsync(AbsoluteFilePath, 0, 10, CancellationToken.None);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.Equal("10", contentLength.First());
+ Assert.Null(response.Headers.TransferEncodingChunked);
+ Assert.Equal(10, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_ContentLength0_PassedThrough()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ httpContext.Response.Headers["Content-lenGth"] = "0";
+ return sendFile.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ IEnumerable<string> contentLength;
+ Assert.True(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
+ Assert.Equal("0", contentLength.First());
+ Assert.Null(response.Headers.TransferEncodingChunked);
+ Assert.Empty((await response.Content.ReadAsByteArrayAsync()));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task ResponseSendFile_TriggersOnStarting()
+ {
+ var onStartingCalled = false;
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.OnStarting(state =>
+ {
+ onStartingCalled = true;
+ Assert.Same(state, httpContext);
+ return Task.FromResult(0);
+ }, httpContext);
+ var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
+ return sendFile.SendFileAsync(AbsoluteFilePath, 0, 10, CancellationToken.None);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.True(onStartingCalled);
+ IEnumerable<string> ignored;
+ Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
+ Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
+ Assert.Equal(10, (await response.Content.ReadAsByteArrayAsync()).Length);
+ }
+ }
+
+ private async Task<HttpResponseMessage> SendRequestAsync(string uri)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetAsync(uri);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseTests.cs
new file mode 100644
index 0000000000..f2d244edce
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ResponseTests.cs
@@ -0,0 +1,223 @@
+// 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 System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class ResponseTests
+ {
+ [ConditionalFact]
+ public async Task Response_ServerSendsDefaultResponse_ServerProvidesStatusCodeAndReasonPhrase()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ Assert.Equal(200, httpContext.Response.StatusCode);
+ Assert.False(httpContext.Response.HasStarted);
+ return Task.FromResult(0);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ Assert.Equal(200, (int)response.StatusCode);
+ Assert.Equal("OK", response.ReasonPhrase);
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Response_ServerSendsSpecificStatus_ServerProvidesReasonPhrase()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.StatusCode = 201;
+ // TODO: httpContext["owin.ResponseProtocol"] = "HTTP/1.0"; // Http.Sys ignores this value
+ return Task.FromResult(0);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ Assert.Equal(201, (int)response.StatusCode);
+ Assert.Equal("Created", response.ReasonPhrase);
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Response_ServerSendsSpecificStatusAndReasonPhrase_PassedThrough()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.StatusCode = 201;
+ httpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = "CustomReasonPhrase"; // TODO?
+ // TODO: httpContext["owin.ResponseProtocol"] = "HTTP/1.0"; // Http.Sys ignores this value
+ return Task.FromResult(0);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ Assert.Equal(201, (int)response.StatusCode);
+ Assert.Equal("CustomReasonPhrase", response.ReasonPhrase);
+ Assert.Equal(new Version(1, 1), response.Version);
+ Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Response_ServerSendsCustomStatus_NoReasonPhrase()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.StatusCode = 901;
+ return Task.FromResult(0);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ Assert.Equal(901, (int)response.StatusCode);
+ Assert.Equal(string.Empty, response.ReasonPhrase);
+ Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Response_StatusCode100_Throws()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.StatusCode = 100;
+ return Task.FromResult(0);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ Assert.Equal(500, (int)response.StatusCode);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Response_StatusCode0_Throws()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.StatusCode = 0;
+ return Task.FromResult(0);
+ }))
+ {
+ HttpResponseMessage response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Response_Empty_CallsOnStartingAndOnCompleted()
+ {
+ var onStartingCalled = new ManualResetEvent(false);
+ var onCompletedCalled = new ManualResetEvent(false);
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.OnStarting(state =>
+ {
+ Assert.Same(state, httpContext);
+ onStartingCalled.Set();
+ return Task.FromResult(0);
+ }, httpContext);
+ httpContext.Response.OnCompleted(state =>
+ {
+ Assert.Same(state, httpContext);
+ onCompletedCalled.Set();
+ return Task.FromResult(0);
+ }, httpContext);
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.True(onStartingCalled.WaitOne(0));
+ // Fires after the response completes
+ Assert.True(onCompletedCalled.WaitOne(TimeSpan.FromSeconds(5)));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Response_OnStartingThrows_StillCallsOnCompleted()
+ {
+ var onStartingCalled = new ManualResetEvent(false);
+ var onCompletedCalled = new ManualResetEvent(false);
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.OnStarting(state =>
+ {
+ onStartingCalled.Set();
+ throw new Exception("Failed OnStarting");
+ }, httpContext);
+ httpContext.Response.OnCompleted(state =>
+ {
+ Assert.Same(state, httpContext);
+ onCompletedCalled.Set();
+ return Task.FromResult(0);
+ }, httpContext);
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ Assert.True(onStartingCalled.WaitOne(0));
+ // Fires after the response completes
+ Assert.True(onCompletedCalled.WaitOne(TimeSpan.FromSeconds(5)));
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Response_OnStartingThrowsAfterWrite_WriteThrowsAndStillCallsOnCompleted()
+ {
+ var onStartingCalled = new ManualResetEvent(false);
+ var onCompletedCalled = new ManualResetEvent(false);
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.OnStarting(state =>
+ {
+ onStartingCalled.Set();
+ throw new InvalidTimeZoneException("Failed OnStarting");
+ }, httpContext);
+ httpContext.Response.OnCompleted(state =>
+ {
+ Assert.Same(state, httpContext);
+ onCompletedCalled.Set();
+ return Task.FromResult(0);
+ }, httpContext);
+ Assert.Throws<InvalidTimeZoneException>(() => httpContext.Response.Body.Write(new byte[10], 0, 10));
+ return Task.FromResult(0);
+ }))
+ {
+ var response = await SendRequestAsync(address);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.True(onStartingCalled.WaitOne(0));
+ // Fires after the response completes
+ Assert.True(onCompletedCalled.WaitOne(TimeSpan.FromSeconds(5)));
+ }
+ }
+
+ private async Task<HttpResponseMessage> SendRequestAsync(string uri)
+ {
+ using (var client = new HttpClient())
+ {
+ return await client.GetAsync(uri);
+ }
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs
new file mode 100644
index 0000000000..e0ecbc7d2b
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/ServerTests.cs
@@ -0,0 +1,695 @@
+// 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.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class ServerTests
+ {
+ [ConditionalFact]
+ public async Task Server_200OK_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(address);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_SendHelloWorld_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ httpContext.Response.ContentLength = 11;
+ return httpContext.Response.WriteAsync("Hello World");
+ }))
+ {
+ string response = await SendRequestAsync(address);
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_EchoHelloWorld_Success()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ var input = await new StreamReader(httpContext.Request.Body).ReadToEndAsync();
+ Assert.Equal("Hello World", input);
+ httpContext.Response.ContentLength = 11;
+ await httpContext.Response.WriteAsync("Hello World");
+ }))
+ {
+ string response = await SendRequestAsync(address, "Hello World");
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_ShutdownDuringRequest_Success()
+ {
+ Task<string> responseTask;
+ ManualResetEvent received = new ManualResetEvent(false);
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ received.Set();
+ httpContext.Response.ContentLength = 11;
+ return httpContext.Response.WriteAsync("Hello World");
+ }))
+ {
+ responseTask = SendRequestAsync(address);
+ Assert.True(received.WaitOne(10000));
+ await server.StopAsync(new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);
+ }
+ string response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+
+ [ConditionalFact]
+ public async Task Server_DisposeWithoutStopDuringRequest_Aborts()
+ {
+ Task<string> responseTask;
+ var received = new ManualResetEvent(false);
+ var stopped = new ManualResetEvent(false);
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ received.Set();
+ Assert.True(stopped.WaitOne(TimeSpan.FromSeconds(10)));
+ httpContext.Response.ContentLength = 11;
+ return httpContext.Response.WriteAsync("Hello World");
+ }))
+ {
+ responseTask = SendRequestAsync(address);
+ Assert.True(received.WaitOne(TimeSpan.FromSeconds(10)));
+ }
+ stopped.Set();
+ await Assert.ThrowsAsync<HttpRequestException>(async () => await responseTask);
+ }
+
+ [ConditionalFact]
+ public async Task Server_ShutdownDuringLongRunningRequest_TimesOut()
+ {
+ Task<string> responseTask;
+ var received = new ManualResetEvent(false);
+ bool? shutdown = null;
+ var waitForShutdown = new ManualResetEvent(false);
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ received.Set();
+ shutdown = waitForShutdown.WaitOne(TimeSpan.FromSeconds(15));
+ httpContext.Response.ContentLength = 11;
+ return httpContext.Response.WriteAsync("Hello World");
+ }))
+ {
+ responseTask = SendRequestAsync(address);
+ Assert.True(received.WaitOne(TimeSpan.FromSeconds(10)));
+ Assert.False(shutdown.HasValue);
+ await server.StopAsync(new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);
+ }
+ waitForShutdown.Set();
+ await Assert.ThrowsAsync<HttpRequestException>(async () => await responseTask);
+ }
+
+ [ConditionalFact]
+ public async Task Server_AppException_ClientReset()
+ {
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ throw new InvalidOperationException();
+ }))
+ {
+ Task<string> requestTask = SendRequestAsync(address);
+ await Assert.ThrowsAsync<HttpRequestException>(async () => await requestTask);
+
+ // Do it again to make sure the server didn't crash
+ requestTask = SendRequestAsync(address);
+ await Assert.ThrowsAsync<HttpRequestException>(async () => await requestTask);
+ }
+ }
+
+ [ConditionalFact]
+ public void Server_MultipleOutstandingSyncRequests_Success()
+ {
+ int requestLimit = 10;
+ int requestCount = 0;
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
+
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ if (Interlocked.Increment(ref requestCount) == requestLimit)
+ {
+ tcs.TrySetResult(null);
+ }
+ else
+ {
+ tcs.Task.Wait();
+ }
+
+ return Task.FromResult(0);
+ }))
+ {
+ List<Task> requestTasks = new List<Task>();
+ for (int i = 0; i < requestLimit; i++)
+ {
+ Task<string> requestTask = SendRequestAsync(address);
+ requestTasks.Add(requestTask);
+ }
+
+ Assert.True(Task.WaitAll(requestTasks.ToArray(), TimeSpan.FromSeconds(60)), "Timed out");
+ }
+ }
+
+ [ConditionalFact]
+ public void Server_MultipleOutstandingAsyncRequests_Success()
+ {
+ int requestLimit = 10;
+ int requestCount = 0;
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
+
+ string address;
+ using (Utilities.CreateHttpServer(out address, async httpContext =>
+ {
+ if (Interlocked.Increment(ref requestCount) == requestLimit)
+ {
+ tcs.TrySetResult(null);
+ }
+ else
+ {
+ await tcs.Task;
+ }
+ }))
+ {
+ List<Task> requestTasks = new List<Task>();
+ for (int i = 0; i < requestLimit; i++)
+ {
+ Task<string> requestTask = SendRequestAsync(address);
+ requestTasks.Add(requestTask);
+ }
+ Assert.True(Task.WaitAll(requestTasks.ToArray(), TimeSpan.FromSeconds(60)), "Timed out");
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_ClientDisconnects_CallCanceled()
+ {
+ TimeSpan interval = TimeSpan.FromSeconds(10);
+ ManualResetEvent received = new ManualResetEvent(false);
+ ManualResetEvent aborted = new ManualResetEvent(false);
+ ManualResetEvent canceled = new ManualResetEvent(false);
+
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ CancellationToken ct = httpContext.RequestAborted;
+ Assert.True(ct.CanBeCanceled, "CanBeCanceled");
+ Assert.False(ct.IsCancellationRequested, "IsCancellationRequested");
+ ct.Register(() => canceled.Set());
+ received.Set();
+ Assert.True(aborted.WaitOne(interval), "Aborted");
+ Assert.True(ct.WaitHandle.WaitOne(interval), "CT Wait");
+ Assert.True(ct.IsCancellationRequested, "IsCancellationRequested");
+ return Task.FromResult(0);
+ }))
+ {
+ // Note: System.Net.Sockets does not RST the connection by default, it just FINs.
+ // Http.Sys's disconnect notice requires a RST.
+ using (var client = await SendHungRequestAsync("GET", address))
+ {
+ Assert.True(received.WaitOne(interval), "Receive Timeout");
+
+ // Force a RST
+ client.LingerState = new LingerOption(true, 0);
+ }
+ aborted.Set();
+ Assert.True(canceled.WaitOne(interval), "canceled");
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_Abort_CallCanceled()
+ {
+ TimeSpan interval = TimeSpan.FromSeconds(100);
+ ManualResetEvent received = new ManualResetEvent(false);
+ ManualResetEvent aborted = new ManualResetEvent(false);
+ ManualResetEvent canceled = new ManualResetEvent(false);
+
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ CancellationToken ct = httpContext.RequestAborted;
+ Assert.True(ct.CanBeCanceled, "CanBeCanceled");
+ Assert.False(ct.IsCancellationRequested, "IsCancellationRequested");
+ ct.Register(() => canceled.Set());
+ received.Set();
+ httpContext.Abort();
+ Assert.True(canceled.WaitOne(interval), "Aborted");
+ Assert.True(ct.IsCancellationRequested, "IsCancellationRequested");
+ return Task.FromResult(0);
+ }))
+ {
+ using (var client = await SendHungRequestAsync("GET", address))
+ {
+ Assert.True(received.WaitOne(interval), "Receive Timeout");
+ Assert.Throws<IOException>(() => client.GetStream().Read(new byte[10], 0, 10));
+ }
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_SetQueueLimit_Success()
+ {
+ // This is just to get a dynamic port
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext => Task.FromResult(0))) { }
+
+ var server = Utilities.CreatePump();
+ server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(address));
+ server.Listener.Options.RequestQueueLimit = 1001;
+
+ using (server)
+ {
+ await server.StartAsync(new DummyApplication(), CancellationToken.None);
+ string response = await SendRequestAsync(address);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_SetHttp503VebosittHittingThrottle_Success()
+ {
+ // This is just to get a dynamic port
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext => Task.FromResult(0))) { }
+
+ var server = Utilities.CreatePump();
+ server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(address));
+ Assert.Null(server.Listener.Options.MaxConnections);
+ server.Listener.Options.MaxConnections = 3;
+ server.Listener.Options.Http503Verbosity = Http503VerbosityLevel.Limited;
+
+ using (server)
+ {
+ await server.StartAsync(new DummyApplication(), CancellationToken.None);
+
+ using (var client1 = await SendHungRequestAsync("GET", address))
+ using (var client2 = await SendHungRequestAsync("GET", address))
+ {
+ using (var client3 = await SendHungRequestAsync("GET", address))
+ {
+ using (HttpClient client4 = new HttpClient())
+ {
+ // Maxed out, refuses connection should return 503
+ HttpResponseMessage response = await client4.GetAsync(address);
+
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
+ }
+ }
+
+ // A connection has been closed, try again.
+ string responseText = await SendRequestAsync(address);
+ Assert.Equal(string.Empty, responseText);
+ }
+ }
+ }
+
+ [ConditionalFact]
+ public void Server_SetConnectionLimitArgumentValidation_Success()
+ {
+ var server = Utilities.CreatePump();
+
+ Assert.Null(server.Listener.Options.MaxConnections);
+ Assert.Throws<ArgumentOutOfRangeException>(() => server.Listener.Options.MaxConnections = -2);
+ Assert.Null(server.Listener.Options.MaxConnections);
+ server.Listener.Options.MaxConnections = null;
+ server.Listener.Options.MaxConnections = 3;
+ }
+
+ [ConditionalFact]
+ public async Task Server_SetConnectionLimit_Success()
+ {
+ // This is just to get a dynamic port
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext => Task.FromResult(0))) { }
+
+ var server = Utilities.CreatePump();
+ server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(address));
+ Assert.Null(server.Listener.Options.MaxConnections);
+ server.Listener.Options.MaxConnections = 3;
+
+ using (server)
+ {
+ await server.StartAsync(new DummyApplication(), CancellationToken.None);
+
+ using (var client1 = await SendHungRequestAsync("GET", address))
+ using (var client2 = await SendHungRequestAsync("GET", address))
+ {
+ using (var client3 = await SendHungRequestAsync("GET", address))
+ {
+ // Maxed out, refuses connection and throws
+ await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
+ }
+
+ // A connection has been closed, try again.
+ string responseText = await SendRequestAsync(address);
+ Assert.Equal(string.Empty, responseText);
+ }
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_SetConnectionLimitChangeAfterStarted_Success()
+ {
+ // This is just to get a dynamic port
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext => Task.FromResult(0))) { }
+
+ var server = Utilities.CreatePump();
+ server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(address));
+ Assert.Null(server.Listener.Options.MaxConnections);
+ server.Listener.Options.MaxConnections = 3;
+
+ using (server)
+ {
+ await server.StartAsync(new DummyApplication(), CancellationToken.None);
+
+ using (var client1 = await SendHungRequestAsync("GET", address))
+ using (var client2 = await SendHungRequestAsync("GET", address))
+ using (var client3 = await SendHungRequestAsync("GET", address))
+ {
+ // Maxed out, refuses connection and throws
+ await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
+
+ server.Listener.Options.MaxConnections = 4;
+
+ string responseText = await SendRequestAsync(address);
+ Assert.Equal(string.Empty, responseText);
+
+ server.Listener.Options.MaxConnections = 2;
+
+ // Maxed out, refuses connection and throws
+ await Assert.ThrowsAsync<HttpRequestException>(() => SendRequestAsync(address));
+ }
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_SetConnectionLimitInfinite_Success()
+ {
+ // This is just to get a dynamic port
+ string address;
+ using (Utilities.CreateHttpServer(out address, httpContext => Task.FromResult(0))) { }
+
+ var server = Utilities.CreatePump();
+ server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(address));
+ server.Listener.Options.MaxConnections = -1; // infinite
+
+ using (server)
+ {
+ await server.StartAsync(new DummyApplication(), CancellationToken.None);
+
+ using (var client1 = await SendHungRequestAsync("GET", address))
+ using (var client2 = await SendHungRequestAsync("GET", address))
+ using (var client3 = await SendHungRequestAsync("GET", address))
+ {
+ // Doesn't max out
+ string responseText = await SendRequestAsync(address);
+ Assert.Equal(string.Empty, responseText);
+ }
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_MultipleStopAsyncCallsWaitForRequestsToDrain_Success()
+ {
+ Task<string> responseTask;
+ ManualResetEvent received = new ManualResetEvent(false);
+ ManualResetEvent run = new ManualResetEvent(false);
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ received.Set();
+ Assert.True(run.WaitOne(TimeSpan.FromSeconds(10)));
+ httpContext.Response.ContentLength = 11;
+ return httpContext.Response.WriteAsync("Hello World");
+ }))
+ {
+ responseTask = SendRequestAsync(address);
+ Assert.True(received.WaitOne(TimeSpan.FromSeconds(10)));
+
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+ var stopTask1 = server.StopAsync(cts.Token);
+ var stopTask2 = server.StopAsync(cts.Token);
+ var stopTask3 = server.StopAsync(cts.Token);
+
+ Assert.False(stopTask1.IsCompleted);
+ Assert.False(stopTask2.IsCompleted);
+ Assert.False(stopTask3.IsCompleted);
+
+ run.Set();
+
+ await Task.WhenAll(stopTask1, stopTask2, stopTask3).TimeoutAfter(TimeSpan.FromSeconds(10));
+ }
+ string response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+
+ [ConditionalFact]
+ public async Task Server_MultipleStopAsyncCallsCompleteOnCancellation_SameToken_Success()
+ {
+ Task<string> responseTask;
+ ManualResetEvent received = new ManualResetEvent(false);
+ ManualResetEvent run = new ManualResetEvent(false);
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ received.Set();
+ Assert.True(run.WaitOne(TimeSpan.FromSeconds(10)));
+ httpContext.Response.ContentLength = 11;
+ return httpContext.Response.WriteAsync("Hello World");
+ }))
+ {
+ responseTask = SendRequestAsync(address);
+ Assert.True(received.WaitOne(TimeSpan.FromSeconds(10)));
+
+ var cts = new CancellationTokenSource();
+ var stopTask1 = server.StopAsync(cts.Token);
+ var stopTask2 = server.StopAsync(cts.Token);
+ var stopTask3 = server.StopAsync(cts.Token);
+
+ Assert.False(stopTask1.IsCompleted);
+ Assert.False(stopTask2.IsCompleted);
+ Assert.False(stopTask3.IsCompleted);
+
+ cts.Cancel();
+
+ await Task.WhenAll(stopTask1, stopTask2, stopTask3).TimeoutAfter(TimeSpan.FromSeconds(10));
+
+ run.Set();
+
+ string response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_MultipleStopAsyncCallsCompleteOnSingleCancellation_FirstToken_Success()
+ {
+ Task<string> responseTask;
+ ManualResetEvent received = new ManualResetEvent(false);
+ ManualResetEvent run = new ManualResetEvent(false);
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ received.Set();
+ Assert.True(run.WaitOne(TimeSpan.FromSeconds(10)));
+ httpContext.Response.ContentLength = 11;
+ return httpContext.Response.WriteAsync("Hello World");
+ }))
+ {
+ responseTask = SendRequestAsync(address);
+ Assert.True(received.WaitOne(TimeSpan.FromSeconds(10)));
+
+ var cts = new CancellationTokenSource();
+ var stopTask1 = server.StopAsync(cts.Token);
+ var stopTask2 = server.StopAsync(new CancellationTokenSource().Token);
+ var stopTask3 = server.StopAsync(new CancellationTokenSource().Token);
+
+ Assert.False(stopTask1.IsCompleted);
+ Assert.False(stopTask2.IsCompleted);
+ Assert.False(stopTask3.IsCompleted);
+
+ cts.Cancel();
+
+ await Task.WhenAll(stopTask1, stopTask2, stopTask3).TimeoutAfter(TimeSpan.FromSeconds(10));
+
+ run.Set();
+
+ string response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_MultipleStopAsyncCallsCompleteOnSingleCancellation_SubsequentToken_Success()
+ {
+ Task<string> responseTask;
+ ManualResetEvent received = new ManualResetEvent(false);
+ ManualResetEvent run = new ManualResetEvent(false);
+ string address;
+ using (var server = Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ received.Set();
+ Assert.True(run.WaitOne(TimeSpan.FromSeconds(10)));
+ httpContext.Response.ContentLength = 11;
+ return httpContext.Response.WriteAsync("Hello World");
+ }))
+ {
+ responseTask = SendRequestAsync(address);
+ Assert.True(received.WaitOne(10000));
+
+ var cts = new CancellationTokenSource();
+ var stopTask1 = server.StopAsync(new CancellationTokenSource().Token);
+ var stopTask2 = server.StopAsync(cts.Token);
+ var stopTask3 = server.StopAsync(new CancellationTokenSource().Token);
+
+ Assert.False(stopTask1.IsCompleted);
+ Assert.False(stopTask2.IsCompleted);
+ Assert.False(stopTask3.IsCompleted);
+
+ cts.Cancel();
+
+ await Task.WhenAll(stopTask1, stopTask2, stopTask3).TimeoutAfter(TimeSpan.FromSeconds(10));
+
+ run.Set();
+
+ string response = await responseTask;
+ Assert.Equal("Hello World", response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Server_DisposeContinuesPendingStopAsyncCalls()
+ {
+ Task<string> responseTask;
+ ManualResetEvent received = new ManualResetEvent(false);
+ ManualResetEvent run = new ManualResetEvent(false);
+ string address;
+ Task stopTask1;
+ Task stopTask2;
+ using (var server = Utilities.CreateHttpServer(out address, httpContext =>
+ {
+ received.Set();
+ Assert.True(run.WaitOne(TimeSpan.FromSeconds(10)));
+ httpContext.Response.ContentLength = 11;
+ return httpContext.Response.WriteAsync("Hello World");
+ }))
+ {
+ responseTask = SendRequestAsync(address);
+ Assert.True(received.WaitOne(TimeSpan.FromSeconds(10)));
+
+ stopTask1 = server.StopAsync(new CancellationTokenSource().Token);
+ stopTask2 = server.StopAsync(new CancellationTokenSource().Token);
+
+ Assert.False(stopTask1.IsCompleted);
+ Assert.False(stopTask2.IsCompleted);
+ }
+
+ await Task.WhenAll(stopTask1, stopTask2).TimeoutAfter(TimeSpan.FromSeconds(10));
+ }
+
+ [ConditionalFact]
+ public async Task Server_StopAsyncCalledWithNoRequests_Success()
+ {
+ using (var server = Utilities.CreateHttpServer(out _, httpContext => Task.CompletedTask))
+ {
+ await server.StopAsync(default(CancellationToken)).TimeoutAfter(TimeSpan.FromSeconds(10));
+ }
+ }
+
+ private async Task<string> SendRequestAsync(string uri)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ return await client.GetStringAsync(uri);
+ }
+ }
+
+ private async Task<string> SendRequestAsync(string uri, string upload)
+ {
+ using (HttpClient client = new HttpClient())
+ {
+ HttpResponseMessage response = await client.PostAsync(uri, new StringContent(upload));
+ response.EnsureSuccessStatusCode();
+ return await response.Content.ReadAsStringAsync();
+ }
+ }
+
+ private async Task<TcpClient> SendHungRequestAsync(string method, string address)
+ {
+ // Connect with a socket
+ Uri uri = new Uri(address);
+ TcpClient client = new TcpClient();
+
+ try
+ {
+ await client.ConnectAsync(uri.Host, uri.Port);
+ NetworkStream stream = client.GetStream();
+
+ // Send an HTTP GET request
+ byte[] requestBytes = BuildGetRequest(method, uri);
+ await stream.WriteAsync(requestBytes, 0, requestBytes.Length);
+
+ return client;
+ }
+ catch (Exception)
+ {
+ ((IDisposable)client).Dispose();
+ throw;
+ }
+ }
+
+ private byte[] BuildGetRequest(string method, Uri uri)
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.Append(method);
+ builder.Append(" ");
+ builder.Append(uri.PathAndQuery);
+ builder.Append(" HTTP/1.1");
+ builder.AppendLine();
+
+ builder.Append("Host: ");
+ builder.Append(uri.Host);
+ builder.Append(':');
+ builder.Append(uri.Port);
+ builder.AppendLine();
+
+ builder.AppendLine();
+ return Encoding.ASCII.GetBytes(builder.ToString());
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Utilities.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Utilities.cs
new file mode 100644
index 0000000000..cecc4e270a
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests/Utilities.cs
@@ -0,0 +1,146 @@
+// 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.Threading;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ internal static class Utilities
+ {
+ // When tests projects are run in parallel, overlapping port ranges can cause a race condition when looking for free
+ // ports during dynamic port allocation.
+ private const int BasePort = 5001;
+ private const int MaxPort = 8000;
+ private static int NextPort = BasePort;
+ private static object PortLock = new object();
+
+ internal static IServer CreateHttpServer(out string baseAddress, RequestDelegate app)
+ {
+ string root;
+ return CreateDynamicHttpServer(string.Empty, out root, out baseAddress, options => { }, app);
+ }
+
+ internal static IServer CreateHttpServer(out string baseAddress, Action<HttpSysOptions> configureOptions, RequestDelegate app)
+ {
+ string root;
+ return CreateDynamicHttpServer(string.Empty, out root, out baseAddress, configureOptions, app);
+ }
+
+ internal static IServer CreateHttpServerReturnRoot(string path, out string root, RequestDelegate app)
+ {
+ string baseAddress;
+ return CreateDynamicHttpServer(path, out root, out baseAddress, options => { }, app);
+ }
+
+ internal static IServer CreateHttpAuthServer(AuthenticationSchemes authType, bool allowAnonymous, out string baseAddress, RequestDelegate app)
+ {
+ string root;
+ return CreateDynamicHttpServer(string.Empty, out root, out baseAddress, options =>
+ {
+ options.Authentication.Schemes = authType;
+ options.Authentication.AllowAnonymous = allowAnonymous;
+ }, app);
+ }
+
+ internal static IWebHost CreateDynamicHost(AuthenticationSchemes authType, bool allowAnonymous, out string root, RequestDelegate app)
+ {
+ return CreateDynamicHost(string.Empty, out root, out var baseAddress, options =>
+ {
+ options.Authentication.Schemes = authType;
+ options.Authentication.AllowAnonymous = allowAnonymous;
+ }, app);
+ }
+
+ internal static IWebHost CreateDynamicHost(string basePath, out string root, out string baseAddress, Action<HttpSysOptions> configureOptions, RequestDelegate app)
+ {
+ lock (PortLock)
+ {
+ while (NextPort < MaxPort)
+ {
+ var port = NextPort++;
+ var prefix = UrlPrefix.Create("http", "localhost", port, basePath);
+ root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
+ baseAddress = prefix.ToString();
+
+ var builder = new WebHostBuilder()
+ .UseHttpSys(options =>
+ {
+ options.UrlPrefixes.Add(prefix);
+ configureOptions(options);
+ })
+ .Configure(appBuilder => appBuilder.Run(app));
+
+ var host = builder.Build();
+
+
+ try
+ {
+ host.Start();
+ return host;
+ }
+ catch (HttpSysException)
+ {
+ }
+
+ }
+ NextPort = BasePort;
+ }
+ throw new Exception("Failed to locate a free port.");
+ }
+
+ internal static MessagePump CreatePump()
+ => new MessagePump(Options.Create(new HttpSysOptions()), new LoggerFactory(), new AuthenticationSchemeProvider(Options.Create(new AuthenticationOptions())));
+
+ internal static IServer CreateDynamicHttpServer(string basePath, out string root, out string baseAddress, Action<HttpSysOptions> configureOptions, RequestDelegate app)
+ {
+ lock (PortLock)
+ {
+ while (NextPort < MaxPort)
+ {
+
+ var port = NextPort++;
+ var prefix = UrlPrefix.Create("http", "localhost", port, basePath);
+ root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
+ baseAddress = prefix.ToString();
+
+ var server = CreatePump();
+ server.Features.Get<IServerAddressesFeature>().Addresses.Add(baseAddress);
+ configureOptions(server.Listener.Options);
+ try
+ {
+ server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
+ return server;
+ }
+ catch (HttpSysException)
+ {
+ }
+ }
+ NextPort = BasePort;
+ }
+ throw new Exception("Failed to locate a free port.");
+ }
+
+ internal static IServer CreateHttpsServer(RequestDelegate app)
+ {
+ return CreateServer("https", "localhost", 9090, string.Empty, app);
+ }
+
+ internal static IServer CreateServer(string scheme, string host, int port, string path, RequestDelegate app)
+ {
+ var server = CreatePump();
+ server.Features.Get<IServerAddressesFeature>().Addresses.Add(UrlPrefix.Create(scheme, host, port, path).ToString());
+ server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
+ return server;
+ }
+ }
+}
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.Tests/Microsoft.AspNetCore.Server.HttpSys.Tests.csproj b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.Tests/Microsoft.AspNetCore.Server.HttpSys.Tests.csproj
new file mode 100644
index 0000000000..5628726d51
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.Tests/Microsoft.AspNetCore.Server.HttpSys.Tests.csproj
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Server.HttpSys\Microsoft.AspNetCore.Server.HttpSys.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.Tests/UrlPrefixTests.cs b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.Tests/UrlPrefixTests.cs
new file mode 100644
index 0000000000..8614ac36db
--- /dev/null
+++ b/src/HttpSysServer/test/Microsoft.AspNetCore.Server.HttpSys.Tests/UrlPrefixTests.cs
@@ -0,0 +1,85 @@
+// 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.Text;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ public class UrlPrefixTests
+ {
+ [Theory]
+ [InlineData("")]
+ [InlineData("5000")]
+ [InlineData("//noscheme")]
+ public void CreateThrowsForUrlsWithoutSchemeDelimiter(string url)
+ {
+ Assert.Throws<FormatException>(() => UrlPrefix.Create(url));
+ }
+
+ [Theory]
+ [InlineData("://emptyscheme")]
+ [InlineData("://")]
+ [InlineData("://:5000")]
+ public void CreateThrowsForUrlsWithEmptyScheme(string url)
+ {
+ Assert.Throws<ArgumentOutOfRangeException>(() => UrlPrefix.Create(url));
+ }
+
+ [Theory]
+ [InlineData("http://")]
+ [InlineData("http://:5000")]
+ [InlineData("http:///")]
+ [InlineData("http:///:5000")]
+ [InlineData("http:////")]
+ [InlineData("http:////:5000")]
+ public void CreateThrowsForUrlsWithoutHost(string url)
+ {
+ Assert.Throws<ArgumentNullException>(() => UrlPrefix.Create(url));
+ }
+
+ [Theory]
+ [InlineData("http://www.example.com:NOTAPORT")]
+ [InlineData("https://www.example.com:NOTAPORT")]
+ [InlineData("http://www.example.com:NOTAPORT/")]
+ [InlineData("http://foo:/tmp/httpsys-test.sock:5000/doesn't/matter")]
+ public void CreateThrowsForUrlsWithInvalidPorts(string url)
+ {
+ Assert.Throws<FormatException>(() => UrlPrefix.Create(url));
+ }
+
+ [Theory]
+ [InlineData("http://+", "http", "+", "80", "/", "http://+:80/")]
+ [InlineData("http://*", "http", "*", "80", "/", "http://*:80/")]
+ [InlineData("http://localhost", "http", "localhost", "80", "/", "http://localhost:80/")]
+ [InlineData("http://www.example.com", "http", "www.example.com", "80", "/", "http://www.example.com:80/")]
+ [InlineData("https://www.example.com", "https", "www.example.com", "443", "/", "https://www.example.com:443/")]
+ [InlineData("http://www.example.com/", "http", "www.example.com", "80", "/", "http://www.example.com:80/")]
+ [InlineData("http://www.example.com/foo?bar=baz", "http", "www.example.com", "80", "/foo?bar=baz/", "http://www.example.com:80/foo?bar=baz/")]
+ [InlineData("http://www.example.com:5000", "http", "www.example.com", "5000", "/", "http://www.example.com:5000/")]
+ [InlineData("https://www.example.com:5000", "https", "www.example.com", "5000", "/", "https://www.example.com:5000/")]
+ [InlineData("http://www.example.com:5000/", "http", "www.example.com", "5000", "/", "http://www.example.com:5000/")]
+ [InlineData("http://www.example.com/foo:bar", "http", "www.example.com", "80", "/foo:bar/", "http://www.example.com:80/foo:bar/")]
+ public void UrlsAreParsedCorrectly(string url, string scheme, string host, string port, string pathBase, string toString)
+ {
+ var urlPrefix = UrlPrefix.Create(url);
+
+ Assert.Equal(scheme, urlPrefix.Scheme);
+ Assert.Equal(host, urlPrefix.Host);
+ Assert.Equal(port, urlPrefix.Port);
+ Assert.Equal(pathBase, urlPrefix.Path);
+
+ Assert.Equal(toString ?? url, urlPrefix.ToString());
+ }
+
+ [Fact]
+ public void PathBaseIsNotNormalized()
+ {
+ var urlPrefix = UrlPrefix.Create("http://localhost:8080/p\u0041\u030Athbase");
+
+ Assert.False(urlPrefix.Path.IsNormalized(NormalizationForm.FormC));
+ Assert.Equal("/p\u0041\u030Athbase/", urlPrefix.Path);
+ }
+ }
+}
diff --git a/src/HttpSysServer/version.props b/src/HttpSysServer/version.props
new file mode 100644
index 0000000000..669c874829
--- /dev/null
+++ b/src/HttpSysServer/version.props
@@ -0,0 +1,12 @@
+<Project>
+ <PropertyGroup>
+ <VersionPrefix>2.1.1</VersionPrefix>
+ <VersionSuffix>rtm</VersionSuffix>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' == 'rtm' ">$(VersionPrefix)</PackageVersion>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' != 'rtm' ">$(VersionPrefix)-$(VersionSuffix)-final</PackageVersion>
+ <BuildNumber Condition="'$(BuildNumber)' == ''">t000</BuildNumber>
+ <FeatureBranchVersionPrefix Condition="'$(FeatureBranchVersionPrefix)' == ''">a-</FeatureBranchVersionPrefix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
+ </PropertyGroup>
+</Project>
diff --git a/src/Installers/Windows/AspNetCoreModule-Setup/Directory.Build.props b/src/Installers/Windows/AspNetCoreModule-Setup/Directory.Build.props
index f6801907b6..9022db40f3 100644
--- a/src/Installers/Windows/AspNetCoreModule-Setup/Directory.Build.props
+++ b/src/Installers/Windows/AspNetCoreModule-Setup/Directory.Build.props
@@ -29,7 +29,7 @@
<!-- Variables used by ANCM wxs projects. -->
<CustomActionVariable>CustomAction=$(AspNetCoreSetupRoot)CustomAction\bin\$(Configuration)\$(Platform)\aspnetcoreca.dll</CustomActionVariable>
- <PreBuiltANCMSchema>$(RepositoryRoot).deps\ANCM\Microsoft.AspNetCore.AspNetCoreModule\$(MicrosoftAspNetCoreAspNetCoreModulePackageVersion)\</PreBuiltANCMSchema>
+ <PreBuiltANCMSchema>$(RepositoryRoot).deps\ANCM\Microsoft.AspNetCore.AspNetCoreModuleV1\$(MicrosoftAspNetCoreAspNetCoreModuleV1PackageVersion)\</PreBuiltANCMSchema>
<PreBuiltANCMV2Schema>$(RepositoryRoot).deps\ANCM\Microsoft.AspNetCore.AspNetCoreModuleV2\$(MicrosoftAspNetCoreAspNetCoreModuleV2PackageVersion)\</PreBuiltANCMV2Schema>
<PreBuiltANCMRoot>$(PreBuiltANCMSchema)contentFiles\any\any\</PreBuiltANCMRoot>
<PreBuiltANCMV2Root>$(PreBuiltANCMV2Schema)contentFiles\any\any\</PreBuiltANCMV2Root>
diff --git a/src/JavaScriptServices/Directory.Build.props b/src/JavaScriptServices/Directory.Build.props
index 2348ddb382..eaf3f3cfaa 100644
--- a/src/JavaScriptServices/Directory.Build.props
+++ b/src/JavaScriptServices/Directory.Build.props
@@ -5,7 +5,7 @@
<PropertyGroup>
<Product>Microsoft ASP.NET Core</Product>
- <RepositoryUrl>https://github.com/aspnet/javascriptservices</RepositoryUrl>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)..\..\eng\AspNetCore.snk</AssemblyOriginatorKeyFile>
diff --git a/src/MetaPackages/Directory.Build.props b/src/MetaPackages/Directory.Build.props
index d393189ce3..7c614d7224 100644
--- a/src/MetaPackages/Directory.Build.props
+++ b/src/MetaPackages/Directory.Build.props
@@ -9,7 +9,7 @@
<PropertyGroup>
<Product>Microsoft ASP.NET Core</Product>
- <RepositoryUrl>https://github.com/aspnet/MetaPackages</RepositoryUrl>
+ <RepositoryUrl>https://github.com/aspnet/AspnetCore</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
<GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute>
diff --git a/src/ResponseCaching/.gitignore b/src/ResponseCaching/.gitignore
new file mode 100644
index 0000000000..23826aae91
--- /dev/null
+++ b/src/ResponseCaching/.gitignore
@@ -0,0 +1,33 @@
+[Oo]bj/
+[Bb]in/
+TestResults/
+.nuget/
+_ReSharper.*/
+packages/
+artifacts/
+PublishProfiles/
+*.user
+*.suo
+*.cache
+*.docstates
+_ReSharper.*
+nuget.exe
+*net45.csproj
+*net451.csproj
+*k10.csproj
+*.psess
+*.vsp
+*.pidb
+*.userprefs
+*DS_Store
+*.ncrunchsolution
+*.*sdf
+*.ipch
+*.sln.ide
+project.lock.json
+/.vs/
+.vscode/
+.build/
+.testPublish/
+launchSettings.json
+global.json
diff --git a/src/ResponseCaching/Directory.Build.props b/src/ResponseCaching/Directory.Build.props
new file mode 100644
index 0000000000..2883c9cc0a
--- /dev/null
+++ b/src/ResponseCaching/Directory.Build.props
@@ -0,0 +1,21 @@
+<Project>
+ <Import
+ Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))\AspNetCoreSettings.props"
+ Condition=" '$(CI)' != 'true' AND '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))' != '' " />
+
+ <Import Project="version.props" />
+ <Import Project="build\dependencies.props" />
+ <Import Project="build\sources.props" />
+
+ <PropertyGroup>
+ <Product>Microsoft ASP.NET Core</Product>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
+ <RepositoryType>git</RepositoryType>
+ <RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
+ <AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)build\Key.snk</AssemblyOriginatorKeyFile>
+ <SignAssembly>true</SignAssembly>
+ <PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+
+</Project>
diff --git a/src/ResponseCaching/Directory.Build.targets b/src/ResponseCaching/Directory.Build.targets
new file mode 100644
index 0000000000..53b3f6e1da
--- /dev/null
+++ b/src/ResponseCaching/Directory.Build.targets
@@ -0,0 +1,7 @@
+<Project>
+ <PropertyGroup>
+ <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">$(MicrosoftNETCoreApp20PackageVersion)</RuntimeFrameworkVersion>
+ <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">$(MicrosoftNETCoreApp21PackageVersion)</RuntimeFrameworkVersion>
+ <NETStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard2.0' ">$(NETStandardLibrary20PackageVersion)</NETStandardImplicitPackageVersion>
+ </PropertyGroup>
+</Project>
diff --git a/src/ResponseCaching/NuGetPackageVerifier.json b/src/ResponseCaching/NuGetPackageVerifier.json
new file mode 100644
index 0000000000..b153ab1515
--- /dev/null
+++ b/src/ResponseCaching/NuGetPackageVerifier.json
@@ -0,0 +1,7 @@
+{
+ "Default": {
+ "rules": [
+ "DefaultCompositeRule"
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/ResponseCaching/README.md b/src/ResponseCaching/README.md
new file mode 100644
index 0000000000..5c6ea8d9c8
--- /dev/null
+++ b/src/ResponseCaching/README.md
@@ -0,0 +1,9 @@
+ASP.NET Core Response Caching
+========
+AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/p52yj0kghdyicvwu/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/ResponseCaching/branch/dev)
+
+Travis: [![Travis](https://travis-ci.org/aspnet/ResponseCaching.svg?branch=dev)](https://travis-ci.org/aspnet/ResponseCaching)
+
+This repo hosts the ASP.NET Core middleware for response caching.
+
+This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
diff --git a/src/ResponseCaching/ResponseCaching.sln b/src/ResponseCaching/ResponseCaching.sln
new file mode 100644
index 0000000000..69a5397248
--- /dev/null
+++ b/src/ResponseCaching/ResponseCaching.sln
@@ -0,0 +1,66 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26730.10
+MinimumVisualStudioVersion = 15.0.26730.03
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{367AABAF-E03C-4491-A9A7-BDDE8903D1B4}"
+ ProjectSection(SolutionItems) = preProject
+ src\Directory.Build.props = src\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{C51DF5BD-B53D-4795-BC01-A9AB066BF286}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{89A50974-E9D4-4F87-ACF2-6A6005E64931}"
+ ProjectSection(SolutionItems) = preProject
+ test\Directory.Build.props = test\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResponseCachingSample", "samples\ResponseCachingSample\ResponseCachingSample.csproj", "{1139BDEE-FA15-474D-8855-0AB91F23CF26}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching.Tests", "test\Microsoft.AspNetCore.ResponseCaching.Tests\Microsoft.AspNetCore.ResponseCaching.Tests.csproj", "{151B2027-3936-44B9-A4A0-E1E5902125AB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching", "src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.ResponseCaching.csproj", "{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching.Abstractions", "src\Microsoft.AspNetCore.ResponseCaching.Abstractions\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "{2D1022E8-CBB6-478D-A420-CB888D0EF7B7}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B984DDCF-0D61-44C4-9D30-2BC59EE6BD29}"
+ ProjectSection(SolutionItems) = preProject
+ Directory.Build.props = Directory.Build.props
+ Directory.Build.targets = Directory.Build.targets
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {1139BDEE-FA15-474D-8855-0AB91F23CF26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1139BDEE-FA15-474D-8855-0AB91F23CF26}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1139BDEE-FA15-474D-8855-0AB91F23CF26}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1139BDEE-FA15-474D-8855-0AB91F23CF26}.Release|Any CPU.Build.0 = Release|Any CPU
+ {151B2027-3936-44B9-A4A0-E1E5902125AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {151B2027-3936-44B9-A4A0-E1E5902125AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {151B2027-3936-44B9-A4A0-E1E5902125AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {151B2027-3936-44B9-A4A0-E1E5902125AB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2D1022E8-CBB6-478D-A420-CB888D0EF7B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2D1022E8-CBB6-478D-A420-CB888D0EF7B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2D1022E8-CBB6-478D-A420-CB888D0EF7B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2D1022E8-CBB6-478D-A420-CB888D0EF7B7}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {1139BDEE-FA15-474D-8855-0AB91F23CF26} = {C51DF5BD-B53D-4795-BC01-A9AB066BF286}
+ {151B2027-3936-44B9-A4A0-E1E5902125AB} = {89A50974-E9D4-4F87-ACF2-6A6005E64931}
+ {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6} = {367AABAF-E03C-4491-A9A7-BDDE8903D1B4}
+ {2D1022E8-CBB6-478D-A420-CB888D0EF7B7} = {367AABAF-E03C-4491-A9A7-BDDE8903D1B4}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {6F6B4994-06D7-4D35-B0F7-F60913AA8402}
+ EndGlobalSection
+EndGlobal
diff --git a/src/ResponseCaching/build/Key.snk b/src/ResponseCaching/build/Key.snk
new file mode 100644
index 0000000000..e10e4889c1
--- /dev/null
+++ b/src/ResponseCaching/build/Key.snk
Binary files differ
diff --git a/src/ResponseCaching/build/dependencies.props b/src/ResponseCaching/build/dependencies.props
new file mode 100644
index 0000000000..6735646e70
--- /dev/null
+++ b/src/ResponseCaching/build/dependencies.props
@@ -0,0 +1,32 @@
+<Project>
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ </PropertyGroup>
+
+ <!-- These package versions may be overridden or updated by automation. -->
+ <PropertyGroup Label="Package Versions: Auto">
+ <InternalAspNetCoreSdkPackageVersion>2.1.3-rtm-15802</InternalAspNetCoreSdkPackageVersion>
+ <MicrosoftNETCoreApp20PackageVersion>2.0.0</MicrosoftNETCoreApp20PackageVersion>
+ <MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion>
+ <MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
+ <NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
+ <XunitPackageVersion>2.3.1</XunitPackageVersion>
+ <XunitRunnerVisualStudioPackageVersion>2.4.0-beta.1.build3945</XunitRunnerVisualStudioPackageVersion>
+ </PropertyGroup>
+
+ <!-- This may import a generated file which may override the variables above. -->
+ <Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
+
+ <!-- These are package versions that should not be overridden or updated by automation. -->
+ <PropertyGroup Label="Package Versions: Pinned">
+ <MicrosoftAspNetCoreHttpExtensionsPackageVersion>2.1.1</MicrosoftAspNetCoreHttpExtensionsPackageVersion>
+ <MicrosoftAspNetCoreHttpPackageVersion>2.1.1</MicrosoftAspNetCoreHttpPackageVersion>
+ <MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.1.1</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
+ <MicrosoftAspNetCoreServerKestrelPackageVersion>2.1.2</MicrosoftAspNetCoreServerKestrelPackageVersion>
+ <MicrosoftAspNetCoreTestHostPackageVersion>2.1.1</MicrosoftAspNetCoreTestHostPackageVersion>
+ <MicrosoftExtensionsCachingMemoryPackageVersion>2.1.1</MicrosoftExtensionsCachingMemoryPackageVersion>
+ <MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
+ <MicrosoftExtensionsLoggingTestingPackageVersion>2.1.1</MicrosoftExtensionsLoggingTestingPackageVersion>
+ <MicrosoftExtensionsPrimitivesPackageVersion>2.1.1</MicrosoftExtensionsPrimitivesPackageVersion>
+ </PropertyGroup>
+</Project> \ No newline at end of file
diff --git a/src/ResponseCaching/build/repo.props b/src/ResponseCaching/build/repo.props
new file mode 100644
index 0000000000..dab1601c88
--- /dev/null
+++ b/src/ResponseCaching/build/repo.props
@@ -0,0 +1,15 @@
+<Project>
+ <Import Project="dependencies.props" />
+
+ <PropertyGroup>
+ <!-- These properties are use by the automation that updates dependencies.props -->
+ <LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
+ <LineupPackageVersion>2.1.0-rc1-*</LineupPackageVersion>
+ <LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp20PackageVersion)" />
+ <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/ResponseCaching/build/sources.props b/src/ResponseCaching/build/sources.props
new file mode 100644
index 0000000000..9215df9751
--- /dev/null
+++ b/src/ResponseCaching/build/sources.props
@@ -0,0 +1,17 @@
+<Project>
+ <Import Project="$(DotNetRestoreSourcePropsPath)" Condition="'$(DotNetRestoreSourcePropsPath)' != ''"/>
+
+ <PropertyGroup Label="RestoreSources">
+ <RestoreSources>$(DotNetRestoreSources)</RestoreSources>
+ <RestoreSources Condition="'$(DotNetBuildOffline)' != 'true' AND '$(AspNetUniverseBuildOffline)' != 'true' ">
+ $(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>
+ <RestoreSources Condition="'$(DotNetBuildOffline)' != 'true'">
+ $(RestoreSources);
+ https://api.nuget.org/v3/index.json;
+ </RestoreSources>
+ </PropertyGroup>
+</Project>
diff --git a/src/ResponseCaching/samples/ResponseCachingSample/README.md b/src/ResponseCaching/samples/ResponseCachingSample/README.md
new file mode 100644
index 0000000000..08583fda40
--- /dev/null
+++ b/src/ResponseCaching/samples/ResponseCachingSample/README.md
@@ -0,0 +1,6 @@
+ASP.NET Core Response Caching Sample
+===================================
+
+This sample illustrates the usage of ASP.NET Core response caching middleware. The application sends a `Hello World!` message and the current time along with a `Cache-Control` header to configure caching behavior. The application also sends a `Vary` header to configure the cache to serve the response only if the `Accept-Encoding` header of subsequent requests matches that from the original request.
+
+When running the sample, a response will be served from cache when possible and will be stored for up to 10 seconds.
diff --git a/src/ResponseCaching/samples/ResponseCachingSample/ResponseCachingSample.csproj b/src/ResponseCaching/samples/ResponseCachingSample/ResponseCachingSample.csproj
new file mode 100644
index 0000000000..3739141e06
--- /dev/null
+++ b/src/ResponseCaching/samples/ResponseCachingSample/ResponseCachingSample.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp2.1</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.ResponseCaching.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(MicrosoftExtensionsCachingMemoryPackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/ResponseCaching/samples/ResponseCachingSample/Startup.cs b/src/ResponseCaching/samples/ResponseCachingSample/Startup.cs
new file mode 100644
index 0000000000..ca2e7fbcf3
--- /dev/null
+++ b/src/ResponseCaching/samples/ResponseCachingSample/Startup.cs
@@ -0,0 +1,49 @@
+// 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.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+
+namespace ResponseCachingSample
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddResponseCaching();
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseResponseCaching();
+ app.Run(async (context) =>
+ {
+ context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
+ {
+ Public = true,
+ MaxAge = TimeSpan.FromSeconds(10)
+ };
+ context.Response.Headers[HeaderNames.Vary] = new string[] { "Accept-Encoding" };
+
+ await context.Response.WriteAsync("Hello World! " + DateTime.UtcNow);
+ });
+ }
+
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .UseKestrel()
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseStartup<Startup>()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Directory.Build.props b/src/ResponseCaching/src/Directory.Build.props
new file mode 100644
index 0000000000..1e0980f663
--- /dev/null
+++ b/src/ResponseCaching/src/Directory.Build.props
@@ -0,0 +1,7 @@
+<Project>
+ <Import Project="..\Directory.Build.props" />
+
+ <ItemGroup>
+ <PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCachingFeature.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCachingFeature.cs
new file mode 100644
index 0000000000..c68c4c8c5c
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCachingFeature.cs
@@ -0,0 +1,16 @@
+// 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.ResponseCaching
+{
+ /// <summary>
+ /// A feature for configuring additional response cache options on the HTTP response.
+ /// </summary>
+ public interface IResponseCachingFeature
+ {
+ /// <summary>
+ /// Gets or sets the query keys used by the response cache middleware for calculating secondary vary keys.
+ /// </summary>
+ string[] VaryByQueryKeys { get; set; }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj
new file mode 100644
index 0000000000..28f2df9c86
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj
@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core response caching middleware abstractions and feature interface definitions.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;cache;caching</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Primitives" Version="$(MicrosoftExtensionsPrimitivesPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json
new file mode 100644
index 0000000000..f8993e5232
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json
@@ -0,0 +1,34 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.ResponseCaching.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.ResponseCaching.IResponseCachingFeature",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_VaryByQueryKeys",
+ "Parameters": [],
+ "ReturnType": "System.String[]",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_VaryByQueryKeys",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String[]"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs
new file mode 100644
index 0000000000..f23286a77e
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs
@@ -0,0 +1,88 @@
+// 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.Primitives;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ internal static class CacheEntryHelpers
+ {
+
+ internal static long EstimateCachedResponseSize(CachedResponse cachedResponse)
+ {
+ if (cachedResponse == null)
+ {
+ return 0L;
+ }
+
+ checked
+ {
+ // StatusCode
+ long size = sizeof(int);
+
+ // Headers
+ if (cachedResponse.Headers != null)
+ {
+ foreach (var item in cachedResponse.Headers)
+ {
+ size += item.Key.Length * sizeof(char) + EstimateStringValuesSize(item.Value);
+ }
+ }
+
+ // Body
+ if (cachedResponse.Body != null)
+ {
+ size += cachedResponse.Body.Length;
+ }
+
+ return size;
+ }
+ }
+
+ internal static long EstimateCachedVaryByRulesySize(CachedVaryByRules cachedVaryByRules)
+ {
+ if (cachedVaryByRules == null)
+ {
+ return 0L;
+ }
+
+ checked
+ {
+ var size = 0L;
+
+ // VaryByKeyPrefix
+ if (!string.IsNullOrEmpty(cachedVaryByRules.VaryByKeyPrefix))
+ {
+ size = cachedVaryByRules.VaryByKeyPrefix.Length * sizeof(char);
+ }
+
+ // Headers
+ size += EstimateStringValuesSize(cachedVaryByRules.Headers);
+
+ // QueryKeys
+ size += EstimateStringValuesSize(cachedVaryByRules.QueryKeys);
+
+ return size;
+ }
+ }
+
+ internal static long EstimateStringValuesSize(StringValues stringValues)
+ {
+ checked
+ {
+ var size = 0L;
+
+ for (var i = 0; i < stringValues.Count; i++)
+ {
+ var stringValue = stringValues[i];
+ if (!string.IsNullOrEmpty(stringValue))
+ {
+ size += stringValues[i].Length * sizeof(char);
+ }
+ }
+
+ return size;
+ }
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs
new file mode 100644
index 0000000000..62734f8039
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs
@@ -0,0 +1,20 @@
+// 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.Http;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ public class CachedResponse : IResponseCacheEntry
+ {
+ public DateTimeOffset Created { get; set; }
+
+ public int StatusCode { get; set; }
+
+ public IHeaderDictionary Headers { get; set; }
+
+ public Stream Body { get; set; }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedVaryByRules.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedVaryByRules.cs
new file mode 100644
index 0000000000..d183724628
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedVaryByRules.cs
@@ -0,0 +1,16 @@
+// 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.Primitives;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ public class CachedVaryByRules : IResponseCacheEntry
+ {
+ public string VaryByKeyPrefix { get; set; }
+
+ public StringValues Headers { get; set; }
+
+ public StringValues QueryKeys { get; set; }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs
new file mode 100644
index 0000000000..76cac184ae
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs
@@ -0,0 +1,79 @@
+// 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.Threading;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ internal class FastGuid
+ {
+ // Base32 encoding - in ascii sort order for easy text based sorting
+ private static readonly string _encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV";
+ // Global ID
+ private static long NextId;
+
+ // Instance components
+ private string _idString;
+ internal long IdValue { get; private set; }
+
+ internal string IdString
+ {
+ get
+ {
+ if (_idString == null)
+ {
+ _idString = GenerateGuidString(this);
+ }
+ return _idString;
+ }
+ }
+
+ // Static constructor to initialize global components
+ static FastGuid()
+ {
+ var guidBytes = Guid.NewGuid().ToByteArray();
+
+ // Use the first 4 bytes from the Guid to initialize global ID
+ NextId =
+ guidBytes[0] << 32 |
+ guidBytes[1] << 40 |
+ guidBytes[2] << 48 |
+ guidBytes[3] << 56;
+ }
+
+ internal FastGuid(long id)
+ {
+ IdValue = id;
+ }
+
+ internal static FastGuid NewGuid()
+ {
+ return new FastGuid(Interlocked.Increment(ref NextId));
+ }
+
+ private static unsafe string GenerateGuidString(FastGuid guid)
+ {
+ // stackalloc to allocate array on stack rather than heap
+ char* charBuffer = stackalloc char[13];
+
+ // ID
+ charBuffer[0] = _encode32Chars[(int)(guid.IdValue >> 60) & 31];
+ charBuffer[1] = _encode32Chars[(int)(guid.IdValue >> 55) & 31];
+ charBuffer[2] = _encode32Chars[(int)(guid.IdValue >> 50) & 31];
+ charBuffer[3] = _encode32Chars[(int)(guid.IdValue >> 45) & 31];
+ charBuffer[4] = _encode32Chars[(int)(guid.IdValue >> 40) & 31];
+ charBuffer[5] = _encode32Chars[(int)(guid.IdValue >> 35) & 31];
+ charBuffer[6] = _encode32Chars[(int)(guid.IdValue >> 30) & 31];
+ charBuffer[7] = _encode32Chars[(int)(guid.IdValue >> 25) & 31];
+ charBuffer[8] = _encode32Chars[(int)(guid.IdValue >> 20) & 31];
+ charBuffer[9] = _encode32Chars[(int)(guid.IdValue >> 15) & 31];
+ charBuffer[10] = _encode32Chars[(int)(guid.IdValue >> 10) & 31];
+ charBuffer[11] = _encode32Chars[(int)(guid.IdValue >> 5) & 31];
+ charBuffer[12] = _encode32Chars[(int)guid.IdValue & 31];
+
+ // string ctor overload that takes char*
+ return new string(charBuffer, 0, 13);
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs
new file mode 100644
index 0000000000..4b560e3dad
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs
@@ -0,0 +1,18 @@
+// 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.ResponseCaching.Internal
+{
+ /// <summary>
+ /// Abstracts the system clock to facilitate testing.
+ /// </summary>
+ internal interface ISystemClock
+ {
+ /// <summary>
+ /// Retrieves the current system time in UTC.
+ /// </summary>
+ DateTimeOffset UtcNow { get; }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs
new file mode 100644
index 0000000000..41c85b277a
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs
@@ -0,0 +1,17 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ public interface IResponseCache
+ {
+ IResponseCacheEntry Get(string key);
+ Task<IResponseCacheEntry> GetAsync(string key);
+
+ void Set(string key, IResponseCacheEntry entry, TimeSpan validFor);
+ Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor);
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheEntry.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheEntry.cs
new file mode 100644
index 0000000000..a8227fe243
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheEntry.cs
@@ -0,0 +1,9 @@
+// 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.ResponseCaching.Internal
+{
+ public interface IResponseCacheEntry
+ {
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingKeyProvider.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingKeyProvider.cs
new file mode 100644
index 0000000000..ac6a20f005
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingKeyProvider.cs
@@ -0,0 +1,31 @@
+// 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;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ public interface IResponseCachingKeyProvider
+ {
+ /// <summary>
+ /// Create a base key for a response cache entry.
+ /// </summary>
+ /// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
+ /// <returns>The created base key.</returns>
+ string CreateBaseKey(ResponseCachingContext context);
+
+ /// <summary>
+ /// Create a vary key for storing cached responses.
+ /// </summary>
+ /// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
+ /// <returns>The created vary key.</returns>
+ string CreateStorageVaryByKey(ResponseCachingContext context);
+
+ /// <summary>
+ /// Create one or more vary keys for looking up cached responses.
+ /// </summary>
+ /// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
+ /// <returns>An ordered <see cref="IEnumerable{T}"/> containing the vary keys to try when looking up items.</returns>
+ IEnumerable<string> CreateLookupVaryByKeys(ResponseCachingContext context);
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs
new file mode 100644
index 0000000000..51a040098b
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs
@@ -0,0 +1,43 @@
+// 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.ResponseCaching.Internal
+{
+ public interface IResponseCachingPolicyProvider
+ {
+ /// <summary>
+ /// Determine whether the response caching logic should be attempted for the incoming HTTP request.
+ /// </summary>
+ /// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
+ /// <returns><c>true</c> if response caching logic should be attempted; otherwise <c>false</c>.</returns>
+ bool AttemptResponseCaching(ResponseCachingContext context);
+
+ /// <summary>
+ /// Determine whether a cache lookup is allowed for the incoming HTTP request.
+ /// </summary>
+ /// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
+ /// <returns><c>true</c> if cache lookup for this request is allowed; otherwise <c>false</c>.</returns>
+ bool AllowCacheLookup(ResponseCachingContext context);
+
+ /// <summary>
+ /// Determine whether storage of the response is allowed for the incoming HTTP request.
+ /// </summary>
+ /// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
+ /// <returns><c>true</c> if storage of the response for this request is allowed; otherwise <c>false</c>.</returns>
+ bool AllowCacheStorage(ResponseCachingContext context);
+
+ /// <summary>
+ /// Determine whether the response received by the middleware can be cached for future requests.
+ /// </summary>
+ /// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
+ /// <returns><c>true</c> if the response is cacheable; otherwise <c>false</c>.</returns>
+ bool IsResponseCacheable(ResponseCachingContext context);
+
+ /// <summary>
+ /// Determine whether the response retrieved from the response cache is fresh and can be served.
+ /// </summary>
+ /// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
+ /// <returns><c>true</c> if the cached entry is fresh; otherwise <c>false</c>.</returns>
+ bool IsCachedEntryFresh(ResponseCachingContext context);
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs
new file mode 100644
index 0000000000..f8a0bf3151
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs
@@ -0,0 +1,310 @@
+// 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;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ /// <summary>
+ /// Defines *all* the logger messages produced by response caching
+ /// </summary>
+ internal static class LoggerExtensions
+ {
+ private static Action<ILogger, string, Exception> _logRequestMethodNotCacheable;
+ private static Action<ILogger, Exception> _logRequestWithAuthorizationNotCacheable;
+ private static Action<ILogger, Exception> _logRequestWithNoCacheNotCacheable;
+ private static Action<ILogger, Exception> _logRequestWithPragmaNoCacheNotCacheable;
+ private static Action<ILogger, TimeSpan, Exception> _logExpirationMinFreshAdded;
+ private static Action<ILogger, TimeSpan, TimeSpan, Exception> _logExpirationSharedMaxAgeExceeded;
+ private static Action<ILogger, TimeSpan, TimeSpan, Exception> _logExpirationMustRevalidate;
+ private static Action<ILogger, TimeSpan, TimeSpan, TimeSpan, Exception> _logExpirationMaxStaleSatisfied;
+ private static Action<ILogger, TimeSpan, TimeSpan, Exception> _logExpirationMaxAgeExceeded;
+ private static Action<ILogger, DateTimeOffset, DateTimeOffset, Exception> _logExpirationExpiresExceeded;
+ private static Action<ILogger, Exception> _logResponseWithoutPublicNotCacheable;
+ private static Action<ILogger, Exception> _logResponseWithNoStoreNotCacheable;
+ private static Action<ILogger, Exception> _logResponseWithNoCacheNotCacheable;
+ private static Action<ILogger, Exception> _logResponseWithSetCookieNotCacheable;
+ private static Action<ILogger, Exception> _logResponseWithVaryStarNotCacheable;
+ private static Action<ILogger, Exception> _logResponseWithPrivateNotCacheable;
+ private static Action<ILogger, int, Exception> _logResponseWithUnsuccessfulStatusCodeNotCacheable;
+ private static Action<ILogger, Exception> _logNotModifiedIfNoneMatchStar;
+ private static Action<ILogger, EntityTagHeaderValue, Exception> _logNotModifiedIfNoneMatchMatched;
+ private static Action<ILogger, DateTimeOffset, DateTimeOffset, Exception> _logNotModifiedIfModifiedSinceSatisfied;
+ private static Action<ILogger, Exception> _logNotModifiedServed;
+ private static Action<ILogger, Exception> _logCachedResponseServed;
+ private static Action<ILogger, Exception> _logGatewayTimeoutServed;
+ private static Action<ILogger, Exception> _logNoResponseServed;
+ private static Action<ILogger, string, string, Exception> _logVaryByRulesUpdated;
+ private static Action<ILogger, Exception> _logResponseCached;
+ private static Action<ILogger, Exception> _logResponseNotCached;
+ private static Action<ILogger, Exception> _logResponseContentLengthMismatchNotCached;
+ private static Action<ILogger, TimeSpan, TimeSpan, Exception> _logExpirationInfiniteMaxStaleSatisfied;
+
+ static LoggerExtensions()
+ {
+ _logRequestMethodNotCacheable = LoggerMessage.Define<string>(
+ logLevel: LogLevel.Debug,
+ eventId: 1,
+ formatString: "The request cannot be served from cache because it uses the HTTP method: {Method}.");
+ _logRequestWithAuthorizationNotCacheable = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 2,
+ formatString: $"The request cannot be served from cache because it contains an '{HeaderNames.Authorization}' header.");
+ _logRequestWithNoCacheNotCacheable = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 3,
+ formatString: "The request cannot be served from cache because it contains a 'no-cache' cache directive.");
+ _logRequestWithPragmaNoCacheNotCacheable = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 4,
+ formatString: "The request cannot be served from cache because it contains a 'no-cache' pragma directive.");
+ _logExpirationMinFreshAdded = LoggerMessage.Define<TimeSpan>(
+ logLevel: LogLevel.Debug,
+ eventId: 5,
+ formatString: "Adding a minimum freshness requirement of {Duration} specified by the 'min-fresh' cache directive.");
+ _logExpirationSharedMaxAgeExceeded = LoggerMessage.Define<TimeSpan, TimeSpan>(
+ logLevel: LogLevel.Debug,
+ eventId: 6,
+ formatString: "The age of the entry is {Age} and has exceeded the maximum age for shared caches of {SharedMaxAge} specified by the 's-maxage' cache directive.");
+ _logExpirationMustRevalidate = LoggerMessage.Define<TimeSpan, TimeSpan>(
+ logLevel: LogLevel.Debug,
+ eventId: 7,
+ formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. It must be revalidated because the 'must-revalidate' or 'proxy-revalidate' cache directive is specified.");
+ _logExpirationMaxStaleSatisfied = LoggerMessage.Define<TimeSpan, TimeSpan, TimeSpan>(
+ logLevel: LogLevel.Debug,
+ eventId: 8,
+ formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. However, it satisfied the maximum stale allowance of {MaxStale} specified by the 'max-stale' cache directive.");
+ _logExpirationMaxAgeExceeded = LoggerMessage.Define<TimeSpan, TimeSpan>(
+ logLevel: LogLevel.Debug,
+ eventId: 9,
+ formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive.");
+ _logExpirationExpiresExceeded = LoggerMessage.Define<DateTimeOffset, DateTimeOffset>(
+ logLevel: LogLevel.Debug,
+ eventId: 10,
+ formatString: $"The response time of the entry is {{ResponseTime}} and has exceeded the expiry date of {{Expired}} specified by the '{HeaderNames.Expires}' header.");
+ _logResponseWithoutPublicNotCacheable = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 11,
+ formatString: "Response is not cacheable because it does not contain the 'public' cache directive.");
+ _logResponseWithNoStoreNotCacheable = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 12,
+ formatString: "Response is not cacheable because it or its corresponding request contains a 'no-store' cache directive.");
+ _logResponseWithNoCacheNotCacheable = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 13,
+ formatString: "Response is not cacheable because it contains a 'no-cache' cache directive.");
+ _logResponseWithSetCookieNotCacheable = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 14,
+ formatString: $"Response is not cacheable because it contains a '{HeaderNames.SetCookie}' header.");
+ _logResponseWithVaryStarNotCacheable = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 15,
+ formatString: $"Response is not cacheable because it contains a '{HeaderNames.Vary}' header with a value of *.");
+ _logResponseWithPrivateNotCacheable = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 16,
+ formatString: "Response is not cacheable because it contains the 'private' cache directive.");
+ _logResponseWithUnsuccessfulStatusCodeNotCacheable = LoggerMessage.Define<int>(
+ logLevel: LogLevel.Debug,
+ eventId: 17,
+ formatString: "Response is not cacheable because its status code {StatusCode} does not indicate success.");
+ _logNotModifiedIfNoneMatchStar = LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: 18,
+ formatString: $"The '{HeaderNames.IfNoneMatch}' header of the request contains a value of *.");
+ _logNotModifiedIfNoneMatchMatched = LoggerMessage.Define<EntityTagHeaderValue>(
+ logLevel: LogLevel.Debug,
+ eventId: 19,
+ formatString: $"The ETag {{ETag}} in the '{HeaderNames.IfNoneMatch}' header matched the ETag of a cached entry.");
+ _logNotModifiedIfModifiedSinceSatisfied = LoggerMessage.Define<DateTimeOffset, DateTimeOffset>(
+ logLevel: LogLevel.Debug,
+ eventId: 20,
+ formatString: $"The last modified date of {{LastModified}} is before the date {{IfModifiedSince}} specified in the '{HeaderNames.IfModifiedSince}' header.");
+ _logNotModifiedServed = LoggerMessage.Define(
+ logLevel: LogLevel.Information,
+ eventId: 21,
+ formatString: "The content requested has not been modified.");
+ _logCachedResponseServed = LoggerMessage.Define(
+ logLevel: LogLevel.Information,
+ eventId: 22,
+ formatString: "Serving response from cache.");
+ _logGatewayTimeoutServed = LoggerMessage.Define(
+ logLevel: LogLevel.Information,
+ eventId: 23,
+ formatString: "No cached response available for this request and the 'only-if-cached' cache directive was specified.");
+ _logNoResponseServed = LoggerMessage.Define(
+ logLevel: LogLevel.Information,
+ eventId: 24,
+ formatString: "No cached response available for this request.");
+ _logVaryByRulesUpdated = LoggerMessage.Define<string, string>(
+ logLevel: LogLevel.Debug,
+ eventId: 25,
+ formatString: "Vary by rules were updated. Headers: {Headers}, Query keys: {QueryKeys}");
+ _logResponseCached = LoggerMessage.Define(
+ logLevel: LogLevel.Information,
+ eventId: 26,
+ formatString: "The response has been cached.");
+ _logResponseNotCached = LoggerMessage.Define(
+ logLevel: LogLevel.Information,
+ eventId: 27,
+ formatString: "The response could not be cached for this request.");
+ _logResponseContentLengthMismatchNotCached = LoggerMessage.Define(
+ logLevel: LogLevel.Warning,
+ eventId: 28,
+ formatString: $"The response could not be cached for this request because the '{HeaderNames.ContentLength}' did not match the body length.");
+ _logExpirationInfiniteMaxStaleSatisfied = LoggerMessage.Define<TimeSpan, TimeSpan>(
+ logLevel: LogLevel.Debug,
+ eventId: 29,
+ formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. However, the 'max-stale' cache directive was specified without an assigned value and a stale response of any age is accepted.");
+ }
+
+ internal static void LogRequestMethodNotCacheable(this ILogger logger, string method)
+ {
+ _logRequestMethodNotCacheable(logger, method, null);
+ }
+
+ internal static void LogRequestWithAuthorizationNotCacheable(this ILogger logger)
+ {
+ _logRequestWithAuthorizationNotCacheable(logger, null);
+ }
+
+ internal static void LogRequestWithNoCacheNotCacheable(this ILogger logger)
+ {
+ _logRequestWithNoCacheNotCacheable(logger, null);
+ }
+
+ internal static void LogRequestWithPragmaNoCacheNotCacheable(this ILogger logger)
+ {
+ _logRequestWithPragmaNoCacheNotCacheable(logger, null);
+ }
+
+ internal static void LogExpirationMinFreshAdded(this ILogger logger, TimeSpan duration)
+ {
+ _logExpirationMinFreshAdded(logger, duration, null);
+ }
+
+ internal static void LogExpirationSharedMaxAgeExceeded(this ILogger logger, TimeSpan age, TimeSpan sharedMaxAge)
+ {
+ _logExpirationSharedMaxAgeExceeded(logger, age, sharedMaxAge, null);
+ }
+
+ internal static void LogExpirationMustRevalidate(this ILogger logger, TimeSpan age, TimeSpan maxAge)
+ {
+ _logExpirationMustRevalidate(logger, age, maxAge, null);
+ }
+
+ internal static void LogExpirationMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge, TimeSpan maxStale)
+ {
+ _logExpirationMaxStaleSatisfied(logger, age, maxAge, maxStale, null);
+ }
+
+ internal static void LogExpirationMaxAgeExceeded(this ILogger logger, TimeSpan age, TimeSpan sharedMaxAge)
+ {
+ _logExpirationMaxAgeExceeded(logger, age, sharedMaxAge, null);
+ }
+
+ internal static void LogExpirationExpiresExceeded(this ILogger logger, DateTimeOffset responseTime, DateTimeOffset expires)
+ {
+ _logExpirationExpiresExceeded(logger, responseTime, expires, null);
+ }
+
+ internal static void LogResponseWithoutPublicNotCacheable(this ILogger logger)
+ {
+ _logResponseWithoutPublicNotCacheable(logger, null);
+ }
+
+ internal static void LogResponseWithNoStoreNotCacheable(this ILogger logger)
+ {
+ _logResponseWithNoStoreNotCacheable(logger, null);
+ }
+
+ internal static void LogResponseWithNoCacheNotCacheable(this ILogger logger)
+ {
+ _logResponseWithNoCacheNotCacheable(logger, null);
+ }
+
+ internal static void LogResponseWithSetCookieNotCacheable(this ILogger logger)
+ {
+ _logResponseWithSetCookieNotCacheable(logger, null);
+ }
+
+ internal static void LogResponseWithVaryStarNotCacheable(this ILogger logger)
+ {
+ _logResponseWithVaryStarNotCacheable(logger, null);
+ }
+
+ internal static void LogResponseWithPrivateNotCacheable(this ILogger logger)
+ {
+ _logResponseWithPrivateNotCacheable(logger, null);
+ }
+
+ internal static void LogResponseWithUnsuccessfulStatusCodeNotCacheable(this ILogger logger, int statusCode)
+ {
+ _logResponseWithUnsuccessfulStatusCodeNotCacheable(logger, statusCode, null);
+ }
+
+ internal static void LogNotModifiedIfNoneMatchStar(this ILogger logger)
+ {
+ _logNotModifiedIfNoneMatchStar(logger, null);
+ }
+
+ internal static void LogNotModifiedIfNoneMatchMatched(this ILogger logger, EntityTagHeaderValue etag)
+ {
+ _logNotModifiedIfNoneMatchMatched(logger, etag, null);
+ }
+
+ internal static void LogNotModifiedIfModifiedSinceSatisfied(this ILogger logger, DateTimeOffset lastModified, DateTimeOffset ifModifiedSince)
+ {
+ _logNotModifiedIfModifiedSinceSatisfied(logger, lastModified, ifModifiedSince, null);
+ }
+
+ internal static void LogNotModifiedServed(this ILogger logger)
+ {
+ _logNotModifiedServed(logger, null);
+ }
+
+ internal static void LogCachedResponseServed(this ILogger logger)
+ {
+ _logCachedResponseServed(logger, null);
+ }
+
+ internal static void LogGatewayTimeoutServed(this ILogger logger)
+ {
+ _logGatewayTimeoutServed(logger, null);
+ }
+
+ internal static void LogNoResponseServed(this ILogger logger)
+ {
+ _logNoResponseServed(logger, null);
+ }
+
+ internal static void LogVaryByRulesUpdated(this ILogger logger, string headers, string queryKeys)
+ {
+ _logVaryByRulesUpdated(logger, headers, queryKeys, null);
+ }
+
+ internal static void LogResponseCached(this ILogger logger)
+ {
+ _logResponseCached(logger, null);
+ }
+
+ internal static void LogResponseNotCached(this ILogger logger)
+ {
+ _logResponseNotCached(logger, null);
+ }
+
+ internal static void LogResponseContentLengthMismatchNotCached(this ILogger logger)
+ {
+ _logResponseContentLengthMismatchNotCached(logger, null);
+ }
+
+ internal static void LogExpirationInfiniteMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge)
+ {
+ _logExpirationInfiniteMaxStaleSatisfied(logger, age, maxAge, null);
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.cs
new file mode 100644
index 0000000000..d24e63a9ff
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.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;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ internal class MemoryCachedResponse
+ {
+ public DateTimeOffset Created { get; set; }
+
+ public int StatusCode { get; set; }
+
+ public IHeaderDictionary Headers { get; set; } = new HeaderDictionary();
+
+ public List<byte[]> BodySegments { get; set; }
+
+ public long BodyLength { get; set; }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs
new file mode 100644
index 0000000000..d69d48ebbd
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs
@@ -0,0 +1,93 @@
+// 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.Threading.Tasks;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ public class MemoryResponseCache : IResponseCache
+ {
+ private readonly IMemoryCache _cache;
+
+ public MemoryResponseCache(IMemoryCache cache)
+ {
+ if (cache == null)
+ {
+ throw new ArgumentNullException(nameof(cache));
+ }
+
+ _cache = cache;
+ }
+
+ public IResponseCacheEntry Get(string key)
+ {
+ var entry = _cache.Get(key);
+
+ var memoryCachedResponse = entry as MemoryCachedResponse;
+ if (memoryCachedResponse != null)
+ {
+ return new CachedResponse
+ {
+ Created = memoryCachedResponse.Created,
+ StatusCode = memoryCachedResponse.StatusCode,
+ Headers = memoryCachedResponse.Headers,
+ Body = new SegmentReadStream(memoryCachedResponse.BodySegments, memoryCachedResponse.BodyLength)
+ };
+ }
+ else
+ {
+ return entry as IResponseCacheEntry;
+ }
+ }
+
+ public Task<IResponseCacheEntry> GetAsync(string key)
+ {
+ return Task.FromResult(Get(key));
+ }
+
+ public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor)
+ {
+ var cachedResponse = entry as CachedResponse;
+ if (cachedResponse != null)
+ {
+ var segmentStream = new SegmentWriteStream(StreamUtilities.BodySegmentSize);
+ cachedResponse.Body.CopyTo(segmentStream);
+
+ _cache.Set(
+ key,
+ new MemoryCachedResponse
+ {
+ Created = cachedResponse.Created,
+ StatusCode = cachedResponse.StatusCode,
+ Headers = cachedResponse.Headers,
+ BodySegments = segmentStream.GetSegments(),
+ BodyLength = segmentStream.Length
+ },
+ new MemoryCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = validFor,
+ Size = CacheEntryHelpers.EstimateCachedResponseSize(cachedResponse)
+ });
+ }
+ else
+ {
+ _cache.Set(
+ key,
+ entry,
+ new MemoryCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = validFor,
+ Size = CacheEntryHelpers.EstimateCachedVaryByRulesySize(entry as CachedVaryByRules)
+ });
+ }
+ }
+
+ public Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor)
+ {
+ Set(key, entry, validFor);
+ return Task.CompletedTask;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs
new file mode 100644
index 0000000000..2d8c79b11b
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs
@@ -0,0 +1,134 @@
+// 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.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ public class ResponseCachingContext
+ {
+ private DateTimeOffset? _responseDate;
+ private bool _parsedResponseDate;
+ private DateTimeOffset? _responseExpires;
+ private bool _parsedResponseExpires;
+ private TimeSpan? _responseSharedMaxAge;
+ private bool _parsedResponseSharedMaxAge;
+ private TimeSpan? _responseMaxAge;
+ private bool _parsedResponseMaxAge;
+
+ internal ResponseCachingContext(HttpContext httpContext, ILogger logger)
+ {
+ HttpContext = httpContext;
+ Logger = logger;
+ }
+
+ public HttpContext HttpContext { get; }
+
+ public DateTimeOffset? ResponseTime { get; internal set; }
+
+ public TimeSpan? CachedEntryAge { get; internal set; }
+
+ public CachedVaryByRules CachedVaryByRules { get; internal set; }
+
+ internal ILogger Logger { get; }
+
+ internal bool ShouldCacheResponse { get; set; }
+
+ internal string BaseKey { get; set; }
+
+ internal string StorageVaryKey { get; set; }
+
+ internal TimeSpan CachedResponseValidFor { get; set; }
+
+ internal CachedResponse CachedResponse { get; set; }
+
+ internal bool ResponseStarted { get; set; }
+
+ internal Stream OriginalResponseStream { get; set; }
+
+ internal ResponseCachingStream ResponseCachingStream { get; set; }
+
+ internal IHttpSendFileFeature OriginalSendFileFeature { get; set; }
+
+ internal IHeaderDictionary CachedResponseHeaders { get; set; }
+
+ internal DateTimeOffset? ResponseDate
+ {
+ get
+ {
+ if (!_parsedResponseDate)
+ {
+ _parsedResponseDate = true;
+ DateTimeOffset date;
+ if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Date].ToString(), out date))
+ {
+ _responseDate = date;
+ }
+ else
+ {
+ _responseDate = null;
+ }
+ }
+ return _responseDate;
+ }
+ set
+ {
+ // Don't reparse the response date again if it's explicitly set
+ _parsedResponseDate = true;
+ _responseDate = value;
+ }
+ }
+
+ internal DateTimeOffset? ResponseExpires
+ {
+ get
+ {
+ if (!_parsedResponseExpires)
+ {
+ _parsedResponseExpires = true;
+ DateTimeOffset expires;
+ if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Expires].ToString(), out expires))
+ {
+ _responseExpires = expires;
+ }
+ else
+ {
+ _responseExpires = null;
+ }
+ }
+ return _responseExpires;
+ }
+ }
+
+ internal TimeSpan? ResponseSharedMaxAge
+ {
+ get
+ {
+ if (!_parsedResponseSharedMaxAge)
+ {
+ _parsedResponseSharedMaxAge = true;
+ HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.SharedMaxAgeString, out _responseSharedMaxAge);
+ }
+ return _responseSharedMaxAge;
+ }
+ }
+
+ internal TimeSpan? ResponseMaxAge
+ {
+ get
+ {
+ if (!_parsedResponseMaxAge)
+ {
+ _parsedResponseMaxAge = true;
+ HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.MaxAgeString, out _responseMaxAge);
+ }
+ return _responseMaxAge;
+ }
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs
new file mode 100644
index 0000000000..d69d9008eb
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs
@@ -0,0 +1,219 @@
+// 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.Text;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ public class ResponseCachingKeyProvider : IResponseCachingKeyProvider
+ {
+ // Use the record separator for delimiting components of the cache key to avoid possible collisions
+ private static readonly char KeyDelimiter = '\x1e';
+ // Use the unit separator for delimiting subcomponents of the cache key to avoid possible collisions
+ private static readonly char KeySubDelimiter = '\x1f';
+
+ private readonly ObjectPool<StringBuilder> _builderPool;
+ private readonly ResponseCachingOptions _options;
+
+ public ResponseCachingKeyProvider(ObjectPoolProvider poolProvider, IOptions<ResponseCachingOptions> options)
+ {
+ if (poolProvider == null)
+ {
+ throw new ArgumentNullException(nameof(poolProvider));
+ }
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ _builderPool = poolProvider.CreateStringBuilderPool();
+ _options = options.Value;
+ }
+
+ public IEnumerable<string> CreateLookupVaryByKeys(ResponseCachingContext context)
+ {
+ return new string[] { CreateStorageVaryByKey(context) };
+ }
+
+ // GET<delimiter>SCHEME<delimiter>HOST:PORT/PATHBASE/PATH
+ public string CreateBaseKey(ResponseCachingContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var request = context.HttpContext.Request;
+ var builder = _builderPool.Get();
+
+ try
+ {
+ builder
+ .AppendUpperInvariant(request.Method)
+ .Append(KeyDelimiter)
+ .AppendUpperInvariant(request.Scheme)
+ .Append(KeyDelimiter)
+ .AppendUpperInvariant(request.Host.Value);
+
+ if (_options.UseCaseSensitivePaths)
+ {
+ builder
+ .Append(request.PathBase.Value)
+ .Append(request.Path.Value);
+ }
+ else
+ {
+ builder
+ .AppendUpperInvariant(request.PathBase.Value)
+ .AppendUpperInvariant(request.Path.Value);
+ }
+
+ return builder.ToString();
+ }
+ finally
+ {
+ _builderPool.Return(builder);
+ }
+ }
+
+ // BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue1<subdelimiter>QueryValue2
+ public string CreateStorageVaryByKey(ResponseCachingContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var varyByRules = context.CachedVaryByRules;
+ if (varyByRules == null)
+ {
+ throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(ResponseCachingContext)}");
+ }
+
+ if ((StringValues.IsNullOrEmpty(varyByRules.Headers) && StringValues.IsNullOrEmpty(varyByRules.QueryKeys)))
+ {
+ return varyByRules.VaryByKeyPrefix;
+ }
+
+ var request = context.HttpContext.Request;
+ var builder = _builderPool.Get();
+
+ try
+ {
+ // Prepend with the Guid of the CachedVaryByRules
+ builder.Append(varyByRules.VaryByKeyPrefix);
+
+ // Vary by headers
+ if (varyByRules?.Headers.Count > 0)
+ {
+ // Append a group separator for the header segment of the cache key
+ builder.Append(KeyDelimiter)
+ .Append('H');
+
+ for (var i = 0; i < varyByRules.Headers.Count; i++)
+ {
+ var header = varyByRules.Headers[i];
+ var headerValues = context.HttpContext.Request.Headers[header];
+ builder.Append(KeyDelimiter)
+ .Append(header)
+ .Append("=");
+
+ var headerValuesArray = headerValues.ToArray();
+ Array.Sort(headerValuesArray, StringComparer.Ordinal);
+
+ for (var j = 0; j < headerValuesArray.Length; j++)
+ {
+ builder.Append(headerValuesArray[j]);
+ }
+ }
+ }
+
+ // Vary by query keys
+ if (varyByRules?.QueryKeys.Count > 0)
+ {
+ // Append a group separator for the query key segment of the cache key
+ builder.Append(KeyDelimiter)
+ .Append('Q');
+
+ if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal))
+ {
+ // Vary by all available query keys
+ var queryArray = context.HttpContext.Request.Query.ToArray();
+ // Query keys are aggregated case-insensitively whereas the query values are compared ordinally.
+ Array.Sort(queryArray, QueryKeyComparer.OrdinalIgnoreCase);
+
+ for (var i = 0; i < queryArray.Length; i++)
+ {
+ builder.Append(KeyDelimiter)
+ .AppendUpperInvariant(queryArray[i].Key)
+ .Append("=");
+
+ var queryValueArray = queryArray[i].Value.ToArray();
+ Array.Sort(queryValueArray, StringComparer.Ordinal);
+
+ for (var j = 0; j < queryValueArray.Length; j++)
+ {
+ if (j > 0)
+ {
+ builder.Append(KeySubDelimiter);
+ }
+
+ builder.Append(queryValueArray[j]);
+ }
+ }
+ }
+ else
+ {
+ for (var i = 0; i < varyByRules.QueryKeys.Count; i++)
+ {
+ var queryKey = varyByRules.QueryKeys[i];
+ var queryKeyValues = context.HttpContext.Request.Query[queryKey];
+ builder.Append(KeyDelimiter)
+ .Append(queryKey)
+ .Append("=");
+
+ var queryValueArray = queryKeyValues.ToArray();
+ Array.Sort(queryValueArray, StringComparer.Ordinal);
+
+ for (var j = 0; j < queryValueArray.Length; j++)
+ {
+ if (j > 0)
+ {
+ builder.Append(KeySubDelimiter);
+ }
+
+ builder.Append(queryValueArray[j]);
+ }
+ }
+ }
+ }
+
+ return builder.ToString();
+ }
+ finally
+ {
+ _builderPool.Return(builder);
+ }
+ }
+
+ private class QueryKeyComparer : IComparer<KeyValuePair<string, StringValues>>
+ {
+ private StringComparer _stringComparer;
+
+ public static QueryKeyComparer OrdinalIgnoreCase { get; } = new QueryKeyComparer(StringComparer.OrdinalIgnoreCase);
+
+ public QueryKeyComparer(StringComparer stringComparer)
+ {
+ _stringComparer = stringComparer;
+ }
+
+ public int Compare(KeyValuePair<string, StringValues> x, KeyValuePair<string, StringValues> y) => _stringComparer.Compare(x.Key, y.Key);
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs
new file mode 100644
index 0000000000..8ffc59612e
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs
@@ -0,0 +1,248 @@
+// 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;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ public class ResponseCachingPolicyProvider : IResponseCachingPolicyProvider
+ {
+ public virtual bool AttemptResponseCaching(ResponseCachingContext context)
+ {
+ var request = context.HttpContext.Request;
+
+ // Verify the method
+ if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method))
+ {
+ context.Logger.LogRequestMethodNotCacheable(request.Method);
+ return false;
+ }
+
+ // Verify existence of authorization headers
+ if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization]))
+ {
+ context.Logger.LogRequestWithAuthorizationNotCacheable();
+ return false;
+ }
+
+ return true;
+ }
+
+ public virtual bool AllowCacheLookup(ResponseCachingContext context)
+ {
+ var request = context.HttpContext.Request;
+
+ // Verify request cache-control parameters
+ if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl]))
+ {
+ if (HeaderUtilities.ContainsCacheDirective(request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoCacheString))
+ {
+ context.Logger.LogRequestWithNoCacheNotCacheable();
+ return false;
+ }
+ }
+ else
+ {
+ // Support for legacy HTTP 1.0 cache directive
+ var pragmaHeaderValues = request.Headers[HeaderNames.Pragma];
+ if (HeaderUtilities.ContainsCacheDirective(request.Headers[HeaderNames.Pragma], CacheControlHeaderValue.NoCacheString))
+ {
+ context.Logger.LogRequestWithPragmaNoCacheNotCacheable();
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public virtual bool AllowCacheStorage(ResponseCachingContext context)
+ {
+ // Check request no-store
+ return !HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoStoreString);
+ }
+
+ public virtual bool IsResponseCacheable(ResponseCachingContext context)
+ {
+ var responseCacheControlHeader = context.HttpContext.Response.Headers[HeaderNames.CacheControl];
+
+ // Only cache pages explicitly marked with public
+ if (!HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PublicString))
+ {
+ context.Logger.LogResponseWithoutPublicNotCacheable();
+ return false;
+ }
+
+ // Check response no-store
+ if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoStoreString))
+ {
+ context.Logger.LogResponseWithNoStoreNotCacheable();
+ return false;
+ }
+
+ // Check no-cache
+ if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoCacheString))
+ {
+ context.Logger.LogResponseWithNoCacheNotCacheable();
+ return false;
+ }
+
+ var response = context.HttpContext.Response;
+
+ // Do not cache responses with Set-Cookie headers
+ if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie]))
+ {
+ context.Logger.LogResponseWithSetCookieNotCacheable();
+ return false;
+ }
+
+ // Do not cache responses varying by *
+ var varyHeader = response.Headers[HeaderNames.Vary];
+ if (varyHeader.Count == 1 && string.Equals(varyHeader, "*", StringComparison.OrdinalIgnoreCase))
+ {
+ context.Logger.LogResponseWithVaryStarNotCacheable();
+ return false;
+ }
+
+ // Check private
+ if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PrivateString))
+ {
+ context.Logger.LogResponseWithPrivateNotCacheable();
+ return false;
+ }
+
+ // Check response code
+ if (response.StatusCode != StatusCodes.Status200OK)
+ {
+ context.Logger.LogResponseWithUnsuccessfulStatusCodeNotCacheable(response.StatusCode);
+ return false;
+ }
+
+ // Check response freshness
+ if (!context.ResponseDate.HasValue)
+ {
+ if (!context.ResponseSharedMaxAge.HasValue &&
+ !context.ResponseMaxAge.HasValue &&
+ context.ResponseTime.Value >= context.ResponseExpires)
+ {
+ context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value);
+ return false;
+ }
+ }
+ else
+ {
+ var age = context.ResponseTime.Value - context.ResponseDate.Value;
+
+ // Validate shared max age
+ if (age >= context.ResponseSharedMaxAge)
+ {
+ context.Logger.LogExpirationSharedMaxAgeExceeded(age, context.ResponseSharedMaxAge.Value);
+ return false;
+ }
+ else if (!context.ResponseSharedMaxAge.HasValue)
+ {
+ // Validate max age
+ if (age >= context.ResponseMaxAge)
+ {
+ context.Logger.LogExpirationMaxAgeExceeded(age, context.ResponseMaxAge.Value);
+ return false;
+ }
+ else if (!context.ResponseMaxAge.HasValue)
+ {
+ // Validate expiration
+ if (context.ResponseTime.Value >= context.ResponseExpires)
+ {
+ context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value);
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public virtual bool IsCachedEntryFresh(ResponseCachingContext context)
+ {
+ var age = context.CachedEntryAge.Value;
+ var cachedCacheControlHeaders = context.CachedResponseHeaders[HeaderNames.CacheControl];
+ var requestCacheControlHeaders = context.HttpContext.Request.Headers[HeaderNames.CacheControl];
+
+ // Add min-fresh requirements
+ TimeSpan? minFresh;
+ if (HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MinFreshString, out minFresh))
+ {
+ age += minFresh.Value;
+ context.Logger.LogExpirationMinFreshAdded(minFresh.Value);
+ }
+
+ // Validate shared max age, this overrides any max age settings for shared caches
+ TimeSpan? cachedSharedMaxAge;
+ HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.SharedMaxAgeString, out cachedSharedMaxAge);
+
+ if (age >= cachedSharedMaxAge)
+ {
+ // shared max age implies must revalidate
+ context.Logger.LogExpirationSharedMaxAgeExceeded(age, cachedSharedMaxAge.Value);
+ return false;
+ }
+ else if (!cachedSharedMaxAge.HasValue)
+ {
+ TimeSpan? requestMaxAge;
+ HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out requestMaxAge);
+
+ TimeSpan? cachedMaxAge;
+ HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out cachedMaxAge);
+
+ var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge;
+ // Validate max age
+ if (age >= lowestMaxAge)
+ {
+ // Must revalidate or proxy revalidate
+ if (HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.MustRevalidateString)
+ || HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.ProxyRevalidateString))
+ {
+ context.Logger.LogExpirationMustRevalidate(age, lowestMaxAge.Value);
+ return false;
+ }
+
+ TimeSpan? requestMaxStale;
+ var maxStaleExist = HeaderUtilities.ContainsCacheDirective(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString);
+ HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString, out requestMaxStale);
+
+ // Request allows stale values with no age limit
+ if (maxStaleExist && !requestMaxStale.HasValue)
+ {
+ context.Logger.LogExpirationInfiniteMaxStaleSatisfied(age, lowestMaxAge.Value);
+ return true;
+ }
+
+ // Request allows stale values with age limit
+ if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale)
+ {
+ context.Logger.LogExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, requestMaxStale.Value);
+ return true;
+ }
+
+ context.Logger.LogExpirationMaxAgeExceeded(age, lowestMaxAge.Value);
+ return false;
+ }
+ else if (!cachedMaxAge.HasValue && !requestMaxAge.HasValue)
+ {
+ // Validate expiration
+ DateTimeOffset expires;
+ if (HeaderUtilities.TryParseDate(context.CachedResponseHeaders[HeaderNames.Expires].ToString(), out expires) &&
+ context.ResponseTime.Value >= expires)
+ {
+ context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, expires);
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs
new file mode 100644
index 0000000000..2716e4cd37
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs
@@ -0,0 +1,28 @@
+// 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;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ internal class SendFileFeatureWrapper : IHttpSendFileFeature
+ {
+ private readonly IHttpSendFileFeature _originalSendFileFeature;
+ private readonly ResponseCachingStream _responseCachingStream;
+
+ public SendFileFeatureWrapper(IHttpSendFileFeature originalSendFileFeature, ResponseCachingStream responseCachingStream)
+ {
+ _originalSendFileFeature = originalSendFileFeature;
+ _responseCachingStream = responseCachingStream;
+ }
+
+ // Flush and disable the buffer if anyone tries to call the SendFile feature.
+ public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
+ {
+ _responseCachingStream.DisableBuffering();
+ return _originalSendFileFeature.SendFileAsync(path, offset, length, cancellation);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs
new file mode 100644
index 0000000000..98cfa7e172
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs
@@ -0,0 +1,24 @@
+// 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.Text;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ internal static class StringBuilderExtensions
+ {
+ internal static StringBuilder AppendUpperInvariant(this StringBuilder builder, string value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ builder.EnsureCapacity(builder.Length + value.Length);
+ for (var i = 0; i < value.Length; i++)
+ {
+ builder.Append(char.ToUpperInvariant(value[i]));
+ }
+ }
+
+ return builder;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs
new file mode 100644
index 0000000000..39b6e4735a
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs
@@ -0,0 +1,24 @@
+// 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.ResponseCaching.Internal
+{
+ /// <summary>
+ /// Provides access to the normal system clock.
+ /// </summary>
+ internal class SystemClock : ISystemClock
+ {
+ /// <summary>
+ /// Retrieves the current system time in UTC.
+ /// </summary>
+ public DateTimeOffset UtcNow
+ {
+ get
+ {
+ return DateTimeOffset.UtcNow;
+ }
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj
new file mode 100644
index 0000000000..c2547522ff
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj
@@ -0,0 +1,23 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core middleware for caching HTTP responses on the server.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;cache;caching</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.ResponseCaching.Abstractions\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(MicrosoftAspNetCoreHttpExtensionsPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(MicrosoftExtensionsCachingMemoryPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..7a0fd0e4de
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// 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("Microsoft.AspNetCore.ResponseCaching.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs
new file mode 100644
index 0000000000..76b81dbccb
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.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;
+using Microsoft.AspNetCore.ResponseCaching;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ public static class ResponseCachingExtensions
+ {
+ public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseMiddleware<ResponseCachingMiddleware>();
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs
new file mode 100644
index 0000000000..14232b97de
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.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.ResponseCaching
+{
+ public class ResponseCachingFeature : IResponseCachingFeature
+ {
+ private string[] _varyByQueryKeys;
+
+ public string[] VaryByQueryKeys
+ {
+ get
+ {
+ return _varyByQueryKeys;
+ }
+ set
+ {
+ if (value?.Length > 1)
+ {
+ for (var i = 0; i < value.Length; i++)
+ {
+ if (string.IsNullOrEmpty(value[i]))
+ {
+ throw new ArgumentException($"When {nameof(value)} contains more than one value, it cannot contain a null or empty value.", nameof(value));
+ }
+ }
+ }
+ _varyByQueryKeys = value;
+ }
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs
new file mode 100644
index 0000000000..d2eee86ad7
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs
@@ -0,0 +1,528 @@
+// 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.Features;
+using Microsoft.AspNetCore.ResponseCaching.Internal;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.ResponseCaching
+{
+ public class ResponseCachingMiddleware
+ {
+ private static readonly TimeSpan DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10);
+
+ private readonly RequestDelegate _next;
+ private readonly ResponseCachingOptions _options;
+ private readonly ILogger _logger;
+ private readonly IResponseCachingPolicyProvider _policyProvider;
+ private readonly IResponseCache _cache;
+ private readonly IResponseCachingKeyProvider _keyProvider;
+
+ public ResponseCachingMiddleware(
+ RequestDelegate next,
+ IOptions<ResponseCachingOptions> options,
+ ILoggerFactory loggerFactory,
+ IResponseCachingPolicyProvider policyProvider,
+ IResponseCachingKeyProvider keyProvider)
+ : this(
+ next,
+ options,
+ loggerFactory,
+ policyProvider,
+ new MemoryResponseCache(new MemoryCache(new MemoryCacheOptions
+ {
+ SizeLimit = options.Value.SizeLimit
+ })), keyProvider)
+ { }
+
+ // for testing
+ internal ResponseCachingMiddleware(
+ RequestDelegate next,
+ IOptions<ResponseCachingOptions> options,
+ ILoggerFactory loggerFactory,
+ IResponseCachingPolicyProvider policyProvider,
+ IResponseCache cache,
+ IResponseCachingKeyProvider keyProvider)
+ {
+ if (next == null)
+ {
+ throw new ArgumentNullException(nameof(next));
+ }
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+ if (policyProvider == null)
+ {
+ throw new ArgumentNullException(nameof(policyProvider));
+ }
+ if (cache == null)
+ {
+ throw new ArgumentNullException(nameof(cache));
+ }
+ if (keyProvider == null)
+ {
+ throw new ArgumentNullException(nameof(keyProvider));
+ }
+
+ _next = next;
+ _options = options.Value;
+ _logger = loggerFactory.CreateLogger<ResponseCachingMiddleware>();
+ _policyProvider = policyProvider;
+ _cache = cache;
+ _keyProvider = keyProvider;
+ }
+
+ public async Task Invoke(HttpContext httpContext)
+ {
+ var context = new ResponseCachingContext(httpContext, _logger);
+
+ // Should we attempt any caching logic?
+ if (_policyProvider.AttemptResponseCaching(context))
+ {
+ // Can this request be served from cache?
+ if (_policyProvider.AllowCacheLookup(context) && await TryServeFromCacheAsync(context))
+ {
+ return;
+ }
+
+ // Should we store the response to this request?
+ if (_policyProvider.AllowCacheStorage(context))
+ {
+ // Hook up to listen to the response stream
+ ShimResponseStream(context);
+
+ try
+ {
+ await _next(httpContext);
+
+ // If there was no response body, check the response headers now. We can cache things like redirects.
+ await StartResponseAsync(context);
+
+ // Finalize the cache entry
+ await FinalizeCacheBodyAsync(context);
+ }
+ finally
+ {
+ UnshimResponseStream(context);
+ }
+
+ return;
+ }
+ }
+
+ // Response should not be captured but add IResponseCachingFeature which may be required when the response is generated
+ AddResponseCachingFeature(httpContext);
+
+ try
+ {
+ await _next(httpContext);
+ }
+ finally
+ {
+ RemoveResponseCachingFeature(httpContext);
+ }
+ }
+
+ internal async Task<bool> TryServeCachedResponseAsync(ResponseCachingContext context, IResponseCacheEntry cacheEntry)
+ {
+ var cachedResponse = cacheEntry as CachedResponse;
+ if (cachedResponse == null)
+ {
+ return false;
+ }
+
+ context.CachedResponse = cachedResponse;
+ context.CachedResponseHeaders = cachedResponse.Headers;
+ context.ResponseTime = _options.SystemClock.UtcNow;
+ var cachedEntryAge = context.ResponseTime.Value - context.CachedResponse.Created;
+ context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero;
+
+ if (_policyProvider.IsCachedEntryFresh(context))
+ {
+ // Check conditional request rules
+ if (ContentIsNotModified(context))
+ {
+ _logger.LogNotModifiedServed();
+ context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified;
+ }
+ else
+ {
+ var response = context.HttpContext.Response;
+ // Copy the cached status code and response headers
+ response.StatusCode = context.CachedResponse.StatusCode;
+ foreach (var header in context.CachedResponse.Headers)
+ {
+ response.Headers[header.Key] = header.Value;
+ }
+
+ // Note: int64 division truncates result and errors may be up to 1 second. This reduction in
+ // accuracy of age calculation is considered appropriate since it is small compared to clock
+ // skews and the "Age" header is an estimate of the real age of cached content.
+ response.Headers[HeaderNames.Age] = HeaderUtilities.FormatNonNegativeInt64(context.CachedEntryAge.Value.Ticks / TimeSpan.TicksPerSecond);
+
+ // Copy the cached response body
+ var body = context.CachedResponse.Body;
+ if (body.Length > 0)
+ {
+ try
+ {
+ await body.CopyToAsync(response.Body, StreamUtilities.BodySegmentSize, context.HttpContext.RequestAborted);
+ }
+ catch (OperationCanceledException)
+ {
+ context.HttpContext.Abort();
+ }
+ }
+ _logger.LogCachedResponseServed();
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ internal async Task<bool> TryServeFromCacheAsync(ResponseCachingContext context)
+ {
+ context.BaseKey = _keyProvider.CreateBaseKey(context);
+ var cacheEntry = await _cache.GetAsync(context.BaseKey);
+
+ var cachedVaryByRules = cacheEntry as CachedVaryByRules;
+ if (cachedVaryByRules != null)
+ {
+ // Request contains vary rules, recompute key(s) and try again
+ context.CachedVaryByRules = cachedVaryByRules;
+
+ foreach (var varyKey in _keyProvider.CreateLookupVaryByKeys(context))
+ {
+ if (await TryServeCachedResponseAsync(context, await _cache.GetAsync(varyKey)))
+ {
+ return true;
+ }
+ }
+ }
+ else
+ {
+ if (await TryServeCachedResponseAsync(context, cacheEntry))
+ {
+ return true;
+ }
+ }
+
+ if (HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.OnlyIfCachedString))
+ {
+ _logger.LogGatewayTimeoutServed();
+ context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
+ return true;
+ }
+
+ _logger.LogNoResponseServed();
+ return false;
+ }
+
+
+ /// <summary>
+ /// Finalize cache headers.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <returns><c>true</c> if a vary by entry needs to be stored in the cache; otherwise <c>false</c>.</returns>
+ private bool OnFinalizeCacheHeaders(ResponseCachingContext context)
+ {
+ if (_policyProvider.IsResponseCacheable(context))
+ {
+ var storeVaryByEntry = false;
+ context.ShouldCacheResponse = true;
+
+ // Create the cache entry now
+ var response = context.HttpContext.Response;
+ var varyHeaders = new StringValues(response.Headers.GetCommaSeparatedValues(HeaderNames.Vary));
+ var varyQueryKeys = new StringValues(context.HttpContext.Features.Get<IResponseCachingFeature>()?.VaryByQueryKeys);
+ context.CachedResponseValidFor = context.ResponseSharedMaxAge ??
+ context.ResponseMaxAge ??
+ (context.ResponseExpires - context.ResponseTime.Value) ??
+ DefaultExpirationTimeSpan;
+
+ // Generate a base key if none exist
+ if (string.IsNullOrEmpty(context.BaseKey))
+ {
+ context.BaseKey = _keyProvider.CreateBaseKey(context);
+ }
+
+ // Check if any vary rules exist
+ if (!StringValues.IsNullOrEmpty(varyHeaders) || !StringValues.IsNullOrEmpty(varyQueryKeys))
+ {
+ // Normalize order and casing of vary by rules
+ var normalizedVaryHeaders = GetOrderCasingNormalizedStringValues(varyHeaders);
+ var normalizedVaryQueryKeys = GetOrderCasingNormalizedStringValues(varyQueryKeys);
+
+ // Update vary rules if they are different
+ if (context.CachedVaryByRules == null ||
+ !StringValues.Equals(context.CachedVaryByRules.QueryKeys, normalizedVaryQueryKeys) ||
+ !StringValues.Equals(context.CachedVaryByRules.Headers, normalizedVaryHeaders))
+ {
+ context.CachedVaryByRules = new CachedVaryByRules
+ {
+ VaryByKeyPrefix = FastGuid.NewGuid().IdString,
+ Headers = normalizedVaryHeaders,
+ QueryKeys = normalizedVaryQueryKeys
+ };
+ }
+
+ // Always overwrite the CachedVaryByRules to update the expiry information
+ _logger.LogVaryByRulesUpdated(normalizedVaryHeaders, normalizedVaryQueryKeys);
+ storeVaryByEntry = true;
+
+ context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context);
+ }
+
+ // Ensure date header is set
+ if (!context.ResponseDate.HasValue)
+ {
+ context.ResponseDate = context.ResponseTime.Value;
+ // Setting the date on the raw response headers.
+ context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(context.ResponseDate.Value);
+ }
+
+ // Store the response on the state
+ context.CachedResponse = new CachedResponse
+ {
+ Created = context.ResponseDate.Value,
+ StatusCode = context.HttpContext.Response.StatusCode,
+ Headers = new HeaderDictionary()
+ };
+
+ foreach (var header in context.HttpContext.Response.Headers)
+ {
+ if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase))
+ {
+ context.CachedResponse.Headers[header.Key] = header.Value;
+ }
+ }
+
+ return storeVaryByEntry;
+ }
+
+ context.ResponseCachingStream.DisableBuffering();
+ return false;
+ }
+
+ internal void FinalizeCacheHeaders(ResponseCachingContext context)
+ {
+ if (OnFinalizeCacheHeaders(context))
+ {
+ _cache.Set(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor);
+ }
+ }
+
+ internal Task FinalizeCacheHeadersAsync(ResponseCachingContext context)
+ {
+ if (OnFinalizeCacheHeaders(context))
+ {
+ return _cache.SetAsync(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor);
+ }
+ return Task.CompletedTask;
+ }
+
+ internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context)
+ {
+ if (context.ShouldCacheResponse && context.ResponseCachingStream.BufferingEnabled)
+ {
+ var contentLength = context.HttpContext.Response.ContentLength;
+ var bufferStream = context.ResponseCachingStream.GetBufferStream();
+ if (!contentLength.HasValue || contentLength == bufferStream.Length)
+ {
+ var response = context.HttpContext.Response;
+ // Add a content-length if required
+ if (!response.ContentLength.HasValue && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
+ {
+ context.CachedResponse.Headers[HeaderNames.ContentLength] = HeaderUtilities.FormatNonNegativeInt64(bufferStream.Length);
+ }
+
+ context.CachedResponse.Body = bufferStream;
+ _logger.LogResponseCached();
+ await _cache.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor);
+ }
+ else
+ {
+ _logger.LogResponseContentLengthMismatchNotCached();
+ }
+ }
+ else
+ {
+ _logger.LogResponseNotCached();
+ }
+ }
+
+ /// <summary>
+ /// Mark the response as started and set the response time if no reponse was started yet.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <returns><c>true</c> if the response was not started before this call; otherwise <c>false</c>.</returns>
+ private bool OnStartResponse(ResponseCachingContext context)
+ {
+ if (!context.ResponseStarted)
+ {
+ context.ResponseStarted = true;
+ context.ResponseTime = _options.SystemClock.UtcNow;
+
+ return true;
+ }
+ return false;
+ }
+
+ internal void StartResponse(ResponseCachingContext context)
+ {
+ if (OnStartResponse(context))
+ {
+ FinalizeCacheHeaders(context);
+ }
+ }
+
+ internal Task StartResponseAsync(ResponseCachingContext context)
+ {
+ if (OnStartResponse(context))
+ {
+ return FinalizeCacheHeadersAsync(context);
+ }
+ return Task.CompletedTask;
+ }
+
+ internal static void AddResponseCachingFeature(HttpContext context)
+ {
+ if (context.Features.Get<IResponseCachingFeature>() != null)
+ {
+ throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingFeature)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application.");
+ }
+ context.Features.Set<IResponseCachingFeature>(new ResponseCachingFeature());
+ }
+
+ internal void ShimResponseStream(ResponseCachingContext context)
+ {
+ // Shim response stream
+ context.OriginalResponseStream = context.HttpContext.Response.Body;
+ context.ResponseCachingStream = new ResponseCachingStream(
+ context.OriginalResponseStream,
+ _options.MaximumBodySize,
+ StreamUtilities.BodySegmentSize,
+ () => StartResponse(context),
+ () => StartResponseAsync(context));
+ context.HttpContext.Response.Body = context.ResponseCachingStream;
+
+ // Shim IHttpSendFileFeature
+ context.OriginalSendFileFeature = context.HttpContext.Features.Get<IHttpSendFileFeature>();
+ if (context.OriginalSendFileFeature != null)
+ {
+ context.HttpContext.Features.Set<IHttpSendFileFeature>(new SendFileFeatureWrapper(context.OriginalSendFileFeature, context.ResponseCachingStream));
+ }
+
+ // Add IResponseCachingFeature
+ AddResponseCachingFeature(context.HttpContext);
+ }
+
+ internal static void RemoveResponseCachingFeature(HttpContext context) =>
+ context.Features.Set<IResponseCachingFeature>(null);
+
+ internal static void UnshimResponseStream(ResponseCachingContext context)
+ {
+ // Unshim response stream
+ context.HttpContext.Response.Body = context.OriginalResponseStream;
+
+ // Unshim IHttpSendFileFeature
+ context.HttpContext.Features.Set(context.OriginalSendFileFeature);
+
+ // Remove IResponseCachingFeature
+ RemoveResponseCachingFeature(context.HttpContext);
+ }
+
+ internal static bool ContentIsNotModified(ResponseCachingContext context)
+ {
+ var cachedResponseHeaders = context.CachedResponseHeaders;
+ var ifNoneMatchHeader = context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch];
+
+ if (!StringValues.IsNullOrEmpty(ifNoneMatchHeader))
+ {
+ if (ifNoneMatchHeader.Count == 1 && StringSegment.Equals(ifNoneMatchHeader[0], EntityTagHeaderValue.Any.Tag, StringComparison.OrdinalIgnoreCase))
+ {
+ context.Logger.LogNotModifiedIfNoneMatchStar();
+ return true;
+ }
+
+ EntityTagHeaderValue eTag;
+ IList<EntityTagHeaderValue> ifNoneMatchEtags;
+ if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag])
+ && EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag].ToString(), out eTag)
+ && EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out ifNoneMatchEtags))
+ {
+ for (var i = 0; i < ifNoneMatchEtags.Count; i++)
+ {
+ var requestETag = ifNoneMatchEtags[i];
+ if (eTag.Compare(requestETag, useStrongComparison: false))
+ {
+ context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag);
+ return true;
+ }
+ }
+ }
+ }
+ else
+ {
+ var ifModifiedSince = context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince];
+ if (!StringValues.IsNullOrEmpty(ifModifiedSince))
+ {
+ DateTimeOffset modified;
+ if (!HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.LastModified].ToString(), out modified) &&
+ !HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.Date].ToString(), out modified))
+ {
+ return false;
+ }
+
+ DateTimeOffset modifiedSince;
+ if (HeaderUtilities.TryParseDate(ifModifiedSince.ToString(), out modifiedSince) &&
+ modified <= modifiedSince)
+ {
+ context.Logger.LogNotModifiedIfModifiedSinceSatisfied(modified, modifiedSince);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ // Normalize order and casing
+ internal static StringValues GetOrderCasingNormalizedStringValues(StringValues stringValues)
+ {
+ if (stringValues.Count == 1)
+ {
+ return new StringValues(stringValues.ToString().ToUpperInvariant());
+ }
+ else
+ {
+ var originalArray = stringValues.ToArray();
+ var newArray = new string[originalArray.Length];
+
+ for (var i = 0; i < originalArray.Length; i++)
+ {
+ newArray[i] = originalArray[i].ToUpperInvariant();
+ }
+
+ // Since the casing has already been normalized, use Ordinal comparison
+ Array.Sort(newArray, StringComparer.Ordinal);
+
+ return new StringValues(newArray);
+ }
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs
new file mode 100644
index 0000000000..4fa75e2135
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs
@@ -0,0 +1,32 @@
+// 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.ComponentModel;
+using Microsoft.AspNetCore.ResponseCaching.Internal;
+
+namespace Microsoft.AspNetCore.ResponseCaching
+{
+ public class ResponseCachingOptions
+ {
+ /// <summary>
+ /// The size limit for the response cache middleware in bytes. The default is set to 100 MB.
+ /// </summary>
+ public long SizeLimit { get; set; } = 100 * 1024 * 1024;
+
+ /// <summary>
+ /// The largest cacheable size for the response body in bytes. The default is set to 64 MB.
+ /// </summary>
+ public long MaximumBodySize { get; set; } = 64 * 1024 * 1024;
+
+ /// <summary>
+ /// <c>true</c> if request paths are case-sensitive; otherwise <c>false</c>. The default is to treat paths as case-insensitive.
+ /// </summary>
+ public bool UseCaseSensitivePaths { get; set; } = false;
+
+ /// <summary>
+ /// For testing purposes only.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ internal ISystemClock SystemClock { get; set; } = new SystemClock();
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs
new file mode 100644
index 0000000000..ef6f815b5e
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs
@@ -0,0 +1,58 @@
+// 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.ResponseCaching;
+using Microsoft.AspNetCore.ResponseCaching.Internal;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ /// <summary>
+ /// Extension methods for the ResponseCaching middleware.
+ /// </summary>
+ public static class ResponseCachingServicesExtensions
+ {
+ /// <summary>
+ /// Add response caching services.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
+ /// <returns></returns>
+ public static IServiceCollection AddResponseCaching(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.AddMemoryCache();
+ services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingPolicyProvider, ResponseCachingPolicyProvider>());
+ services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingKeyProvider, ResponseCachingKeyProvider>());
+
+ return services;
+ }
+
+ /// <summary>
+ /// Add response caching services and configure the related options.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
+ /// <param name="configureOptions">A delegate to configure the <see cref="ResponseCachingOptions"/>.</param>
+ /// <returns></returns>
+ public static IServiceCollection AddResponseCaching(this IServiceCollection services, Action<ResponseCachingOptions> configureOptions)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+ if (configureOptions == null)
+ {
+ throw new ArgumentNullException(nameof(configureOptions));
+ }
+
+ services.Configure(configureOptions);
+ services.AddResponseCaching();
+
+ return services;
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs
new file mode 100644
index 0000000000..c9d476c97d
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs
@@ -0,0 +1,200 @@
+// 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 System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ internal class ResponseCachingStream : Stream
+ {
+ private readonly Stream _innerStream;
+ private readonly long _maxBufferSize;
+ private readonly int _segmentSize;
+ private SegmentWriteStream _segmentWriteStream;
+ private Action _startResponseCallback;
+ private Func<Task> _startResponseCallbackAsync;
+
+ internal ResponseCachingStream(Stream innerStream, long maxBufferSize, int segmentSize, Action startResponseCallback, Func<Task> startResponseCallbackAsync)
+ {
+ _innerStream = innerStream;
+ _maxBufferSize = maxBufferSize;
+ _segmentSize = segmentSize;
+ _startResponseCallback = startResponseCallback;
+ _startResponseCallbackAsync = startResponseCallbackAsync;
+ _segmentWriteStream = new SegmentWriteStream(_segmentSize);
+ }
+
+ internal bool BufferingEnabled { get; private set; } = true;
+
+ public override bool CanRead => _innerStream.CanRead;
+
+ public override bool CanSeek => _innerStream.CanSeek;
+
+ public override bool CanWrite => _innerStream.CanWrite;
+
+ public override long Length => _innerStream.Length;
+
+ public override long Position
+ {
+ get { return _innerStream.Position; }
+ set
+ {
+ DisableBuffering();
+ _innerStream.Position = value;
+ }
+ }
+
+ internal Stream GetBufferStream()
+ {
+ if (!BufferingEnabled)
+ {
+ throw new InvalidOperationException("Buffer stream cannot be retrieved since buffering is disabled.");
+ }
+ return new SegmentReadStream(_segmentWriteStream.GetSegments(), _segmentWriteStream.Length);
+ }
+
+ internal void DisableBuffering()
+ {
+ BufferingEnabled = false;
+ _segmentWriteStream.Dispose();
+ }
+
+ public override void SetLength(long value)
+ {
+ DisableBuffering();
+ _innerStream.SetLength(value);
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ DisableBuffering();
+ return _innerStream.Seek(offset, origin);
+ }
+
+ public override void Flush()
+ {
+ try
+ {
+ _startResponseCallback();
+ _innerStream.Flush();
+ }
+ catch
+ {
+ DisableBuffering();
+ throw;
+ }
+ }
+
+ public override async Task FlushAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ await _startResponseCallbackAsync();
+ await _innerStream.FlushAsync();
+ }
+ catch
+ {
+ DisableBuffering();
+ throw;
+ }
+ }
+
+ // Underlying stream is write-only, no need to override other read related methods
+ public override int Read(byte[] buffer, int offset, int count)
+ => _innerStream.Read(buffer, offset, count);
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ try
+ {
+ _startResponseCallback();
+ _innerStream.Write(buffer, offset, count);
+ }
+ catch
+ {
+ DisableBuffering();
+ throw;
+ }
+
+ if (BufferingEnabled)
+ {
+ if (_segmentWriteStream.Length + count > _maxBufferSize)
+ {
+ DisableBuffering();
+ }
+ else
+ {
+ _segmentWriteStream.Write(buffer, offset, count);
+ }
+ }
+ }
+
+ public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ try
+ {
+ await _startResponseCallbackAsync();
+ await _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
+ }
+ catch
+ {
+ DisableBuffering();
+ throw;
+ }
+
+ if (BufferingEnabled)
+ {
+ if (_segmentWriteStream.Length + count > _maxBufferSize)
+ {
+ DisableBuffering();
+ }
+ else
+ {
+ await _segmentWriteStream.WriteAsync(buffer, offset, count, cancellationToken);
+ }
+ }
+ }
+
+ public override void WriteByte(byte value)
+ {
+ try
+ {
+ _innerStream.WriteByte(value);
+ }
+ catch
+ {
+ DisableBuffering();
+ throw;
+ }
+
+ if (BufferingEnabled)
+ {
+ if (_segmentWriteStream.Length + 1 > _maxBufferSize)
+ {
+ DisableBuffering();
+ }
+ else
+ {
+ _segmentWriteStream.WriteByte(value);
+ }
+ }
+ }
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ return StreamUtilities.ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state);
+ }
+
+ public override void EndWrite(IAsyncResult asyncResult)
+ {
+ if (asyncResult == null)
+ {
+ throw new ArgumentNullException(nameof(asyncResult));
+ }
+ ((Task)asyncResult).GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs
new file mode 100644
index 0000000000..83c60dd0c5
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs
@@ -0,0 +1,230 @@
+// 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.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ internal class SegmentReadStream : Stream
+ {
+ private readonly List<byte[]> _segments;
+ private readonly long _length;
+ private int _segmentIndex;
+ private int _segmentOffset;
+ private long _position;
+
+ internal SegmentReadStream(List<byte[]> segments, long length)
+ {
+ if (segments == null)
+ {
+ throw new ArgumentNullException(nameof(segments));
+ }
+
+ _segments = segments;
+ _length = length;
+ }
+
+ public override bool CanRead => true;
+
+ public override bool CanSeek => true;
+
+ public override bool CanWrite => false;
+
+ public override long Length => _length;
+
+ public override long Position
+ {
+ get
+ {
+ return _position;
+ }
+ set
+ {
+ // The stream only supports a full rewind. This will need an update if random access becomes a required feature.
+ if (value != 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(Position)} can only be set to 0.");
+ }
+
+ _position = 0;
+ _segmentOffset = 0;
+ _segmentIndex = 0;
+ }
+ }
+
+ public override void Flush()
+ {
+ throw new NotSupportedException("The stream does not support writing.");
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, "Non-negative number required.");
+ }
+ // Read of length 0 will return zero and indicate end of stream.
+ if (count <= 0 )
+ {
+ throw new ArgumentOutOfRangeException(nameof(count), count, "Positive number required.");
+ }
+ if (count > buffer.Length - offset)
+ {
+ throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection.");
+ }
+
+ if (_segmentIndex == _segments.Count)
+ {
+ return 0;
+ }
+
+ var bytesRead = 0;
+ while (count > 0)
+ {
+ if (_segmentOffset == _segments[_segmentIndex].Length)
+ {
+ // Move to the next segment
+ _segmentIndex++;
+ _segmentOffset = 0;
+
+ if (_segmentIndex == _segments.Count)
+ {
+ break;
+ }
+ }
+
+ // Read up to the end of the segment
+ var segmentBytesRead = Math.Min(count, _segments[_segmentIndex].Length - _segmentOffset);
+ Buffer.BlockCopy(_segments[_segmentIndex], _segmentOffset, buffer, offset, segmentBytesRead);
+ bytesRead += segmentBytesRead;
+ _segmentOffset += segmentBytesRead;
+ _position += segmentBytesRead;
+ offset += segmentBytesRead;
+ count -= segmentBytesRead;
+ }
+
+ return bytesRead;
+ }
+
+ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(Read(buffer, offset, count));
+ }
+
+ public override int ReadByte()
+ {
+ if (Position == Length)
+ {
+ return -1;
+ }
+
+ if (_segmentOffset == _segments[_segmentIndex].Length)
+ {
+ // Move to the next segment
+ _segmentIndex++;
+ _segmentOffset = 0;
+ }
+
+ var byteRead = _segments[_segmentIndex][_segmentOffset];
+ _segmentOffset++;
+ _position++;
+
+ return byteRead;
+ }
+
+ public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ var tcs = new TaskCompletionSource<int>(state);
+
+ try
+ {
+ tcs.TrySetResult(Read(buffer, offset, count));
+ }
+ catch (Exception ex)
+ {
+ tcs.TrySetException(ex);
+ }
+
+ if (callback != null)
+ {
+ // Offload callbacks to avoid stack dives on sync completions.
+ var ignored = Task.Run(() =>
+ {
+ try
+ {
+ callback(tcs.Task);
+ }
+ catch (Exception)
+ {
+ // Suppress exceptions on background threads.
+ }
+ });
+ }
+
+ return tcs.Task;
+ }
+
+ public override int EndRead(IAsyncResult asyncResult)
+ {
+ if (asyncResult == null)
+ {
+ throw new ArgumentNullException(nameof(asyncResult));
+ }
+ return ((Task<int>)asyncResult).GetAwaiter().GetResult();
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ // The stream only supports a full rewind. This will need an update if random access becomes a required feature.
+ if (origin != SeekOrigin.Begin)
+ {
+ throw new ArgumentException(nameof(origin), $"{nameof(Seek)} can only be set to {nameof(SeekOrigin.Begin)}.");
+ }
+ if (offset != 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, $"{nameof(Seek)} can only be set to 0.");
+ }
+
+ Position = 0;
+ return Position;
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException("The stream does not support writing.");
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException("The stream does not support writing.");
+ }
+
+ public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
+ {
+ if (destination == null)
+ {
+ throw new ArgumentNullException(nameof(destination));
+ }
+ if (!destination.CanWrite)
+ {
+ throw new NotSupportedException("The destination stream does not support writing.");
+ }
+
+ for (; _segmentIndex < _segments.Count; _segmentIndex++, _segmentOffset = 0)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var bytesCopied = _segments[_segmentIndex].Length - _segmentOffset;
+ await destination.WriteAsync(_segments[_segmentIndex], _segmentOffset, bytesCopied, cancellationToken);
+ _position += bytesCopied;
+ }
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs
new file mode 100644
index 0000000000..81df72a9d1
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs
@@ -0,0 +1,206 @@
+// 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.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ internal class SegmentWriteStream : Stream
+ {
+ private readonly List<byte[]> _segments = new List<byte[]>();
+ private readonly MemoryStream _bufferStream = new MemoryStream();
+ private readonly int _segmentSize;
+ private long _length;
+ private bool _closed;
+ private bool _disposed;
+
+ internal SegmentWriteStream(int segmentSize)
+ {
+ if (segmentSize <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(segmentSize), segmentSize, $"{nameof(segmentSize)} must be greater than 0.");
+ }
+
+ _segmentSize = segmentSize;
+ }
+
+ // Extracting the buffered segments closes the stream for writing
+ internal List<byte[]> GetSegments()
+ {
+ if (!_closed)
+ {
+ _closed = true;
+ FinalizeSegments();
+ }
+ return _segments;
+ }
+
+ public override bool CanRead => false;
+
+ public override bool CanSeek => false;
+
+ public override bool CanWrite => !_closed;
+
+ public override long Length => _length;
+
+ public override long Position
+ {
+ get
+ {
+ return _length;
+ }
+ set
+ {
+ throw new NotSupportedException("The stream does not support seeking.");
+ }
+ }
+
+ private void DisposeMemoryStream()
+ {
+ // Clean up the memory stream
+ _bufferStream.SetLength(0);
+ _bufferStream.Capacity = 0;
+ _bufferStream.Dispose();
+ }
+
+ private void FinalizeSegments()
+ {
+ // Append any remaining segments
+ if (_bufferStream.Length > 0)
+ {
+ // Add the last segment
+ _segments.Add(_bufferStream.ToArray());
+ }
+
+ DisposeMemoryStream();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ try
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ _segments.Clear();
+ DisposeMemoryStream();
+ }
+
+ _disposed = true;
+ _closed = true;
+ }
+ finally
+ {
+ base.Dispose(disposing);
+ }
+ }
+
+ public override void Flush()
+ {
+ if (!CanWrite)
+ {
+ throw new ObjectDisposedException("The stream has been closed for writing.");
+ }
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException("The stream does not support reading.");
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException("The stream does not support seeking.");
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException("The stream does not support seeking.");
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+ if (offset < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, "Non-negative number required.");
+ }
+ if (count < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count), count, "Non-negative number required.");
+ }
+ if (count > buffer.Length - offset)
+ {
+ throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection.");
+ }
+ if (!CanWrite)
+ {
+ throw new ObjectDisposedException("The stream has been closed for writing.");
+ }
+
+ while (count > 0)
+ {
+ if ((int)_bufferStream.Length == _segmentSize)
+ {
+ _segments.Add(_bufferStream.ToArray());
+ _bufferStream.SetLength(0);
+ }
+
+ var bytesWritten = Math.Min(count, _segmentSize - (int)_bufferStream.Length);
+
+ _bufferStream.Write(buffer, offset, bytesWritten);
+ count -= bytesWritten;
+ offset += bytesWritten;
+ _length += bytesWritten;
+ }
+ }
+
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ Write(buffer, offset, count);
+ return Task.CompletedTask;
+ }
+
+ public override void WriteByte(byte value)
+ {
+ if (!CanWrite)
+ {
+ throw new ObjectDisposedException("The stream has been closed for writing.");
+ }
+
+ if ((int)_bufferStream.Length == _segmentSize)
+ {
+ _segments.Add(_bufferStream.ToArray());
+ _bufferStream.SetLength(0);
+ }
+
+ _bufferStream.WriteByte(value);
+ _length++;
+ }
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ return StreamUtilities.ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state);
+ }
+
+ public override void EndWrite(IAsyncResult asyncResult)
+ {
+ if (asyncResult == null)
+ {
+ throw new ArgumentNullException(nameof(asyncResult));
+ }
+ ((Task)asyncResult).GetAwaiter().GetResult();
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs
new file mode 100644
index 0000000000..d128a9f8f2
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs
@@ -0,0 +1,41 @@
+// 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.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ internal static class StreamUtilities
+ {
+ /// <summary>
+ /// The segment size for buffering the response body in bytes. The default is set to 80 KB (81920 Bytes) to avoid allocations on the LOH.
+ /// </summary>
+ // Internal for testing
+ internal static int BodySegmentSize { get; set; } = 81920;
+
+ internal static IAsyncResult ToIAsyncResult(Task task, AsyncCallback callback, object state)
+ {
+ var tcs = new TaskCompletionSource<int>(state);
+ task.ContinueWith(t =>
+ {
+ if (t.IsFaulted)
+ {
+ tcs.TrySetException(t.Exception.InnerExceptions);
+ }
+ else if (t.IsCanceled)
+ {
+ tcs.TrySetCanceled();
+ }
+ else
+ {
+ tcs.TrySetResult(0);
+ }
+
+ callback?.Invoke(tcs.Task);
+ }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
+ return tcs.Task;
+ }
+ }
+}
diff --git a/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json
new file mode 100644
index 0000000000..9bec30264e
--- /dev/null
+++ b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json
@@ -0,0 +1,252 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.ResponseCaching, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.ResponseCachingServicesExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddResponseCaching",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddResponseCaching",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.ResponseCaching.ResponseCachingOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.ResponseCachingExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseResponseCaching",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.ResponseCaching.ResponseCachingFeature",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.ResponseCaching.IResponseCachingFeature"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_VaryByQueryKeys",
+ "Parameters": [],
+ "ReturnType": "System.String[]",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.ResponseCaching.IResponseCachingFeature",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_VaryByQueryKeys",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String[]"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.ResponseCaching.IResponseCachingFeature",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.ResponseCaching.ResponseCachingMiddleware",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Invoke",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "next",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.ResponseCaching.ResponseCachingOptions>"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "policyProvider",
+ "Type": "Microsoft.AspNetCore.ResponseCaching.Internal.IResponseCachingPolicyProvider"
+ },
+ {
+ "Name": "keyProvider",
+ "Type": "Microsoft.AspNetCore.ResponseCaching.Internal.IResponseCachingKeyProvider"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.ResponseCaching.ResponseCachingOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_SizeLimit",
+ "Parameters": [],
+ "ReturnType": "System.Int64",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SizeLimit",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Int64"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_MaximumBodySize",
+ "Parameters": [],
+ "ReturnType": "System.Int64",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_MaximumBodySize",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Int64"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_UseCaseSensitivePaths",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_UseCaseSensitivePaths",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/ResponseCaching/test/Directory.Build.props b/src/ResponseCaching/test/Directory.Build.props
new file mode 100644
index 0000000000..270e1fa209
--- /dev/null
+++ b/src/ResponseCaching/test/Directory.Build.props
@@ -0,0 +1,14 @@
+<Project>
+ <Import Project="..\Directory.Build.props" />
+
+ <PropertyGroup>
+ <DeveloperBuildTestTfms>netcoreapp2.1</DeveloperBuildTestTfms>
+ <StandardTestTfms>$(DeveloperBuildTestTfms)</StandardTestTfms>
+ <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' ">netcoreapp2.1;netcoreapp2.0</StandardTestTfms>
+ <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' AND '$(OS)' == 'Windows_NT' ">$(StandardTestTfms);net461</StandardTestTfms>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj
new file mode 100644
index 0000000000..79223f0c73
--- /dev/null
+++ b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.ResponseCaching.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
+ <PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs
new file mode 100644
index 0000000000..3d5b57bf65
--- /dev/null
+++ b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs
@@ -0,0 +1,59 @@
+// 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.ResponseCaching.Tests
+{
+ public class ResponseCachingFeatureTests
+ {
+ public static TheoryData<string[]> ValidNullOrEmptyVaryRules
+ {
+ get
+ {
+ return new TheoryData<string[]>
+ {
+ null,
+ new string[0],
+ new string[] { null },
+ new string[] { string.Empty }
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(ValidNullOrEmptyVaryRules))]
+ public void VaryByQueryKeys_Set_ValidEmptyValues_Succeeds(string[] value)
+ {
+ // Does not throw
+ new ResponseCachingFeature().VaryByQueryKeys = value;
+ }
+
+ public static TheoryData<string[]> InvalidVaryRules
+ {
+ get
+ {
+ return new TheoryData<string[]>
+ {
+ new string[] { null, null },
+ new string[] { null, string.Empty },
+ new string[] { string.Empty, null },
+ new string[] { string.Empty, "Valid" },
+ new string[] { "Valid", string.Empty },
+ new string[] { null, "Valid" },
+ new string[] { "Valid", null }
+ };
+ }
+ }
+
+
+ [Theory]
+ [MemberData(nameof(InvalidVaryRules))]
+ public void VaryByQueryKeys_Set_InValidEmptyValues_Throws(string[] value)
+ {
+ // Throws
+ Assert.Throws<ArgumentException>(() => new ResponseCachingFeature().VaryByQueryKeys = value);
+ }
+ }
+}
diff --git a/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs
new file mode 100644
index 0000000000..36bd3da0c8
--- /dev/null
+++ b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs
@@ -0,0 +1,218 @@
+// 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;
+using Microsoft.AspNetCore.ResponseCaching.Internal;
+using Xunit;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Tests
+{
+ public class ResponseCachingKeyProviderTests
+ {
+ private static readonly char KeyDelimiter = '\x1e';
+ private static readonly char KeySubDelimiter = '\x1f';
+
+ [Fact]
+ public void ResponseCachingKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodSchemeHostPortAndPath()
+ {
+ var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+ var context = TestUtils.CreateTestContext();
+ context.HttpContext.Request.Method = "head";
+ context.HttpContext.Request.Path = "/path/subpath";
+ context.HttpContext.Request.Scheme = "https";
+ context.HttpContext.Request.Host = new HostString("example.com", 80);
+ context.HttpContext.Request.PathBase = "/pathBase";
+ context.HttpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b");
+
+ Assert.Equal($"HEAD{KeyDelimiter}HTTPS{KeyDelimiter}EXAMPLE.COM:80/PATHBASE/PATH/SUBPATH", cacheKeyProvider.CreateBaseKey(context));
+ }
+
+ [Fact]
+ public void ResponseCachingKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath()
+ {
+ var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions()
+ {
+ UseCaseSensitivePaths = false
+ });
+ var context = TestUtils.CreateTestContext();
+ context.HttpContext.Request.Method = HttpMethods.Get;
+ context.HttpContext.Request.Path = "/Path";
+
+ Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/PATH", cacheKeyProvider.CreateBaseKey(context));
+ }
+
+ [Fact]
+ public void ResponseCachingKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase()
+ {
+ var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions()
+ {
+ UseCaseSensitivePaths = true
+ });
+ var context = TestUtils.CreateTestContext();
+ context.HttpContext.Request.Method = HttpMethods.Get;
+ context.HttpContext.Request.Path = "/Path";
+
+ Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/Path", cacheKeyProvider.CreateBaseKey(context));
+ }
+
+ [Fact]
+ public void ResponseCachingKeyProvider_CreateStorageVaryByKey_Throws_IfVaryByRulesIsNull()
+ {
+ var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+ var context = TestUtils.CreateTestContext();
+
+ Assert.Throws<InvalidOperationException>(() => cacheKeyProvider.CreateStorageVaryByKey(context));
+ }
+
+ [Fact]
+ public void ResponseCachingKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryByGuid_IfVaryByRulesIsEmpty()
+ {
+ var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+ var context = TestUtils.CreateTestContext();
+ context.CachedVaryByRules = new CachedVaryByRules()
+ {
+ VaryByKeyPrefix = FastGuid.NewGuid().IdString
+ };
+
+ Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}", cacheKeyProvider.CreateStorageVaryByKey(context));
+ }
+
+ [Fact]
+ public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly()
+ {
+ var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+ var context = TestUtils.CreateTestContext();
+ context.HttpContext.Request.Headers["HeaderA"] = "ValueA";
+ context.HttpContext.Request.Headers["HeaderB"] = "ValueB";
+ context.CachedVaryByRules = new CachedVaryByRules()
+ {
+ Headers = new string[] { "HeaderA", "HeaderC" }
+ };
+
+ Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=",
+ cacheKeyProvider.CreateStorageVaryByKey(context));
+ }
+
+ [Fact]
+ public void ResponseCachingKeyProvider_CreateStorageVaryKey_HeaderValuesAreSorted()
+ {
+ var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+ var context = TestUtils.CreateTestContext();
+ context.HttpContext.Request.Headers["HeaderA"] = "ValueB";
+ context.HttpContext.Request.Headers.Append("HeaderA", "ValueA");
+ context.CachedVaryByRules = new CachedVaryByRules()
+ {
+ Headers = new string[] { "HeaderA", "HeaderC" }
+ };
+
+ Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueAValueB{KeyDelimiter}HeaderC=",
+ cacheKeyProvider.CreateStorageVaryByKey(context));
+ }
+
+ [Fact]
+ public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedQueryKeysOnly()
+ {
+ var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+ var context = TestUtils.CreateTestContext();
+ context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB");
+ context.CachedVaryByRules = new CachedVaryByRules()
+ {
+ VaryByKeyPrefix = FastGuid.NewGuid().IdString,
+ QueryKeys = new string[] { "QueryA", "QueryC" }
+ };
+
+ Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=",
+ cacheKeyProvider.CreateStorageVaryByKey(context));
+ }
+
+ [Fact]
+ public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesQueryKeys_QueryKeyCaseInsensitive_UseQueryKeyCasing()
+ {
+ var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+ var context = TestUtils.CreateTestContext();
+ context.HttpContext.Request.QueryString = new QueryString("?queryA=ValueA&queryB=ValueB");
+ context.CachedVaryByRules = new CachedVaryByRules()
+ {
+ VaryByKeyPrefix = FastGuid.NewGuid().IdString,
+ QueryKeys = new string[] { "QueryA", "QueryC" }
+ };
+
+ Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=",
+ cacheKeyProvider.CreateStorageVaryByKey(context));
+ }
+
+ [Fact]
+ public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesAllQueryKeysGivenAsterisk()
+ {
+ var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+ var context = TestUtils.CreateTestContext();
+ context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB");
+ context.CachedVaryByRules = new CachedVaryByRules()
+ {
+ VaryByKeyPrefix = FastGuid.NewGuid().IdString,
+ QueryKeys = new string[] { "*" }
+ };
+
+ // To support case insensitivity, all query keys are converted to upper case.
+ // Explicit query keys uses the casing specified in the setting.
+ Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeyDelimiter}QUERYB=ValueB",
+ cacheKeyProvider.CreateStorageVaryByKey(context));
+ }
+
+ [Fact]
+ public void ResponseCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesNotConsolidated()
+ {
+ var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+ var context = TestUtils.CreateTestContext();
+ context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryA=ValueB");
+ context.CachedVaryByRules = new CachedVaryByRules()
+ {
+ VaryByKeyPrefix = FastGuid.NewGuid().IdString,
+ QueryKeys = new string[] { "*" }
+ };
+
+ // To support case insensitivity, all query keys are converted to upper case.
+ // Explicit query keys uses the casing specified in the setting.
+ Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB",
+ cacheKeyProvider.CreateStorageVaryByKey(context));
+ }
+
+ [Fact]
+ public void ResponseCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesAreSorted()
+ {
+ var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+ var context = TestUtils.CreateTestContext();
+ context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueB&QueryA=ValueA");
+ context.CachedVaryByRules = new CachedVaryByRules()
+ {
+ VaryByKeyPrefix = FastGuid.NewGuid().IdString,
+ QueryKeys = new string[] { "*" }
+ };
+
+ // To support case insensitivity, all query keys are converted to upper case.
+ // Explicit query keys uses the casing specified in the setting.
+ Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB",
+ cacheKeyProvider.CreateStorageVaryByKey(context));
+ }
+
+ [Fact]
+ public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys()
+ {
+ var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+ var context = TestUtils.CreateTestContext();
+ context.HttpContext.Request.Headers["HeaderA"] = "ValueA";
+ context.HttpContext.Request.Headers["HeaderB"] = "ValueB";
+ context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB");
+ context.CachedVaryByRules = new CachedVaryByRules()
+ {
+ VaryByKeyPrefix = FastGuid.NewGuid().IdString,
+ Headers = new string[] { "HeaderA", "HeaderC" },
+ QueryKeys = new string[] { "QueryA", "QueryC" }
+ };
+
+ Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=",
+ cacheKeyProvider.CreateStorageVaryByKey(context));
+ }
+ }
+}
diff --git a/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs
new file mode 100644
index 0000000000..831e9ea67e
--- /dev/null
+++ b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs
@@ -0,0 +1,940 @@
+// 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.Features;
+using Microsoft.AspNetCore.ResponseCaching.Internal;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Tests
+{
+ public class ResponseCachingMiddlewareTests
+ {
+ [Fact]
+ public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider());
+ var context = TestUtils.CreateTestContext();
+ context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ OnlyIfCached = true
+ }.ToString();
+
+ Assert.True(await middleware.TryServeFromCacheAsync(context));
+ Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.GatewayTimeoutServed);
+ }
+
+ [Fact]
+ public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey"));
+ var context = TestUtils.CreateTestContext();
+
+ Assert.False(await middleware.TryServeFromCacheAsync(context));
+ Assert.Equal(1, cache.GetCount);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.NoResponseServed);
+ }
+
+ [Fact]
+ public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey"));
+ var context = TestUtils.CreateTestContext();
+
+ await cache.SetAsync(
+ "BaseKey",
+ new CachedResponse()
+ {
+ Headers = new HeaderDictionary(),
+ Body = new SegmentReadStream(new List<byte[]>(0), 0)
+ },
+ TimeSpan.Zero);
+
+ Assert.True(await middleware.TryServeFromCacheAsync(context));
+ Assert.Equal(1, cache.GetCount);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.CachedResponseServed);
+ }
+
+ [Fact]
+ public async Task TryServeFromCacheAsync_CachedResponseFound_OverwritesExistingHeaders()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey"));
+ var context = TestUtils.CreateTestContext();
+
+ context.HttpContext.Response.Headers["MyHeader"] = "OldValue";
+ await cache.SetAsync(
+ "BaseKey",
+ new CachedResponse()
+ {
+ Headers = new HeaderDictionary()
+ {
+ { "MyHeader", "NewValue" }
+ },
+ Body = new SegmentReadStream(new List<byte[]>(0), 0)
+ },
+ TimeSpan.Zero);
+
+ Assert.True(await middleware.TryServeFromCacheAsync(context));
+ Assert.Equal("NewValue", context.HttpContext.Response.Headers["MyHeader"]);
+ Assert.Equal(1, cache.GetCount);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.CachedResponseServed);
+ }
+
+ [Fact]
+ public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseNotFound_Fails()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey", "VaryKey"));
+ var context = TestUtils.CreateTestContext();
+
+ await cache.SetAsync(
+ "BaseKey",
+ new CachedVaryByRules(),
+ TimeSpan.Zero);
+
+ Assert.False(await middleware.TryServeFromCacheAsync(context));
+ Assert.Equal(2, cache.GetCount);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.NoResponseServed);
+ }
+
+ [Fact]
+ public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseFound_Succeeds()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey", new[] { "VaryKey", "VaryKey2" }));
+ var context = TestUtils.CreateTestContext();
+
+ await cache.SetAsync(
+ "BaseKey",
+ new CachedVaryByRules(),
+ TimeSpan.Zero);
+ await cache.SetAsync(
+ "BaseKeyVaryKey2",
+ new CachedResponse()
+ {
+ Headers = new HeaderDictionary(),
+ Body = new SegmentReadStream(new List<byte[]>(0), 0)
+ },
+ TimeSpan.Zero);
+
+ Assert.True(await middleware.TryServeFromCacheAsync(context));
+ Assert.Equal(3, cache.GetCount);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.CachedResponseServed);
+ }
+
+ [Fact]
+ public async Task TryServeFromCacheAsync_CachedResponseFound_Serves304IfPossible()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey"));
+ var context = TestUtils.CreateTestContext();
+ context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "*";
+
+ await cache.SetAsync(
+ "BaseKey",
+ new CachedResponse()
+ {
+ Body = new SegmentReadStream(new List<byte[]>(0), 0)
+ },
+ TimeSpan.Zero);
+
+ Assert.True(await middleware.TryServeFromCacheAsync(context));
+ Assert.Equal(1, cache.GetCount);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.NotModifiedServed);
+ }
+
+ [Fact]
+ public void ContentIsNotModified_NotConditionalRequest_False()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.CachedResponseHeaders = new HeaderDictionary();
+
+ Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void ContentIsNotModified_IfModifiedSince_FallsbackToDateHeader()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.CachedResponseHeaders = new HeaderDictionary();
+
+ context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow);
+
+ // Verify modifications in the past succeeds
+ context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10));
+ Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context));
+ Assert.Single(sink.Writes);
+
+ // Verify modifications at present succeeds
+ context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
+ Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context));
+ Assert.Equal(2, sink.Writes.Count);
+
+ // Verify modifications in the future fails
+ context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10));
+ Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context));
+
+ // Verify logging
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.NotModifiedIfModifiedSinceSatisfied,
+ LoggedMessage.NotModifiedIfModifiedSinceSatisfied);
+ }
+
+ [Fact]
+ public void ContentIsNotModified_IfModifiedSince_LastModifiedOverridesDateHeader()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.CachedResponseHeaders = new HeaderDictionary();
+
+ context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow);
+
+ // Verify modifications in the past succeeds
+ context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10));
+ context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10));
+ Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context));
+ Assert.Single(sink.Writes);
+
+ // Verify modifications at present
+ context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10));
+ context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow);
+ Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context));
+ Assert.Equal(2, sink.Writes.Count);
+
+ // Verify modifications in the future fails
+ context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10));
+ context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10));
+ Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context));
+
+ // Verify logging
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.NotModifiedIfModifiedSinceSatisfied,
+ LoggedMessage.NotModifiedIfModifiedSinceSatisfied);
+ }
+
+ [Fact]
+ public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToTrue()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.CachedResponseHeaders = new HeaderDictionary();
+
+ // This would fail the IfModifiedSince checks
+ context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow);
+ context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10));
+
+ context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = EntityTagHeaderValue.Any.ToString();
+ Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.NotModifiedIfNoneMatchStar);
+ }
+
+ [Fact]
+ public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToFalse()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.CachedResponseHeaders = new HeaderDictionary();
+
+ // This would pass the IfModifiedSince checks
+ context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow);
+ context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10));
+
+ context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\"";
+ Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void ContentIsNotModified_IfNoneMatch_AnyWithoutETagInResponse_False()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\"";
+
+ Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ public static TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> EquivalentWeakETags
+ {
+ get
+ {
+ return new TheoryData<EntityTagHeaderValue, EntityTagHeaderValue>
+ {
+ { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"") },
+ { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"") },
+ { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"", true) },
+ { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"", true) }
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(EquivalentWeakETags))]
+ public void ContentIsNotModified_IfNoneMatch_ExplicitWithMatch_True(EntityTagHeaderValue responseETag, EntityTagHeaderValue requestETag)
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.ETag] = responseETag.ToString();
+ context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = requestETag.ToString();
+
+ Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.NotModifiedIfNoneMatchMatched);
+ }
+
+ [Fact]
+ public void ContentIsNotModified_IfNoneMatch_ExplicitWithoutMatch_False()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\"";
+ context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\"";
+
+ Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\"";
+ context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new string[] { "\"E0\", \"E1\"", "\"E1\", \"E2\"" };
+
+ Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.NotModifiedIfNoneMatchMatched);
+ }
+
+ [Fact]
+ public async Task StartResponsegAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime()
+ {
+ var clock = new TestClock
+ {
+ UtcNow = DateTimeOffset.UtcNow
+ };
+ var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions
+ {
+ SystemClock = clock
+ });
+ var context = TestUtils.CreateTestContext();
+ context.ResponseTime = null;
+
+ await middleware.StartResponseAsync(context);
+
+ Assert.Equal(clock.UtcNow, context.ResponseTime);
+ }
+
+ [Fact]
+ public async Task StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnlyOnce()
+ {
+ var clock = new TestClock
+ {
+ UtcNow = DateTimeOffset.UtcNow
+ };
+ var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions
+ {
+ SystemClock = clock
+ });
+ var context = TestUtils.CreateTestContext();
+ var initialTime = clock.UtcNow;
+ context.ResponseTime = null;
+
+ await middleware.StartResponseAsync(context);
+ Assert.Equal(initialTime, context.ResponseTime);
+
+ clock.UtcNow += TimeSpan.FromSeconds(10);
+
+ await middleware.StartResponseAsync(context);
+ Assert.NotEqual(clock.UtcNow, context.ResponseTime);
+ Assert.Equal(initialTime, context.ResponseTime);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheHeadersAsync_UpdateShouldCacheResponse_IfResponseCacheable()
+ {
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider());
+ var context = TestUtils.CreateTestContext();
+
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true
+ }.ToString();
+
+ Assert.False(context.ShouldCacheResponse);
+
+ await middleware.FinalizeCacheHeadersAsync(context);
+
+ Assert.True(context.ShouldCacheResponse);
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheHeadersAsync_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable()
+ {
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider());
+ var context = TestUtils.CreateTestContext();
+
+ middleware.ShimResponseStream(context);
+
+ await middleware.FinalizeCacheHeadersAsync(context);
+
+ Assert.False(context.ShouldCacheResponse);
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheHeadersAsync_DefaultResponseValidity_Is10Seconds()
+ {
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
+ var context = TestUtils.CreateTestContext();
+
+ await middleware.FinalizeCacheHeadersAsync(context);
+
+ Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor);
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheHeadersAsync_ResponseValidity_UseExpiryIfAvailable()
+ {
+ var clock = new TestClock
+ {
+ UtcNow = DateTimeOffset.MinValue
+ };
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
+ {
+ SystemClock = clock
+ });
+ var context = TestUtils.CreateTestContext();
+
+ context.ResponseTime = clock.UtcNow;
+ context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11));
+
+ await middleware.FinalizeCacheHeadersAsync(context);
+
+ Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor);
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable()
+ {
+ var clock = new TestClock
+ {
+ UtcNow = DateTimeOffset.UtcNow
+ };
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
+ {
+ SystemClock = clock
+ });
+ var context = TestUtils.CreateTestContext();
+
+ context.ResponseTime = clock.UtcNow;
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(12)
+ }.ToString();
+
+ context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11));
+
+ await middleware.FinalizeCacheHeadersAsync(context);
+
+ Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor);
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheHeadersAsync_ResponseValidity_UseSharedMaxAgeIfAvailable()
+ {
+ var clock = new TestClock
+ {
+ UtcNow = DateTimeOffset.UtcNow
+ };
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
+ {
+ SystemClock = clock
+ });
+ var context = TestUtils.CreateTestContext();
+
+ context.ResponseTime = clock.UtcNow;
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(12),
+ SharedMaxAge = TimeSpan.FromSeconds(13)
+ }.ToString();
+ context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11));
+
+ await middleware.FinalizeCacheHeadersAsync(context);
+
+ Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor);
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfNotEquivalentToPrevious()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
+ var context = TestUtils.CreateTestContext();
+
+ context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" });
+ context.HttpContext.Features.Set<IResponseCachingFeature>(new ResponseCachingFeature()
+ {
+ VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" })
+ });
+ var cachedVaryByRules = new CachedVaryByRules()
+ {
+ Headers = new StringValues(new[] { "HeaderA", "HeaderB" }),
+ QueryKeys = new StringValues(new[] { "QueryA", "QueryB" })
+ };
+ context.CachedVaryByRules = cachedVaryByRules;
+
+ await middleware.FinalizeCacheHeadersAsync(context);
+
+ Assert.Equal(1, cache.SetCount);
+ Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.VaryByRulesUpdated);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfEquivalentToPrevious()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
+ var context = TestUtils.CreateTestContext();
+
+ context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" });
+ context.HttpContext.Features.Set<IResponseCachingFeature>(new ResponseCachingFeature()
+ {
+ VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" })
+ });
+ var cachedVaryByRules = new CachedVaryByRules()
+ {
+ VaryByKeyPrefix = FastGuid.NewGuid().IdString,
+ Headers = new StringValues(new[] { "HEADERA", "HEADERB" }),
+ QueryKeys = new StringValues(new[] { "QUERYA", "QUERYB" })
+ };
+ context.CachedVaryByRules = cachedVaryByRules;
+
+ await middleware.FinalizeCacheHeadersAsync(context);
+
+ // An update to the cache is always made but the entry should be the same
+ Assert.Equal(1, cache.SetCount);
+ Assert.Same(cachedVaryByRules, context.CachedVaryByRules);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.VaryByRulesUpdated);
+ }
+
+ public static TheoryData<StringValues> NullOrEmptyVaryRules
+ {
+ get
+ {
+ return new TheoryData<StringValues>
+ {
+ default(StringValues),
+ StringValues.Empty,
+ new StringValues((string)null),
+ new StringValues(string.Empty),
+ new StringValues((string[])null),
+ new StringValues(new string[0]),
+ new StringValues(new string[] { null }),
+ new StringValues(new string[] { string.Empty })
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(NullOrEmptyVaryRules))]
+ public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_NullOrEmptyRules(StringValues vary)
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
+ var context = TestUtils.CreateTestContext();
+
+ context.HttpContext.Response.Headers[HeaderNames.Vary] = vary;
+ context.HttpContext.Features.Set<IResponseCachingFeature>(new ResponseCachingFeature()
+ {
+ VaryByQueryKeys = vary
+ });
+
+ await middleware.FinalizeCacheHeadersAsync(context);
+
+ // Vary rules should not be updated
+ Assert.Equal(0, cache.SetCount);
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheHeadersAsync_AddsDate_IfNoneSpecified()
+ {
+ var clock = new TestClock
+ {
+ UtcNow = DateTimeOffset.UtcNow
+ };
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
+ {
+ SystemClock = clock
+ });
+ var context = TestUtils.CreateTestContext();
+
+ Assert.True(StringValues.IsNullOrEmpty(context.HttpContext.Response.Headers[HeaderNames.Date]));
+
+ await middleware.FinalizeCacheHeadersAsync(context);
+
+ Assert.Equal(HeaderUtilities.FormatDate(clock.UtcNow), context.HttpContext.Response.Headers[HeaderNames.Date]);
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheHeadersAsync_DoNotAddDate_IfSpecified()
+ {
+ var utcNow = DateTimeOffset.MinValue;
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
+ var context = TestUtils.CreateTestContext();
+
+ context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
+ context.ResponseTime = utcNow + TimeSpan.FromSeconds(10);
+
+ Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]);
+
+ await middleware.FinalizeCacheHeadersAsync(context);
+
+ Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]);
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheHeadersAsync_StoresCachedResponse_InState()
+ {
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
+ var context = TestUtils.CreateTestContext();
+
+ Assert.Null(context.CachedResponse);
+
+ await middleware.FinalizeCacheHeadersAsync(context);
+
+ Assert.NotNull(context.CachedResponse);
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheHeadersAsync_SplitsVaryHeaderByCommas()
+ {
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
+ var context = TestUtils.CreateTestContext();
+
+ context.HttpContext.Response.Headers[HeaderNames.Vary] = "HeaderB, heaDera";
+
+ await middleware.FinalizeCacheHeadersAsync(context);
+
+ Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.VaryByRulesUpdated);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheBody_Cache_IfContentLengthMatches()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
+ var context = TestUtils.CreateTestContext();
+
+ context.ShouldCacheResponse = true;
+ middleware.ShimResponseStream(context);
+ context.HttpContext.Response.ContentLength = 20;
+
+ await context.HttpContext.Response.WriteAsync(new string('0', 20));
+
+ context.CachedResponse = new CachedResponse();
+ context.BaseKey = "BaseKey";
+ context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
+
+ await middleware.FinalizeCacheBodyAsync(context);
+
+ Assert.Equal(1, cache.SetCount);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ResponseCached);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
+ var context = TestUtils.CreateTestContext();
+
+ context.ShouldCacheResponse = true;
+ middleware.ShimResponseStream(context);
+ context.HttpContext.Response.ContentLength = 9;
+
+ await context.HttpContext.Response.WriteAsync(new string('0', 10));
+
+ context.CachedResponse = new CachedResponse();
+ context.BaseKey = "BaseKey";
+ context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
+
+ await middleware.FinalizeCacheBodyAsync(context);
+
+ Assert.Equal(0, cache.SetCount);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ResponseContentLengthMismatchNotCached);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
+ var context = TestUtils.CreateTestContext();
+
+ context.ShouldCacheResponse = true;
+ middleware.ShimResponseStream(context);
+
+ await context.HttpContext.Response.WriteAsync(new string('0', 10));
+
+ context.CachedResponse = new CachedResponse()
+ {
+ Headers = new HeaderDictionary()
+ };
+ context.BaseKey = "BaseKey";
+ context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
+
+ await middleware.FinalizeCacheBodyAsync(context);
+
+ Assert.Equal(1, cache.SetCount);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ResponseCached);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheBody_DoNotCache_IfShouldCacheResponseFalse()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
+ var context = TestUtils.CreateTestContext();
+
+ middleware.ShimResponseStream(context);
+ await context.HttpContext.Response.WriteAsync(new string('0', 10));
+ context.ShouldCacheResponse = false;
+
+ await middleware.FinalizeCacheBodyAsync(context);
+
+ Assert.Equal(0, cache.SetCount);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ResponseNotCached);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled()
+ {
+ var cache = new TestResponseCache();
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
+ var context = TestUtils.CreateTestContext();
+
+ context.ShouldCacheResponse = true;
+ middleware.ShimResponseStream(context);
+ await context.HttpContext.Response.WriteAsync(new string('0', 10));
+
+ context.ResponseCachingStream.DisableBuffering();
+
+ await middleware.FinalizeCacheBodyAsync(context);
+
+ Assert.Equal(0, cache.SetCount);
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ResponseNotCached);
+ }
+
+ [Fact]
+ public async Task FinalizeCacheBody_DoNotCache_IfSizeTooBig()
+ {
+ var sink = new TestSink();
+ var middleware = TestUtils.CreateTestMiddleware(
+ testSink: sink,
+ keyProvider: new TestResponseCachingKeyProvider("BaseKey"),
+ cache: new MemoryResponseCache(new MemoryCache(new MemoryCacheOptions
+ {
+ SizeLimit = 100
+ })));
+ var context = TestUtils.CreateTestContext();
+
+ context.ShouldCacheResponse = true;
+ middleware.ShimResponseStream(context);
+
+ await context.HttpContext.Response.WriteAsync(new string('0', 101));
+
+ context.CachedResponse = new CachedResponse() { Headers = new HeaderDictionary() };
+ context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
+
+ await middleware.FinalizeCacheBodyAsync(context);
+
+ // The response cached message will be logged but the adding of the entry will no-op
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ResponseCached);
+
+ // The entry cannot be retrieved
+ Assert.False(await middleware.TryServeFromCacheAsync(context));
+ }
+
+ [Fact]
+ public void AddResponseCachingFeature_SecondInvocation_Throws()
+ {
+ var httpContext = new DefaultHttpContext();
+
+ // Should not throw
+ ResponseCachingMiddleware.AddResponseCachingFeature(httpContext);
+
+ // Should throw
+ Assert.ThrowsAny<InvalidOperationException>(() => ResponseCachingMiddleware.AddResponseCachingFeature(httpContext));
+ }
+
+ private class FakeResponseFeature : HttpResponseFeature
+ {
+ public override void OnStarting(Func<object, Task> callback, object state) { }
+ }
+
+ [Theory]
+ // If allowResponseCaching is false, other settings will not matter but are included for completeness
+ [InlineData(false, false, false)]
+ [InlineData(false, false, true)]
+ [InlineData(false, true, false)]
+ [InlineData(false, true, true)]
+ [InlineData(true, false, false)]
+ [InlineData(true, false, true)]
+ [InlineData(true, true, false)]
+ [InlineData(true, true, true)]
+ public async Task Invoke_AddsResponseCachingFeature_Always(bool allowResponseCaching, bool allowCacheLookup, bool allowCacheStorage)
+ {
+ var responseCachingFeatureAdded = false;
+ var middleware = TestUtils.CreateTestMiddleware(next: httpContext =>
+ {
+ responseCachingFeatureAdded = httpContext.Features.Get<IResponseCachingFeature>() != null;
+ return Task.CompletedTask;
+ },
+ policyProvider: new TestResponseCachingPolicyProvider
+ {
+ AttemptResponseCachingValue = allowResponseCaching,
+ AllowCacheLookupValue = allowCacheLookup,
+ AllowCacheStorageValue = allowCacheStorage
+ });
+
+ var context = new DefaultHttpContext();
+ context.Features.Set<IHttpResponseFeature>(new FakeResponseFeature());
+ await middleware.Invoke(context);
+
+ Assert.True(responseCachingFeatureAdded);
+ }
+
+ [Fact]
+ public void GetOrderCasingNormalizedStringValues_NormalizesCasingToUpper()
+ {
+ var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
+ var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" });
+
+ var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(lowercaseStrings);
+
+ Assert.Equal(uppercaseStrings, normalizedStrings);
+ }
+
+ [Fact]
+ public void GetOrderCasingNormalizedStringValues_NormalizesOrder()
+ {
+ var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
+ var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" });
+
+ var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(reverseOrderStrings);
+
+ Assert.Equal(orderedStrings, normalizedStrings);
+ }
+
+ [Fact]
+ public void GetOrderCasingNormalizedStringValues_PreservesCommas()
+ {
+ var originalStrings = new StringValues(new[] { "STRINGA, STRINGB" });
+
+ var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(originalStrings);
+
+ Assert.Equal(originalStrings, normalizedStrings);
+ }
+ }
+}
diff --git a/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs
new file mode 100644
index 0000000000..4f1307b4bc
--- /dev/null
+++ b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs
@@ -0,0 +1,794 @@
+// 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;
+using Microsoft.AspNetCore.ResponseCaching.Internal;
+using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Tests
+{
+ public class ResponseCachingPolicyProviderTests
+ {
+ public static TheoryData<string> CacheableMethods
+ {
+ get
+ {
+ return new TheoryData<string>
+ {
+ HttpMethods.Get,
+ HttpMethods.Head
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(CacheableMethods))]
+ public void AttemptResponseCaching_CacheableMethods_Allowed(string method)
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Method = method;
+
+ Assert.True(new ResponseCachingPolicyProvider().AttemptResponseCaching(context));
+ Assert.Empty(sink.Writes);
+ }
+ public static TheoryData<string> NonCacheableMethods
+ {
+ get
+ {
+ return new TheoryData<string>
+ {
+ HttpMethods.Post,
+ HttpMethods.Put,
+ HttpMethods.Delete,
+ HttpMethods.Trace,
+ HttpMethods.Connect,
+ HttpMethods.Options,
+ "",
+ null
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(NonCacheableMethods))]
+ public void AttemptResponseCaching_UncacheableMethods_NotAllowed(string method)
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Method = method;
+
+ Assert.False(new ResponseCachingPolicyProvider().AttemptResponseCaching(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.RequestMethodNotCacheable);
+ }
+
+ [Fact]
+ public void AttemptResponseCaching_AuthorizationHeaders_NotAllowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Method = HttpMethods.Get;
+ context.HttpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW";
+
+ Assert.False(new ResponseCachingPolicyProvider().AttemptResponseCaching(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.RequestWithAuthorizationNotCacheable);
+ }
+
+ [Fact]
+ public void AllowCacheStorage_NoStore_Allowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Method = HttpMethods.Get;
+ context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ NoStore = true
+ }.ToString();
+
+ Assert.True(new ResponseCachingPolicyProvider().AllowCacheLookup(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void AllowCacheLookup_NoCache_NotAllowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Method = HttpMethods.Get;
+ context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ NoCache = true
+ }.ToString();
+
+ Assert.False(new ResponseCachingPolicyProvider().AllowCacheLookup(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.RequestWithNoCacheNotCacheable);
+ }
+
+ [Fact]
+ public void AllowCacheLookup_LegacyDirectives_NotAllowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Method = HttpMethods.Get;
+ context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
+
+ Assert.False(new ResponseCachingPolicyProvider().AllowCacheLookup(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.RequestWithPragmaNoCacheNotCacheable);
+ }
+
+ [Fact]
+ public void AllowCacheLookup_LegacyDirectives_OverridenByCacheControl()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Method = HttpMethods.Get;
+ context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
+ context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10";
+
+ Assert.True(new ResponseCachingPolicyProvider().AllowCacheLookup(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void AllowCacheStorage_NoStore_NotAllowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Method = HttpMethods.Get;
+ context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ NoStore = true
+ }.ToString();
+
+ Assert.False(new ResponseCachingPolicyProvider().AllowCacheStorage(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void IsResponseCacheable_NoPublic_NotAllowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+
+ Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ResponseWithoutPublicNotCacheable);
+ }
+
+ [Fact]
+ public void IsResponseCacheable_Public_Allowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true
+ }.ToString();
+
+ Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void IsResponseCacheable_NoCache_NotAllowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true,
+ NoCache = true
+ }.ToString();
+
+ Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ResponseWithNoCacheNotCacheable);
+ }
+
+ [Fact]
+ public void IsResponseCacheable_ResponseNoStore_NotAllowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true,
+ NoStore = true
+ }.ToString();
+
+ Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ResponseWithNoStoreNotCacheable);
+ }
+
+ [Fact]
+ public void IsResponseCacheable_SetCookieHeader_NotAllowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true
+ }.ToString();
+ context.HttpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue";
+
+ Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ResponseWithSetCookieNotCacheable);
+ }
+
+ [Fact]
+ public void IsResponseCacheable_VaryHeaderByStar_NotAllowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true
+ }.ToString();
+ context.HttpContext.Response.Headers[HeaderNames.Vary] = "*";
+
+ Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ResponseWithVaryStarNotCacheable);
+ }
+
+ [Fact]
+ public void IsResponseCacheable_Private_NotAllowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true,
+ Private = true
+ }.ToString();
+
+ Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ResponseWithPrivateNotCacheable);
+ }
+
+ [Theory]
+ [InlineData(StatusCodes.Status200OK)]
+ public void IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode)
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.StatusCode = statusCode;
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true
+ }.ToString();
+
+ Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Theory]
+ [InlineData(StatusCodes.Status100Continue)]
+ [InlineData(StatusCodes.Status101SwitchingProtocols)]
+ [InlineData(StatusCodes.Status102Processing)]
+ [InlineData(StatusCodes.Status201Created)]
+ [InlineData(StatusCodes.Status202Accepted)]
+ [InlineData(StatusCodes.Status203NonAuthoritative)]
+ [InlineData(StatusCodes.Status204NoContent)]
+ [InlineData(StatusCodes.Status205ResetContent)]
+ [InlineData(StatusCodes.Status206PartialContent)]
+ [InlineData(StatusCodes.Status207MultiStatus)]
+ [InlineData(StatusCodes.Status208AlreadyReported)]
+ [InlineData(StatusCodes.Status226IMUsed)]
+ [InlineData(StatusCodes.Status300MultipleChoices)]
+ [InlineData(StatusCodes.Status301MovedPermanently)]
+ [InlineData(StatusCodes.Status302Found)]
+ [InlineData(StatusCodes.Status303SeeOther)]
+ [InlineData(StatusCodes.Status304NotModified)]
+ [InlineData(StatusCodes.Status305UseProxy)]
+ [InlineData(StatusCodes.Status306SwitchProxy)]
+ [InlineData(StatusCodes.Status307TemporaryRedirect)]
+ [InlineData(StatusCodes.Status308PermanentRedirect)]
+ [InlineData(StatusCodes.Status400BadRequest)]
+ [InlineData(StatusCodes.Status401Unauthorized)]
+ [InlineData(StatusCodes.Status402PaymentRequired)]
+ [InlineData(StatusCodes.Status403Forbidden)]
+ [InlineData(StatusCodes.Status404NotFound)]
+ [InlineData(StatusCodes.Status405MethodNotAllowed)]
+ [InlineData(StatusCodes.Status406NotAcceptable)]
+ [InlineData(StatusCodes.Status407ProxyAuthenticationRequired)]
+ [InlineData(StatusCodes.Status408RequestTimeout)]
+ [InlineData(StatusCodes.Status409Conflict)]
+ [InlineData(StatusCodes.Status410Gone)]
+ [InlineData(StatusCodes.Status411LengthRequired)]
+ [InlineData(StatusCodes.Status412PreconditionFailed)]
+ [InlineData(StatusCodes.Status413RequestEntityTooLarge)]
+ [InlineData(StatusCodes.Status414RequestUriTooLong)]
+ [InlineData(StatusCodes.Status415UnsupportedMediaType)]
+ [InlineData(StatusCodes.Status416RequestedRangeNotSatisfiable)]
+ [InlineData(StatusCodes.Status417ExpectationFailed)]
+ [InlineData(StatusCodes.Status418ImATeapot)]
+ [InlineData(StatusCodes.Status419AuthenticationTimeout)]
+ [InlineData(StatusCodes.Status421MisdirectedRequest)]
+ [InlineData(StatusCodes.Status422UnprocessableEntity)]
+ [InlineData(StatusCodes.Status423Locked)]
+ [InlineData(StatusCodes.Status424FailedDependency)]
+ [InlineData(StatusCodes.Status426UpgradeRequired)]
+ [InlineData(StatusCodes.Status428PreconditionRequired)]
+ [InlineData(StatusCodes.Status429TooManyRequests)]
+ [InlineData(StatusCodes.Status431RequestHeaderFieldsTooLarge)]
+ [InlineData(StatusCodes.Status451UnavailableForLegalReasons)]
+ [InlineData(StatusCodes.Status500InternalServerError)]
+ [InlineData(StatusCodes.Status501NotImplemented)]
+ [InlineData(StatusCodes.Status502BadGateway)]
+ [InlineData(StatusCodes.Status503ServiceUnavailable)]
+ [InlineData(StatusCodes.Status504GatewayTimeout)]
+ [InlineData(StatusCodes.Status505HttpVersionNotsupported)]
+ [InlineData(StatusCodes.Status506VariantAlsoNegotiates)]
+ [InlineData(StatusCodes.Status507InsufficientStorage)]
+ [InlineData(StatusCodes.Status508LoopDetected)]
+ [InlineData(StatusCodes.Status510NotExtended)]
+ [InlineData(StatusCodes.Status511NetworkAuthenticationRequired)]
+ public void IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode)
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.StatusCode = statusCode;
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true
+ }.ToString();
+
+ Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ResponseWithUnsuccessfulStatusCodeNotCacheable);
+ }
+
+ [Fact]
+ public void IsResponseCacheable_NoExpiryRequirements_IsAllowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true
+ }.ToString();
+
+ var utcNow = DateTimeOffset.UtcNow;
+ context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
+ context.ResponseTime = DateTimeOffset.MaxValue;
+
+ Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void IsResponseCacheable_AtExpiry_NotAllowed()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true
+ }.ToString();
+ var utcNow = DateTimeOffset.UtcNow;
+ context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow);
+
+ context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
+ context.ResponseTime = utcNow;
+
+ Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ExpirationExpiresExceeded);
+ }
+
+ [Fact]
+ public void IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true,
+ MaxAge = TimeSpan.FromSeconds(10)
+ }.ToString();
+ context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow);
+ context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
+ context.ResponseTime = utcNow + TimeSpan.FromSeconds(9);
+
+ Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void IsResponseCacheable_MaxAgeOverridesExpiry_ToNotAllowed()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true,
+ MaxAge = TimeSpan.FromSeconds(10)
+ }.ToString();
+ context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow);
+ context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
+ context.ResponseTime = utcNow + TimeSpan.FromSeconds(10);
+
+ Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ExpirationMaxAgeExceeded);
+ }
+
+ [Fact]
+ public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true,
+ MaxAge = TimeSpan.FromSeconds(10),
+ SharedMaxAge = TimeSpan.FromSeconds(15)
+ }.ToString();
+ context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
+ context.ResponseTime = utcNow + TimeSpan.FromSeconds(11);
+
+ Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotAllowed()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
+ context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true,
+ MaxAge = TimeSpan.FromSeconds(10),
+ SharedMaxAge = TimeSpan.FromSeconds(5)
+ }.ToString();
+ context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
+ context.ResponseTime = utcNow + TimeSpan.FromSeconds(5);
+
+ Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ExpirationSharedMaxAgeExceeded);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.ResponseTime = DateTimeOffset.MaxValue;
+ context.CachedEntryAge = TimeSpan.MaxValue;
+ context.CachedResponseHeaders = new HeaderDictionary();
+
+ Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_NoExpiryRequirements_IsFresh()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.ResponseTime = DateTimeOffset.MaxValue;
+ context.CachedEntryAge = TimeSpan.MaxValue;
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true
+ }.ToString();
+
+ Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_AtExpiry_IsNotFresh()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.ResponseTime = utcNow;
+ context.CachedEntryAge = TimeSpan.Zero;
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true
+ }.ToString();
+ context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow);
+
+ Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ExpirationExpiresExceeded);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToFresh()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.CachedEntryAge = TimeSpan.FromSeconds(9);
+ context.ResponseTime = utcNow + context.CachedEntryAge;
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true,
+ MaxAge = TimeSpan.FromSeconds(10)
+ }.ToString();
+ context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow);
+
+ Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToNotFresh()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.CachedEntryAge = TimeSpan.FromSeconds(10);
+ context.ResponseTime = utcNow + context.CachedEntryAge;
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true,
+ MaxAge = TimeSpan.FromSeconds(10)
+ }.ToString();
+ context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow);
+
+ Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ExpirationMaxAgeExceeded);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.CachedEntryAge = TimeSpan.FromSeconds(11);
+ context.ResponseTime = utcNow + context.CachedEntryAge;
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true,
+ MaxAge = TimeSpan.FromSeconds(10),
+ SharedMaxAge = TimeSpan.FromSeconds(15)
+ }.ToString();
+ context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow);
+
+ Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh()
+ {
+ var utcNow = DateTimeOffset.UtcNow;
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.CachedEntryAge = TimeSpan.FromSeconds(5);
+ context.ResponseTime = utcNow + context.CachedEntryAge;
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ Public = true,
+ MaxAge = TimeSpan.FromSeconds(10),
+ SharedMaxAge = TimeSpan.FromSeconds(5)
+ }.ToString();
+ context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow);
+
+ Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ExpirationSharedMaxAgeExceeded);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_MinFreshReducesFreshness_ToNotFresh()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MinFresh = TimeSpan.FromSeconds(2)
+ }.ToString();
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(10),
+ SharedMaxAge = TimeSpan.FromSeconds(5)
+ }.ToString();
+ context.CachedEntryAge = TimeSpan.FromSeconds(3);
+
+ Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ExpirationMinFreshAdded,
+ LoggedMessage.ExpirationSharedMaxAgeExceeded);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_RequestMaxAgeRestrictAge_ToNotFresh()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(5)
+ }.ToString();
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(10),
+ }.ToString();
+ context.CachedEntryAge = TimeSpan.FromSeconds(5);
+
+ Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ExpirationMaxAgeExceeded);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ToFresh()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(5),
+ MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit
+ MaxStaleLimit = TimeSpan.FromSeconds(2)
+ }.ToString();
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(5),
+ }.ToString();
+ context.CachedEntryAge = TimeSpan.FromSeconds(6);
+
+ Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ExpirationMaxStaleSatisfied);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_MaxStaleInfiniteOverridesFreshness_ToFresh()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(5),
+ MaxStale = true // No value specified means a MaxStaleLimit of infinity
+ }.ToString();
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(5),
+ }.ToString();
+ context.CachedEntryAge = TimeSpan.FromSeconds(6);
+
+ Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ExpirationInfiniteMaxStaleSatisfied);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ButStillNotFresh()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(5),
+ MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit
+ MaxStaleLimit = TimeSpan.FromSeconds(1)
+ }.ToString();
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(5),
+ }.ToString();
+ context.CachedEntryAge = TimeSpan.FromSeconds(6);
+
+ Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ExpirationMaxAgeExceeded);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(5),
+ MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit
+ MaxStaleLimit = TimeSpan.FromSeconds(2)
+ }.ToString();
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(5),
+ MustRevalidate = true
+ }.ToString();
+ context.CachedEntryAge = TimeSpan.FromSeconds(6);
+
+ Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ExpirationMustRevalidate);
+ }
+
+ [Fact]
+ public void IsCachedEntryFresh_ProxyRevalidateOverridesRequestMaxStale_ToNotFresh()
+ {
+ var sink = new TestSink();
+ var context = TestUtils.CreateTestContext(sink);
+ context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(5),
+ MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit
+ MaxStaleLimit = TimeSpan.FromSeconds(2)
+ }.ToString();
+ context.CachedResponseHeaders = new HeaderDictionary();
+ context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(5),
+ MustRevalidate = true
+ }.ToString();
+ context.CachedEntryAge = TimeSpan.FromSeconds(6);
+
+ Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
+ TestUtils.AssertLoggedMessages(
+ sink.Writes,
+ LoggedMessage.ExpirationMustRevalidate);
+ }
+ }
+}
diff --git a/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs
new file mode 100644
index 0000000000..25fc1360e8
--- /dev/null
+++ b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs
@@ -0,0 +1,849 @@
+// 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.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Tests
+{
+ public class ResponseCachingTests
+ {
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("HEAD")]
+ public async void ServesCachedContent_IfAvailable(string method)
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
+ var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("HEAD")]
+ public async void ServesFreshContent_IfNotAvailable(string method)
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
+ var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "different"));
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_Post()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.PostAsync("", new StringContent(string.Empty));
+ var subsequentResponse = await client.PostAsync("", new StringContent(string.Empty));
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_Head_Get()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, ""));
+ var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, ""));
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_Get_Head()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, ""));
+ var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, ""));
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("HEAD")]
+ public async void ServesFreshContent_If_CacheControlNoCache(string method)
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+
+ var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
+
+ // verify the response is cached
+ var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
+ await AssertCachedResponseAsync(initialResponse, cachedResponse);
+
+ // assert cached response no longer served
+ client.DefaultRequestHeaders.CacheControl =
+ new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true };
+ var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("HEAD")]
+ public async void ServesFreshContent_If_PragmaNoCache(string method)
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+
+ var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
+
+ // verify the response is cached
+ var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
+ await AssertCachedResponseAsync(initialResponse, cachedResponse);
+
+ // assert cached response no longer served
+ client.DefaultRequestHeaders.Pragma.Clear();
+ client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("no-cache"));
+ var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("HEAD")]
+ public async void ServesCachedContent_If_PathCasingDiffers(string method)
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "path"));
+ var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "PATH"));
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("HEAD")]
+ public async void ServesFreshContent_If_ResponseExpired(string method)
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "?Expires=0"));
+ var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("HEAD")]
+ public async void ServesFreshContent_If_Authorization_HeaderExists(string method)
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("abc");
+ var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
+ var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesCachedContent_IfVaryHeader_Matches()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = HeaderNames.From);
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ client.DefaultRequestHeaders.From = "user@example.com";
+ var initialResponse = await client.GetAsync("");
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_IfVaryHeader_Mismatches()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = HeaderNames.From);
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ client.DefaultRequestHeaders.From = "user@example.com";
+ var initialResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.From = "user2@example.com";
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesCachedContent_IfVaryQueryKeys_Matches()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "query" });
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("?query=value");
+ var subsequentResponse = await client.GetAsync("?query=value");
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "QueryA", "queryb" });
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb");
+ var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb");
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "*" });
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb");
+ var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb");
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "QueryB", "QueryA" });
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB");
+ var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA");
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "*" });
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB");
+ var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA");
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_IfVaryQueryKey_Mismatches()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "query" });
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("?query=value");
+ var subsequentResponse = await client.GetAsync("?query=value2");
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCaseSensitive()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "QueryA", "QueryB" });
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb");
+ var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB");
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_IfVaryQueryKeyStar_Mismatch_QueryKeyValueCaseSensitive()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "*" });
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb");
+ var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB");
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_IfRequestRequirements_NotMet()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue()
+ {
+ MaxAge = TimeSpan.FromSeconds(0)
+ };
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void Serves504_IfOnlyIfCachedHeader_IsSpecified()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue()
+ {
+ OnlyIfCached = true
+ };
+ var subsequentResponse = await client.GetAsync("/different");
+
+ initialResponse.EnsureSuccessStatusCode();
+ Assert.Equal(System.Net.HttpStatusCode.GatewayTimeout, subsequentResponse.StatusCode);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_IfSetCookie_IsSpecified()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue");
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("");
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(app =>
+ {
+ app.Use(async (context, next) =>
+ {
+ context.Features.Set<IHttpSendFileFeature>(new DummySendFileFeature());
+ await next.Invoke();
+ });
+ });
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("");
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_IfIHttpSendFileFeature_Used()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(
+ app =>
+ {
+ app.Use(async (context, next) =>
+ {
+ context.Features.Set<IHttpSendFileFeature>(new DummySendFileFeature());
+ await next.Invoke();
+ });
+ },
+ contextAction: async context => await context.Features.Get<IHttpSendFileFeature>().SendFileAsync("dummy", 0, 0, CancellationToken.None));
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("");
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesCachedContent_IfSubsequentRequestContainsNoStore()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue()
+ {
+ NoStore = true
+ };
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_IfInitialRequestContainsNoStore()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue()
+ {
+ NoStore = true
+ };
+ var initialResponse = await client.GetAsync("");
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_IfInitialResponseContainsNoStore()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.CacheControl] = CacheControlHeaderValue.NoStoreString);
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("");
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void Serves304_IfIfModifiedSince_Satisfied()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MaxValue;
+ var subsequentResponse = await client.GetAsync("");
+
+ initialResponse.EnsureSuccessStatusCode();
+ Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching();
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void Serves304_IfIfNoneMatch_Satisfied()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""));
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\""));
+ var subsequentResponse = await client.GetAsync("");
+
+ initialResponse.EnsureSuccessStatusCode();
+ Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""));
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\""));
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesCachedContent_IfBodySize_IsCacheable()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions()
+ {
+ MaximumBodySize = 100
+ });
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("");
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_IfBodySize_IsNotCacheable()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions()
+ {
+ MaximumBodySize = 1
+ });
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("");
+ var subsequentResponse = await client.GetAsync("/different");
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_CaseSensitivePaths_IsNotCacheable()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions()
+ {
+ UseCaseSensitivePaths = true
+ });
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ var initialResponse = await client.GetAsync("/path");
+ var subsequentResponse = await client.GetAsync("/Path");
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = HeaderNames.From);
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ client.DefaultRequestHeaders.From = "user@example.com";
+ var initialResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.From = "user2@example.com";
+ var otherResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.From = "user@example.com";
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesFreshContent_IfCachedVaryByUpdated_OnCacheMiss()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]);
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ client.DefaultRequestHeaders.From = "user@example.com";
+ client.DefaultRequestHeaders.Pragma.Clear();
+ client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From"));
+ client.DefaultRequestHeaders.MaxForwards = 1;
+ var initialResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.From = "user2@example.com";
+ client.DefaultRequestHeaders.Pragma.Clear();
+ client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("Max-Forwards"));
+ client.DefaultRequestHeaders.MaxForwards = 2;
+ var otherResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.From = "user@example.com";
+ client.DefaultRequestHeaders.Pragma.Clear();
+ client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From"));
+ client.DefaultRequestHeaders.MaxForwards = 1;
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertFreshResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ [Fact]
+ public async void ServesCachedContent_IfCachedVaryByNotUpdated_OnCacheMiss()
+ {
+ var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]);
+
+ foreach (var builder in builders)
+ {
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ client.DefaultRequestHeaders.From = "user@example.com";
+ client.DefaultRequestHeaders.Pragma.Clear();
+ client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From"));
+ client.DefaultRequestHeaders.MaxForwards = 1;
+ var initialResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.From = "user2@example.com";
+ client.DefaultRequestHeaders.Pragma.Clear();
+ client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From"));
+ client.DefaultRequestHeaders.MaxForwards = 2;
+ var otherResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.From = "user@example.com";
+ client.DefaultRequestHeaders.Pragma.Clear();
+ client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From"));
+ client.DefaultRequestHeaders.MaxForwards = 1;
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertCachedResponseAsync(initialResponse, subsequentResponse);
+ }
+ }
+ }
+
+ private static async Task AssertCachedResponseAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse)
+ {
+ initialResponse.EnsureSuccessStatusCode();
+ subsequentResponse.EnsureSuccessStatusCode();
+
+ foreach (var header in initialResponse.Headers)
+ {
+ Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key));
+ }
+ Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age));
+ Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
+ }
+
+ private static async Task AssertFreshResponseAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse)
+ {
+ initialResponse.EnsureSuccessStatusCode();
+ subsequentResponse.EnsureSuccessStatusCode();
+
+ Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age));
+
+ if (initialResponse.RequestMessage.Method == HttpMethod.Head &&
+ subsequentResponse.RequestMessage.Method == HttpMethod.Head)
+ {
+ Assert.True(initialResponse.Headers.Contains("X-Value"));
+ Assert.NotEqual(initialResponse.Headers.GetValues("X-Value"), subsequentResponse.Headers.GetValues("X-Value"));
+ }
+ else
+ {
+ Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
+ }
+ }
+ }
+}
diff --git a/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs
new file mode 100644
index 0000000000..5247df3096
--- /dev/null
+++ b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs
@@ -0,0 +1,285 @@
+// 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.IO;
+using System.Linq;
+using Microsoft.AspNetCore.ResponseCaching.Internal;
+using Xunit;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Tests
+{
+ public class SegmentReadStreamTests
+ {
+ public class TestStreamInitInfo
+ {
+ internal List<byte[]> Segments { get; set; }
+ internal int SegmentSize { get; set; }
+ internal long Length { get; set; }
+ }
+
+ public static TheoryData<TestStreamInitInfo> TestStreams
+ {
+ get
+ {
+ return new TheoryData<TestStreamInitInfo>
+ {
+ // Partial Segment
+ new TestStreamInitInfo()
+ {
+ Segments = new List<byte[]>(new[]
+ {
+ new byte[] { 0, 1, 2, 3, 4 },
+ new byte[] { 5, 6, 7, 8, 9 },
+ new byte[] { 10, 11, 12 },
+ }),
+ SegmentSize = 5,
+ Length = 13
+ },
+ // Full Segments
+ new TestStreamInitInfo()
+ {
+ Segments = new List<byte[]>(new[]
+ {
+ new byte[] { 0, 1, 2, 3, 4 },
+ new byte[] { 5, 6, 7, 8, 9 },
+ new byte[] { 10, 11, 12, 13, 14 },
+ }),
+ SegmentSize = 5,
+ Length = 15
+ }
+ };
+ }
+ }
+
+ [Fact]
+ public void SegmentReadStream_NullSegments_Throws()
+ {
+ Assert.Throws<ArgumentNullException>(() => new SegmentReadStream(null, 0));
+ }
+
+ [Fact]
+ public void Position_ResetToZero_Succeeds()
+ {
+ var stream = new SegmentReadStream(new List<byte[]>(), 0);
+
+ // This should not throw
+ stream.Position = 0;
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(-1)]
+ [InlineData(100)]
+ [InlineData(long.MaxValue)]
+ [InlineData(long.MinValue)]
+ public void Position_SetToNonZero_Throws(long position)
+ {
+ var stream = new SegmentReadStream(new List<byte[]>(new[] { new byte[100] }), 100);
+
+ Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = position);
+ }
+
+ [Fact]
+ public void WriteOperations_Throws()
+ {
+ var stream = new SegmentReadStream(new List<byte[]>(), 0);
+
+
+ Assert.Throws<NotSupportedException>(() => stream.Flush());
+ Assert.Throws<NotSupportedException>(() => stream.Write(new byte[1], 0, 0));
+ }
+
+ [Fact]
+ public void SetLength_Throws()
+ {
+ var stream = new SegmentReadStream(new List<byte[]>(), 0);
+
+ Assert.Throws<NotSupportedException>(() => stream.SetLength(0));
+ }
+
+ [Theory]
+ [InlineData(SeekOrigin.Current)]
+ [InlineData(SeekOrigin.End)]
+ public void Seek_NotBegin_Throws(SeekOrigin origin)
+ {
+ var stream = new SegmentReadStream(new List<byte[]>(), 0);
+
+ Assert.Throws<ArgumentException>(() => stream.Seek(0, origin));
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(-1)]
+ [InlineData(100)]
+ [InlineData(long.MaxValue)]
+ [InlineData(long.MinValue)]
+ public void Seek_NotZero_Throws(long offset)
+ {
+ var stream = new SegmentReadStream(new List<byte[]>(), 0);
+
+ Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(offset, SeekOrigin.Begin));
+ }
+
+ [Theory]
+ [MemberData(nameof(TestStreams))]
+ public void ReadByte_CanReadAllBytes(TestStreamInitInfo info)
+ {
+ var stream = new SegmentReadStream(info.Segments, info.Length);
+
+ for (var i = 0; i < stream.Length; i++)
+ {
+ Assert.Equal(i, stream.Position);
+ Assert.Equal(i, stream.ReadByte());
+ }
+ Assert.Equal(stream.Length, stream.Position);
+ Assert.Equal(-1, stream.ReadByte());
+ Assert.Equal(stream.Length, stream.Position);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestStreams))]
+ public void Read_CountLessThanSegmentSize_CanReadAllBytes(TestStreamInitInfo info)
+ {
+ var stream = new SegmentReadStream(info.Segments, info.Length);
+ var count = info.SegmentSize - 1;
+
+ for (var i = 0; i < stream.Length; i+=count)
+ {
+ var output = new byte[count];
+ var expectedOutput = new byte[count];
+ var expectedBytesRead = Math.Min(count, stream.Length - i);
+ for (var j = 0; j < expectedBytesRead; j++)
+ {
+ expectedOutput[j] = (byte)(i + j);
+ }
+ Assert.Equal(i, stream.Position);
+ Assert.Equal(expectedBytesRead, stream.Read(output, 0, count));
+ Assert.True(expectedOutput.SequenceEqual(output));
+ }
+ Assert.Equal(stream.Length, stream.Position);
+ Assert.Equal(0, stream.Read(new byte[count], 0, count));
+ Assert.Equal(stream.Length, stream.Position);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestStreams))]
+ public void Read_CountEqualSegmentSize_CanReadAllBytes(TestStreamInitInfo info)
+ {
+ var stream = new SegmentReadStream(info.Segments, info.Length);
+ var count = info.SegmentSize;
+
+ for (var i = 0; i < stream.Length; i += count)
+ {
+ var output = new byte[count];
+ var expectedOutput = new byte[count];
+ var expectedBytesRead = Math.Min(count, stream.Length - i);
+ for (var j = 0; j < expectedBytesRead; j++)
+ {
+ expectedOutput[j] = (byte)(i + j);
+ }
+ Assert.Equal(i, stream.Position);
+ Assert.Equal(expectedBytesRead, stream.Read(output, 0, count));
+ Assert.True(expectedOutput.SequenceEqual(output));
+ }
+ Assert.Equal(stream.Length, stream.Position);
+ Assert.Equal(0, stream.Read(new byte[count], 0, count));
+ Assert.Equal(stream.Length, stream.Position);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestStreams))]
+ public void Read_CountGreaterThanSegmentSize_CanReadAllBytes(TestStreamInitInfo info)
+ {
+ var stream = new SegmentReadStream(info.Segments, info.Length);
+ var count = info.SegmentSize + 1;
+
+ for (var i = 0; i < stream.Length; i += count)
+ {
+ var output = new byte[count];
+ var expectedOutput = new byte[count];
+ var expectedBytesRead = Math.Min(count, stream.Length - i);
+ for (var j = 0; j < expectedBytesRead; j++)
+ {
+ expectedOutput[j] = (byte)(i + j);
+ }
+ Assert.Equal(i, stream.Position);
+ Assert.Equal(expectedBytesRead, stream.Read(output, 0, count));
+ Assert.True(expectedOutput.SequenceEqual(output));
+ }
+ Assert.Equal(stream.Length, stream.Position);
+ Assert.Equal(0, stream.Read(new byte[count], 0, count));
+ Assert.Equal(stream.Length, stream.Position);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestStreams))]
+ public void CopyToAsync_CopiesAllBytes(TestStreamInitInfo info)
+ {
+ var stream = new SegmentReadStream(info.Segments, info.Length);
+ var writeStream = new SegmentWriteStream(info.SegmentSize);
+
+ stream.CopyTo(writeStream);
+
+ Assert.Equal(stream.Length, stream.Position);
+ Assert.Equal(stream.Length, writeStream.Length);
+ var writeSegments = writeStream.GetSegments();
+ for (var i = 0; i < info.Segments.Count; i++)
+ {
+ Assert.True(writeSegments[i].SequenceEqual(info.Segments[i]));
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TestStreams))]
+ public void CopyToAsync_CopiesFromCurrentPosition(TestStreamInitInfo info)
+ {
+ var skippedBytes = info.SegmentSize;
+ var writeStream = new SegmentWriteStream((int)info.Length);
+ var stream = new SegmentReadStream(info.Segments, info.Length);
+ stream.Read(new byte[skippedBytes], 0, skippedBytes);
+
+ stream.CopyTo(writeStream);
+
+ Assert.Equal(stream.Length, stream.Position);
+ Assert.Equal(stream.Length - skippedBytes, writeStream.Length);
+ var writeSegments = writeStream.GetSegments();
+
+ for (var i = skippedBytes; i < info.Length; i++)
+ {
+ Assert.Equal(info.Segments[i / info.SegmentSize][i % info.SegmentSize], writeSegments[0][i - skippedBytes]);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TestStreams))]
+ public void CopyToAsync_CopiesFromStart_AfterReset(TestStreamInitInfo info)
+ {
+ var skippedBytes = info.SegmentSize;
+ var writeStream = new SegmentWriteStream(info.SegmentSize);
+ var stream = new SegmentReadStream(info.Segments, info.Length);
+ stream.Read(new byte[skippedBytes], 0, skippedBytes);
+
+ stream.CopyTo(writeStream);
+
+ // Assert bytes read from current location to the end
+ Assert.Equal(stream.Length, stream.Position);
+ Assert.Equal(stream.Length - skippedBytes, writeStream.Length);
+
+ // Reset
+ stream.Position = 0;
+ writeStream = new SegmentWriteStream(info.SegmentSize);
+
+ stream.CopyTo(writeStream);
+
+ Assert.Equal(stream.Length, stream.Position);
+ Assert.Equal(stream.Length, writeStream.Length);
+ var writeSegments = writeStream.GetSegments();
+ for (var i = 0; i < info.Segments.Count; i++)
+ {
+ Assert.True(writeSegments[i].SequenceEqual(info.Segments[i]));
+ }
+ }
+ }
+}
diff --git a/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs
new file mode 100644
index 0000000000..6043128e7b
--- /dev/null
+++ b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs
@@ -0,0 +1,113 @@
+// 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 System.Linq;
+using Microsoft.AspNetCore.ResponseCaching.Internal;
+using Xunit;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Tests
+{
+ public class SegmentWriteStreamTests
+ {
+ private static byte[] WriteData = new byte[]
+ {
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14
+ };
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(-1)]
+ public void SegmentWriteStream_InvalidSegmentSize_Throws(int segmentSize)
+ {
+ Assert.Throws<ArgumentOutOfRangeException>(() => new SegmentWriteStream(segmentSize));
+ }
+
+ [Fact]
+ public void ReadAndSeekOperations_Throws()
+ {
+ var stream = new SegmentWriteStream(1);
+
+ Assert.Throws<NotSupportedException>(() => stream.Read(new byte[1], 0, 0));
+ Assert.Throws<NotSupportedException>(() => stream.Position = 0);
+ Assert.Throws<NotSupportedException>(() => stream.Seek(0, SeekOrigin.Begin));
+ }
+
+ [Fact]
+ public void GetSegments_ExtractionDisablesWriting()
+ {
+ var stream = new SegmentWriteStream(1);
+
+ Assert.True(stream.CanWrite);
+ Assert.Empty(stream.GetSegments());
+ Assert.False(stream.CanWrite);
+ }
+
+ [Theory]
+ [InlineData(4)]
+ [InlineData(5)]
+ [InlineData(6)]
+ public void WriteByte_CanWriteAllBytes(int segmentSize)
+ {
+ var stream = new SegmentWriteStream(segmentSize);
+
+ foreach (var datum in WriteData)
+ {
+ stream.WriteByte(datum);
+ }
+ var segments = stream.GetSegments();
+
+ Assert.Equal(WriteData.Length, stream.Length);
+ Assert.Equal((WriteData.Length + segmentSize - 1)/ segmentSize, segments.Count);
+
+ for (var i = 0; i < WriteData.Length; i += segmentSize)
+ {
+ var expectedSegmentSize = Math.Min(segmentSize, WriteData.Length - i);
+ var expectedSegment = new byte[expectedSegmentSize];
+ for (int j = 0; j < expectedSegmentSize; j++)
+ {
+ expectedSegment[j] = (byte)(i + j);
+ }
+ var segment = segments[i / segmentSize];
+
+ Assert.Equal(expectedSegmentSize, segment.Length);
+ Assert.True(expectedSegment.SequenceEqual(segment));
+ }
+ }
+
+ [Theory]
+ [InlineData(4)]
+ [InlineData(5)]
+ [InlineData(6)]
+ public void Write_CanWriteAllBytes(int writeSize)
+ {
+ var segmentSize = 5;
+ var stream = new SegmentWriteStream(segmentSize);
+
+
+ for (var i = 0; i < WriteData.Length; i += writeSize)
+ {
+ stream.Write(WriteData, i, Math.Min(writeSize, WriteData.Length - i));
+ }
+ var segments = stream.GetSegments();
+
+ Assert.Equal(WriteData.Length, stream.Length);
+ Assert.Equal((WriteData.Length + segmentSize - 1) / segmentSize, segments.Count);
+
+ for (var i = 0; i < WriteData.Length; i += segmentSize)
+ {
+ var expectedSegmentSize = Math.Min(segmentSize, WriteData.Length - i);
+ var expectedSegment = new byte[expectedSegmentSize];
+ for (int j = 0; j < expectedSegmentSize; j++)
+ {
+ expectedSegment[j] = (byte)(i + j);
+ }
+ var segment = segments[i / segmentSize];
+
+ Assert.Equal(expectedSegmentSize, segment.Length);
+ Assert.True(expectedSegment.SequenceEqual(segment));
+ }
+ }
+ }
+}
diff --git a/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs
new file mode 100644
index 0000000000..09f21a8878
--- /dev/null
+++ b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs
@@ -0,0 +1,396 @@
+// 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.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.ResponseCaching.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+using ISystemClock = Microsoft.AspNetCore.ResponseCaching.Internal.ISystemClock;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Tests
+{
+ internal class TestUtils
+ {
+ static TestUtils()
+ {
+ // Force sharding in tests
+ StreamUtilities.BodySegmentSize = 10;
+ }
+
+ private static bool TestRequestDelegate(HttpContext context, string guid)
+ {
+ var headers = context.Response.GetTypedHeaders();
+
+ var expires = context.Request.Query["Expires"];
+ if (!string.IsNullOrEmpty(expires))
+ {
+ headers.Expires = DateTimeOffset.Now.AddSeconds(int.Parse(expires));
+ }
+
+ if (headers.CacheControl == null)
+ {
+ headers.CacheControl = new CacheControlHeaderValue
+ {
+ Public = true,
+ MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null
+ };
+ }
+ else
+ {
+ headers.CacheControl.Public = true;
+ headers.CacheControl.MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null;
+ }
+ headers.Date = DateTimeOffset.UtcNow;
+ headers.Headers["X-Value"] = guid;
+
+ if (context.Request.Method != "HEAD")
+ {
+ return true;
+ }
+ return false;
+ }
+
+ internal static async Task TestRequestDelegateWriteAsync(HttpContext context)
+ {
+ var uniqueId = Guid.NewGuid().ToString();
+ if (TestRequestDelegate(context, uniqueId))
+ {
+ await context.Response.WriteAsync(uniqueId);
+ }
+ }
+
+ internal static Task TestRequestDelegateWrite(HttpContext context)
+ {
+ var uniqueId = Guid.NewGuid().ToString();
+ if (TestRequestDelegate(context, uniqueId))
+ {
+ context.Response.Write(uniqueId);
+ }
+ return Task.CompletedTask;
+ }
+
+ internal static IResponseCachingKeyProvider CreateTestKeyProvider()
+ {
+ return CreateTestKeyProvider(new ResponseCachingOptions());
+ }
+
+ internal static IResponseCachingKeyProvider CreateTestKeyProvider(ResponseCachingOptions options)
+ {
+ return new ResponseCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
+ }
+
+ internal static IEnumerable<IWebHostBuilder> CreateBuildersWithResponseCaching(
+ Action<IApplicationBuilder> configureDelegate = null,
+ ResponseCachingOptions options = null,
+ Action<HttpContext> contextAction = null)
+ {
+ return CreateBuildersWithResponseCaching(configureDelegate, options, new RequestDelegate[]
+ {
+ context =>
+ {
+ contextAction?.Invoke(context);
+ return TestRequestDelegateWrite(context);
+ },
+ context =>
+ {
+ contextAction?.Invoke(context);
+ return TestRequestDelegateWriteAsync(context);
+ },
+ });
+ }
+
+ private static IEnumerable<IWebHostBuilder> CreateBuildersWithResponseCaching(
+ Action<IApplicationBuilder> configureDelegate = null,
+ ResponseCachingOptions options = null,
+ IEnumerable<RequestDelegate> requestDelegates = null)
+ {
+ if (configureDelegate == null)
+ {
+ configureDelegate = app => { };
+ }
+ if (requestDelegates == null)
+ {
+ requestDelegates = new RequestDelegate[]
+ {
+ TestRequestDelegateWriteAsync,
+ TestRequestDelegateWrite
+ };
+ }
+
+ foreach (var requestDelegate in requestDelegates)
+ {
+ // Test with in memory ResponseCache
+ yield return new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddResponseCaching(responseCachingOptions =>
+ {
+ if (options != null)
+ {
+ responseCachingOptions.MaximumBodySize = options.MaximumBodySize;
+ responseCachingOptions.UseCaseSensitivePaths = options.UseCaseSensitivePaths;
+ responseCachingOptions.SystemClock = options.SystemClock;
+ }
+ });
+ })
+ .Configure(app =>
+ {
+ configureDelegate(app);
+ app.UseResponseCaching();
+ app.Run(requestDelegate);
+ });
+ }
+ }
+
+ internal static ResponseCachingMiddleware CreateTestMiddleware(
+ RequestDelegate next = null,
+ IResponseCache cache = null,
+ ResponseCachingOptions options = null,
+ TestSink testSink = null,
+ IResponseCachingKeyProvider keyProvider = null,
+ IResponseCachingPolicyProvider policyProvider = null)
+ {
+ if (next == null)
+ {
+ next = httpContext => Task.CompletedTask;
+ }
+ if (cache == null)
+ {
+ cache = new TestResponseCache();
+ }
+ if (options == null)
+ {
+ options = new ResponseCachingOptions();
+ }
+ if (keyProvider == null)
+ {
+ keyProvider = new ResponseCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
+ }
+ if (policyProvider == null)
+ {
+ policyProvider = new TestResponseCachingPolicyProvider();
+ }
+
+ return new ResponseCachingMiddleware(
+ next,
+ Options.Create(options),
+ testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true),
+ policyProvider,
+ cache,
+ keyProvider);
+ }
+
+ internal static ResponseCachingContext CreateTestContext()
+ {
+ return new ResponseCachingContext(new DefaultHttpContext(), NullLogger.Instance)
+ {
+ ResponseTime = DateTimeOffset.UtcNow
+ };
+ }
+
+ internal static ResponseCachingContext CreateTestContext(ITestSink testSink)
+ {
+ return new ResponseCachingContext(new DefaultHttpContext(), new TestLogger("ResponseCachingTests", testSink, true))
+ {
+ ResponseTime = DateTimeOffset.UtcNow
+ };
+ }
+
+ internal static void AssertLoggedMessages(IEnumerable<WriteContext> messages, params LoggedMessage[] expectedMessages)
+ {
+ var messageList = messages.ToList();
+ Assert.Equal(messageList.Count, expectedMessages.Length);
+
+ for (var i = 0; i < messageList.Count; i++)
+ {
+ Assert.Equal(expectedMessages[i].EventId, messageList[i].EventId);
+ Assert.Equal(expectedMessages[i].LogLevel, messageList[i].LogLevel);
+ }
+ }
+
+ public static HttpRequestMessage CreateRequest(string method, string requestUri)
+ {
+ return new HttpRequestMessage(new HttpMethod(method), requestUri);
+ }
+ }
+
+ internal static class HttpResponseWritingExtensions
+ {
+ internal static void Write(this HttpResponse response, string text)
+ {
+ if (response == null)
+ {
+ throw new ArgumentNullException(nameof(response));
+ }
+
+ if (text == null)
+ {
+ throw new ArgumentNullException(nameof(text));
+ }
+
+ byte[] data = Encoding.UTF8.GetBytes(text);
+ response.Body.Write(data, 0, data.Length);
+ }
+ }
+
+ internal class LoggedMessage
+ {
+ internal static LoggedMessage RequestMethodNotCacheable => new LoggedMessage(1, LogLevel.Debug);
+ internal static LoggedMessage RequestWithAuthorizationNotCacheable => new LoggedMessage(2, LogLevel.Debug);
+ internal static LoggedMessage RequestWithNoCacheNotCacheable => new LoggedMessage(3, LogLevel.Debug);
+ internal static LoggedMessage RequestWithPragmaNoCacheNotCacheable => new LoggedMessage(4, LogLevel.Debug);
+ internal static LoggedMessage ExpirationMinFreshAdded => new LoggedMessage(5, LogLevel.Debug);
+ internal static LoggedMessage ExpirationSharedMaxAgeExceeded => new LoggedMessage(6, LogLevel.Debug);
+ internal static LoggedMessage ExpirationMustRevalidate => new LoggedMessage(7, LogLevel.Debug);
+ internal static LoggedMessage ExpirationMaxStaleSatisfied => new LoggedMessage(8, LogLevel.Debug);
+ internal static LoggedMessage ExpirationMaxAgeExceeded => new LoggedMessage(9, LogLevel.Debug);
+ internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(10, LogLevel.Debug);
+ internal static LoggedMessage ResponseWithoutPublicNotCacheable => new LoggedMessage(11, LogLevel.Debug);
+ internal static LoggedMessage ResponseWithNoStoreNotCacheable => new LoggedMessage(12, LogLevel.Debug);
+ internal static LoggedMessage ResponseWithNoCacheNotCacheable => new LoggedMessage(13, LogLevel.Debug);
+ internal static LoggedMessage ResponseWithSetCookieNotCacheable => new LoggedMessage(14, LogLevel.Debug);
+ internal static LoggedMessage ResponseWithVaryStarNotCacheable => new LoggedMessage(15, LogLevel.Debug);
+ internal static LoggedMessage ResponseWithPrivateNotCacheable => new LoggedMessage(16, LogLevel.Debug);
+ internal static LoggedMessage ResponseWithUnsuccessfulStatusCodeNotCacheable => new LoggedMessage(17, LogLevel.Debug);
+ internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(18, LogLevel.Debug);
+ internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(19, LogLevel.Debug);
+ internal static LoggedMessage NotModifiedIfModifiedSinceSatisfied => new LoggedMessage(20, LogLevel.Debug);
+ internal static LoggedMessage NotModifiedServed => new LoggedMessage(21, LogLevel.Information);
+ internal static LoggedMessage CachedResponseServed => new LoggedMessage(22, LogLevel.Information);
+ internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(23, LogLevel.Information);
+ internal static LoggedMessage NoResponseServed => new LoggedMessage(24, LogLevel.Information);
+ internal static LoggedMessage VaryByRulesUpdated => new LoggedMessage(25, LogLevel.Debug);
+ internal static LoggedMessage ResponseCached => new LoggedMessage(26, LogLevel.Information);
+ internal static LoggedMessage ResponseNotCached => new LoggedMessage(27, LogLevel.Information);
+ internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(28, LogLevel.Warning);
+ internal static LoggedMessage ExpirationInfiniteMaxStaleSatisfied => new LoggedMessage(29, LogLevel.Debug);
+
+ private LoggedMessage(int evenId, LogLevel logLevel)
+ {
+ EventId = evenId;
+ LogLevel = logLevel;
+ }
+
+ internal int EventId { get; }
+ internal LogLevel LogLevel { get; }
+ }
+
+ internal class DummySendFileFeature : IHttpSendFileFeature
+ {
+ public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
+ {
+ return Task.CompletedTask;
+ }
+ }
+
+ internal class TestResponseCachingPolicyProvider : IResponseCachingPolicyProvider
+ {
+ public bool AllowCacheLookupValue { get; set; } = false;
+ public bool AllowCacheStorageValue { get; set; } = false;
+ public bool AttemptResponseCachingValue { get; set; } = false;
+ public bool IsCachedEntryFreshValue { get; set; } = true;
+ public bool IsResponseCacheableValue { get; set; } = true;
+
+ public bool AllowCacheLookup(ResponseCachingContext context) => AllowCacheLookupValue;
+
+ public bool AllowCacheStorage(ResponseCachingContext context) => AllowCacheStorageValue;
+
+ public bool AttemptResponseCaching(ResponseCachingContext context) => AttemptResponseCachingValue;
+
+ public bool IsCachedEntryFresh(ResponseCachingContext context) => IsCachedEntryFreshValue;
+
+ public bool IsResponseCacheable(ResponseCachingContext context) => IsResponseCacheableValue;
+ }
+
+ internal class TestResponseCachingKeyProvider : IResponseCachingKeyProvider
+ {
+ private readonly string _baseKey;
+ private readonly StringValues _varyKey;
+
+ public TestResponseCachingKeyProvider(string lookupBaseKey = null, StringValues? lookupVaryKey = null)
+ {
+ _baseKey = lookupBaseKey;
+ if (lookupVaryKey.HasValue)
+ {
+ _varyKey = lookupVaryKey.Value;
+ }
+ }
+
+ public IEnumerable<string> CreateLookupVaryByKeys(ResponseCachingContext context)
+ {
+ foreach (var varyKey in _varyKey)
+ {
+ yield return _baseKey + varyKey;
+ }
+ }
+
+ public string CreateBaseKey(ResponseCachingContext context)
+ {
+ return _baseKey;
+ }
+
+ public string CreateStorageVaryByKey(ResponseCachingContext context)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ internal class TestResponseCache : IResponseCache
+ {
+ private readonly IDictionary<string, IResponseCacheEntry> _storage = new Dictionary<string, IResponseCacheEntry>();
+ public int GetCount { get; private set; }
+ public int SetCount { get; private set; }
+
+ public IResponseCacheEntry Get(string key)
+ {
+ GetCount++;
+ try
+ {
+ return _storage[key];
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public Task<IResponseCacheEntry> GetAsync(string key)
+ {
+ return Task.FromResult(Get(key));
+ }
+
+ public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor)
+ {
+ SetCount++;
+ _storage[key] = entry;
+ }
+
+ public Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor)
+ {
+ Set(key, entry, validFor);
+ return Task.CompletedTask;
+ }
+ }
+
+ internal class TestClock : ISystemClock
+ {
+ public DateTimeOffset UtcNow { get; set; }
+ }
+}
diff --git a/src/ResponseCaching/version.props b/src/ResponseCaching/version.props
new file mode 100644
index 0000000000..669c874829
--- /dev/null
+++ b/src/ResponseCaching/version.props
@@ -0,0 +1,12 @@
+<Project>
+ <PropertyGroup>
+ <VersionPrefix>2.1.1</VersionPrefix>
+ <VersionSuffix>rtm</VersionSuffix>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' == 'rtm' ">$(VersionPrefix)</PackageVersion>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' != 'rtm' ">$(VersionPrefix)-$(VersionSuffix)-final</PackageVersion>
+ <BuildNumber Condition="'$(BuildNumber)' == ''">t000</BuildNumber>
+ <FeatureBranchVersionPrefix Condition="'$(FeatureBranchVersionPrefix)' == ''">a-</FeatureBranchVersionPrefix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
+ </PropertyGroup>
+</Project>
diff --git a/src/Routing/.gitignore b/src/Routing/.gitignore
new file mode 100644
index 0000000000..5f8dfa9f48
--- /dev/null
+++ b/src/Routing/.gitignore
@@ -0,0 +1,41 @@
+[Oo]bj/
+[Bb]in/
+TestResults/
+.nuget/
+*.sln.ide/
+_ReSharper.*/
+packages/
+artifacts/
+PublishProfiles/
+.vs/
+.build/
+.testPublish/
+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
+global.json
+BenchmarkDotNet.Artifacts/
diff --git a/src/Routing/Directory.Build.props b/src/Routing/Directory.Build.props
new file mode 100644
index 0000000000..f1986d9953
--- /dev/null
+++ b/src/Routing/Directory.Build.props
@@ -0,0 +1,20 @@
+<Project>
+ <Import
+ Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))\AspNetCoreSettings.props"
+ Condition=" '$(CI)' != 'true' AND '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))' != '' " />
+
+ <Import Project="version.props" />
+ <Import Project="build\dependencies.props" />
+ <Import Project="build\sources.props" />
+
+ <PropertyGroup>
+ <Product>Microsoft ASP.NET Core</Product>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
+ <RepositoryType>git</RepositoryType>
+ <RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
+ <AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)build\Key.snk</AssemblyOriginatorKeyFile>
+ <SignAssembly>true</SignAssembly>
+ <PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+</Project>
diff --git a/src/Routing/Directory.Build.targets b/src/Routing/Directory.Build.targets
new file mode 100644
index 0000000000..53b3f6e1da
--- /dev/null
+++ b/src/Routing/Directory.Build.targets
@@ -0,0 +1,7 @@
+<Project>
+ <PropertyGroup>
+ <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">$(MicrosoftNETCoreApp20PackageVersion)</RuntimeFrameworkVersion>
+ <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">$(MicrosoftNETCoreApp21PackageVersion)</RuntimeFrameworkVersion>
+ <NETStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard2.0' ">$(NETStandardLibrary20PackageVersion)</NETStandardImplicitPackageVersion>
+ </PropertyGroup>
+</Project>
diff --git a/src/Routing/NuGetPackageVerifier.json b/src/Routing/NuGetPackageVerifier.json
new file mode 100644
index 0000000000..9ed455034c
--- /dev/null
+++ b/src/Routing/NuGetPackageVerifier.json
@@ -0,0 +1,13 @@
+{
+ "adx-nonshipping": {
+ "rules": [],
+ "packages": {
+ "Microsoft.AspNetCore.Routing.DecisionTree.Sources": {}
+ }
+ },
+ "Default": {
+ "rules": [
+ "DefaultCompositeRule"
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/Routing/README.md b/src/Routing/README.md
new file mode 100644
index 0000000000..3c0794570b
--- /dev/null
+++ b/src/Routing/README.md
@@ -0,0 +1,10 @@
+ASP.NET Routing
+===
+
+AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/fe4o5h1s9ve86nyv/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/Routing/branch/dev)
+
+Travis: [![Travis](https://travis-ci.org/aspnet/Routing.svg?branch=dev)](https://travis-ci.org/aspnet/Routing)
+
+Contains routing middleware for routing requests to application logic.
+
+This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
diff --git a/src/Routing/Routing.sln b/src/Routing/Routing.sln
new file mode 100644
index 0000000000..13657187fa
--- /dev/null
+++ b/src/Routing/Routing.sln
@@ -0,0 +1,165 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27106.3000
+MinimumVisualStudioVersion = 15.0.26730.03
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0E966C37-7334-4D96-AAF6-9F49FBD166E3}"
+ ProjectSection(SolutionItems) = preProject
+ src\Directory.Build.props = src\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{C3ADD55B-B9C7-4061-8AD4-6A70D1AE3B2E}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{95359B4B-4C85-4B44-A75B-0621905C4CF6}"
+ ProjectSection(SolutionItems) = preProject
+ test\Directory.Build.props = test\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routing", "src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj", "{1EE54D32-6CED-4206-ACF5-3DC1DD39D228}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routing.Tests", "test\Microsoft.AspNetCore.Routing.Tests\Microsoft.AspNetCore.Routing.Tests.csproj", "{636D79ED-7B32-487C-BDA5-D2A1AAA97371}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RoutingSample.Web", "samples\RoutingSample.Web\RoutingSample.Web.csproj", "{DB94E647-C73A-4F52-A126-AA7544CCF33B}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C430C499-382D-47BD-B351-CF8F89C08CD2}"
+ ProjectSection(SolutionItems) = preProject
+ global.json = global.json
+ NuGet.config = NuGet.config
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests", "test\Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests\Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj", "{09C2933C-23AC-41B7-994D-E8A5184A629C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routing.Abstractions", "src\Microsoft.AspNetCore.Routing.Abstractions\Microsoft.AspNetCore.Routing.Abstractions.csproj", "{ED253B01-24F1-43D1-AA0B-079391E105A9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests", "test\Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests\Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj", "{741B0B05-CE96-473B-B962-6B0A347DF79A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routing.FunctionalTests", "test\Microsoft.AspNetCore.Routing.FunctionalTests\Microsoft.AspNetCore.Routing.FunctionalTests.csproj", "{5C73140B-41F3-466F-A07B-3614E4D80DF9}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{6DC6B416-C8C4-4BFA-8C1E-A55A6D7EFD08}"
+ ProjectSection(SolutionItems) = preProject
+ build\dependencies.props = build\dependencies.props
+ build\Key.snk = build\Key.snk
+ build\repo.props = build\repo.props
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Routing.Performance", "benchmarks\Microsoft.AspNetCore.Routing.Performance\Microsoft.AspNetCore.Routing.Performance.csproj", "{F3D86714-4E64-41A6-9B36-A47B3683CF5D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{D5F39F59-5725-4127-82E7-67028D006185}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|Mixed Platforms = Debug|Mixed Platforms
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|Mixed Platforms = Release|Mixed Platforms
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {1EE54D32-6CED-4206-ACF5-3DC1DD39D228}.Release|x86.ActiveCfg = Release|Any CPU
+ {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Release|Any CPU.Build.0 = Release|Any CPU
+ {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {636D79ED-7B32-487C-BDA5-D2A1AAA97371}.Release|x86.ActiveCfg = Release|Any CPU
+ {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {DB94E647-C73A-4F52-A126-AA7544CCF33B}.Release|x86.ActiveCfg = Release|Any CPU
+ {09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {09C2933C-23AC-41B7-994D-E8A5184A629C}.Debug|x86.Build.0 = Debug|Any CPU
+ {09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|x86.ActiveCfg = Release|Any CPU
+ {09C2933C-23AC-41B7-994D-E8A5184A629C}.Release|x86.Build.0 = Release|Any CPU
+ {ED253B01-24F1-43D1-AA0B-079391E105A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ED253B01-24F1-43D1-AA0B-079391E105A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ED253B01-24F1-43D1-AA0B-079391E105A9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {ED253B01-24F1-43D1-AA0B-079391E105A9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {ED253B01-24F1-43D1-AA0B-079391E105A9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {ED253B01-24F1-43D1-AA0B-079391E105A9}.Debug|x86.Build.0 = Debug|Any CPU
+ {ED253B01-24F1-43D1-AA0B-079391E105A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ED253B01-24F1-43D1-AA0B-079391E105A9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ED253B01-24F1-43D1-AA0B-079391E105A9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {ED253B01-24F1-43D1-AA0B-079391E105A9}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {ED253B01-24F1-43D1-AA0B-079391E105A9}.Release|x86.ActiveCfg = Release|Any CPU
+ {ED253B01-24F1-43D1-AA0B-079391E105A9}.Release|x86.Build.0 = Release|Any CPU
+ {741B0B05-CE96-473B-B962-6B0A347DF79A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {741B0B05-CE96-473B-B962-6B0A347DF79A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {741B0B05-CE96-473B-B962-6B0A347DF79A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {741B0B05-CE96-473B-B962-6B0A347DF79A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {741B0B05-CE96-473B-B962-6B0A347DF79A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {741B0B05-CE96-473B-B962-6B0A347DF79A}.Debug|x86.Build.0 = Debug|Any CPU
+ {741B0B05-CE96-473B-B962-6B0A347DF79A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {741B0B05-CE96-473B-B962-6B0A347DF79A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {741B0B05-CE96-473B-B962-6B0A347DF79A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {741B0B05-CE96-473B-B962-6B0A347DF79A}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {741B0B05-CE96-473B-B962-6B0A347DF79A}.Release|x86.ActiveCfg = Release|Any CPU
+ {741B0B05-CE96-473B-B962-6B0A347DF79A}.Release|x86.Build.0 = Release|Any CPU
+ {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Debug|x86.Build.0 = Debug|Any CPU
+ {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Release|x86.ActiveCfg = Release|Any CPU
+ {5C73140B-41F3-466F-A07B-3614E4D80DF9}.Release|x86.Build.0 = Release|Any CPU
+ {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Debug|x86.Build.0 = Debug|Any CPU
+ {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|x86.ActiveCfg = Release|Any CPU
+ {F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {1EE54D32-6CED-4206-ACF5-3DC1DD39D228} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3}
+ {636D79ED-7B32-487C-BDA5-D2A1AAA97371} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
+ {DB94E647-C73A-4F52-A126-AA7544CCF33B} = {C3ADD55B-B9C7-4061-8AD4-6A70D1AE3B2E}
+ {09C2933C-23AC-41B7-994D-E8A5184A629C} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
+ {ED253B01-24F1-43D1-AA0B-079391E105A9} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3}
+ {741B0B05-CE96-473B-B962-6B0A347DF79A} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
+ {5C73140B-41F3-466F-A07B-3614E4D80DF9} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
+ {F3D86714-4E64-41A6-9B36-A47B3683CF5D} = {D5F39F59-5725-4127-82E7-67028D006185}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {36C8D815-B7F1-479D-894B-E606FB8DECDA}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Configs/CoreConfig.cs b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Configs/CoreConfig.cs
new file mode 100644
index 0000000000..bbeb805e6d
--- /dev/null
+++ b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Configs/CoreConfig.cs
@@ -0,0 +1,31 @@
+// 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 BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Engines;
+using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Validators;
+
+namespace Microsoft.AspNetCore.Routing.Performance
+{
+ public class CoreConfig : ManualConfig
+ {
+ public CoreConfig()
+ {
+ Add(JitOptimizationsValidator.FailOnError);
+ Add(MemoryDiagnoser.Default);
+ Add(StatisticColumn.OperationsPerSecond);
+
+ Add(Job.Default
+ .With(BenchmarkDotNet.Environments.Runtime.Core)
+ .WithRemoveOutliers(false)
+ .With(new GcMode() { Server = true })
+ .With(RunStrategy.Throughput)
+ .WithLaunchCount(3)
+ .WithWarmupCount(5)
+ .WithTargetCount(10));
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Microsoft.AspNetCore.Routing.Performance.csproj b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Microsoft.AspNetCore.Routing.Performance.csproj
new file mode 100644
index 0000000000..453925e513
--- /dev/null
+++ b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Microsoft.AspNetCore.Routing.Performance.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
+ <TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
+ <OutputType>Exe</OutputType>
+ <ServerGarbageCollection>true</ServerGarbageCollection>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <IsPackable>false</IsPackable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="BenchmarkDotNet" Version="$(BenchmarkDotNetPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Program.cs b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Program.cs
new file mode 100644
index 0000000000..b510a13a28
--- /dev/null
+++ b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/Program.cs
@@ -0,0 +1,17 @@
+// 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.Reflection;
+using BenchmarkDotNet.Running;
+
+namespace Microsoft.AspNetCore.Routing.Performance
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs
new file mode 100644
index 0000000000..d499fd706a
--- /dev/null
+++ b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/RoutingBenchmark.cs
@@ -0,0 +1,114 @@
+// 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.Text.Encodings.Web;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.AspNetCore.Routing.Template;
+using Microsoft.AspNetCore.Routing.Tree;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Routing.Performance
+{
+ public class RoutingBenchmark
+ {
+ private const int NumberOfRequestTypes = 3;
+ private const int Iterations = 100;
+
+ private readonly IRouter _treeRouter;
+ private readonly RequestEntry[] _requests;
+
+ public RoutingBenchmark()
+ {
+ var handler = new RouteHandler((next) => Task.FromResult<object>(null));
+
+ var treeBuilder = new TreeRouteBuilder(
+ NullLoggerFactory.Instance,
+ new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
+ new DefaultInlineConstraintResolver(new OptionsManager<RouteOptions>(new OptionsFactory<RouteOptions>(Enumerable.Empty<IConfigureOptions<RouteOptions>>(), Enumerable.Empty<IPostConfigureOptions<RouteOptions>>()))));
+
+ treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets"), "default", 0);
+ treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets/{id}"), "default", 0);
+ treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets/search/{term}"), "default", 0);
+ treeBuilder.MapInbound(handler, TemplateParser.Parse("admin/users/{id}"), "default", 0);
+ treeBuilder.MapInbound(handler, TemplateParser.Parse("admin/users/{id}/manage"), "default", 0);
+
+ _treeRouter = treeBuilder.Build();
+
+ _requests = new RequestEntry[NumberOfRequestTypes];
+
+ _requests[0].HttpContext = new DefaultHttpContext();
+ _requests[0].HttpContext.Request.Path = "/api/Widgets/5";
+ _requests[0].IsMatch = true;
+ _requests[0].Values = new RouteValueDictionary(new { id = 5 });
+
+ _requests[1].HttpContext = new DefaultHttpContext();
+ _requests[1].HttpContext.Request.Path = "/admin/users/17/mAnage";
+ _requests[1].IsMatch = true;
+ _requests[1].Values = new RouteValueDictionary(new { id = 17 });
+
+ _requests[2].HttpContext = new DefaultHttpContext();
+ _requests[2].HttpContext.Request.Path = "/api/Widgets/search/dldldldldld/ddld";
+ _requests[2].IsMatch = false;
+ _requests[2].Values = new RouteValueDictionary();
+ }
+
+ [Benchmark(Description = "Attribute Routing", OperationsPerInvoke = Iterations * NumberOfRequestTypes)]
+ public async Task AttributeRouting()
+ {
+ for (var i = 0; i < Iterations; i++)
+ {
+ for (var j = 0; j < _requests.Length; j++)
+ {
+ var context = new RouteContext(_requests[j].HttpContext);
+
+ await _treeRouter.RouteAsync(context);
+
+ Verify(context, j);
+ }
+ }
+ }
+
+ private void Verify(RouteContext context, int i)
+ {
+ if (_requests[i].IsMatch)
+ {
+ if (context.Handler == null)
+ {
+ throw new InvalidOperationException($"Failed {i}");
+ }
+
+ var values = _requests[i].Values;
+ if (values.Count != context.RouteData.Values.Count)
+ {
+ throw new InvalidOperationException($"Failed {i}");
+ }
+ }
+ else
+ {
+ if (context.Handler != null)
+ {
+ throw new InvalidOperationException($"Failed {i}");
+ }
+
+ if (context.RouteData.Values.Count != 0)
+ {
+ throw new InvalidOperationException($"Failed {i}");
+ }
+ }
+ }
+
+ private struct RequestEntry
+ {
+ public HttpContext HttpContext;
+ public bool IsMatch;
+ public RouteValueDictionary Values;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/readme.md b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/readme.md
new file mode 100644
index 0000000000..38ce0ff71a
--- /dev/null
+++ b/src/Routing/benchmarks/Microsoft.AspNetCore.Routing.Performance/readme.md
@@ -0,0 +1,11 @@
+Compile the solution in Release mode (so binaries are available in release)
+
+To run a specific benchmark add it as parameter.
+```
+dotnet run -c Release <benchmark_name>
+```
+
+If you run without any parameters, you'll be offered the list of all benchmarks and get to choose.
+```
+dotnet run -c Release
+``` \ No newline at end of file
diff --git a/src/Routing/build/Key.snk b/src/Routing/build/Key.snk
new file mode 100644
index 0000000000..e10e4889c1
--- /dev/null
+++ b/src/Routing/build/Key.snk
Binary files differ
diff --git a/src/Routing/build/dependencies.props b/src/Routing/build/dependencies.props
new file mode 100644
index 0000000000..6aafe4467f
--- /dev/null
+++ b/src/Routing/build/dependencies.props
@@ -0,0 +1,46 @@
+<Project>
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ </PropertyGroup>
+
+ <!-- These package versions may be overridden or updated by automation. -->
+ <PropertyGroup Label="Package Versions: Auto">
+ <BenchmarkDotNetPackageVersion>0.10.13</BenchmarkDotNetPackageVersion>
+ <InternalAspNetCoreSdkPackageVersion>2.1.3-rtm-15802</InternalAspNetCoreSdkPackageVersion>
+ <MicrosoftNETCoreApp20PackageVersion>2.0.0</MicrosoftNETCoreApp20PackageVersion>
+ <MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion>
+ <MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
+ <MoqPackageVersion>4.7.49</MoqPackageVersion>
+ <NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
+ <XunitAnalyzersPackageVersion>0.8.0</XunitAnalyzersPackageVersion>
+ <XunitPackageVersion>2.3.1</XunitPackageVersion>
+ <XunitRunnerVisualStudioPackageVersion>2.4.0-beta.1.build3945</XunitRunnerVisualStudioPackageVersion>
+ </PropertyGroup>
+
+ <!-- This may import a generated file which may override the variables above. -->
+ <Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
+
+ <!-- These are package versions that should not be overridden or updated by automation. -->
+ <PropertyGroup Label="Package Versions: Pinned">
+ <MicrosoftAspNetCoreHostingAbstractionsPackageVersion>2.1.1</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
+ <MicrosoftAspNetCoreHostingPackageVersion>2.1.1</MicrosoftAspNetCoreHostingPackageVersion>
+ <MicrosoftAspNetCoreHttpAbstractionsPackageVersion>2.1.1</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
+ <MicrosoftAspNetCoreHttpExtensionsPackageVersion>2.1.1</MicrosoftAspNetCoreHttpExtensionsPackageVersion>
+ <MicrosoftAspNetCoreHttpPackageVersion>2.1.1</MicrosoftAspNetCoreHttpPackageVersion>
+ <MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.1.1</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
+ <MicrosoftAspNetCoreServerKestrelPackageVersion>2.1.2</MicrosoftAspNetCoreServerKestrelPackageVersion>
+ <MicrosoftAspNetCoreTestHostPackageVersion>2.1.1</MicrosoftAspNetCoreTestHostPackageVersion>
+ <MicrosoftAspNetCoreTestingPackageVersion>2.1.0</MicrosoftAspNetCoreTestingPackageVersion>
+ <MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>
+ <MicrosoftExtensionsDependencyInjectionPackageVersion>2.1.1</MicrosoftExtensionsDependencyInjectionPackageVersion>
+ <MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion>2.1.1</MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion>
+ <MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
+ <MicrosoftExtensionsLoggingConsolePackageVersion>2.1.1</MicrosoftExtensionsLoggingConsolePackageVersion>
+ <MicrosoftExtensionsLoggingPackageVersion>2.1.1</MicrosoftExtensionsLoggingPackageVersion>
+ <MicrosoftExtensionsLoggingTestingPackageVersion>2.1.1</MicrosoftExtensionsLoggingTestingPackageVersion>
+ <MicrosoftExtensionsObjectPoolPackageVersion>2.1.1</MicrosoftExtensionsObjectPoolPackageVersion>
+ <MicrosoftExtensionsOptionsPackageVersion>2.1.1</MicrosoftExtensionsOptionsPackageVersion>
+ <MicrosoftExtensionsPropertyHelperSourcesPackageVersion>2.1.1</MicrosoftExtensionsPropertyHelperSourcesPackageVersion>
+ <MicrosoftExtensionsWebEncodersPackageVersion>2.1.1</MicrosoftExtensionsWebEncodersPackageVersion>
+ </PropertyGroup>
+</Project> \ No newline at end of file
diff --git a/src/Routing/build/repo.props b/src/Routing/build/repo.props
new file mode 100644
index 0000000000..dab1601c88
--- /dev/null
+++ b/src/Routing/build/repo.props
@@ -0,0 +1,15 @@
+<Project>
+ <Import Project="dependencies.props" />
+
+ <PropertyGroup>
+ <!-- These properties are use by the automation that updates dependencies.props -->
+ <LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
+ <LineupPackageVersion>2.1.0-rc1-*</LineupPackageVersion>
+ <LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp20PackageVersion)" />
+ <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/Routing/build/sources.props b/src/Routing/build/sources.props
new file mode 100644
index 0000000000..9215df9751
--- /dev/null
+++ b/src/Routing/build/sources.props
@@ -0,0 +1,17 @@
+<Project>
+ <Import Project="$(DotNetRestoreSourcePropsPath)" Condition="'$(DotNetRestoreSourcePropsPath)' != ''"/>
+
+ <PropertyGroup Label="RestoreSources">
+ <RestoreSources>$(DotNetRestoreSources)</RestoreSources>
+ <RestoreSources Condition="'$(DotNetBuildOffline)' != 'true' AND '$(AspNetUniverseBuildOffline)' != 'true' ">
+ $(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>
+ <RestoreSources Condition="'$(DotNetBuildOffline)' != 'true'">
+ $(RestoreSources);
+ https://api.nuget.org/v3/index.json;
+ </RestoreSources>
+ </PropertyGroup>
+</Project>
diff --git a/src/Routing/samples/RoutingSample.Web/PrefixRoute.cs b/src/Routing/samples/RoutingSample.Web/PrefixRoute.cs
new file mode 100644
index 0000000000..908e8a5462
--- /dev/null
+++ b/src/Routing/samples/RoutingSample.Web/PrefixRoute.cs
@@ -0,0 +1,62 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Routing;
+
+namespace RoutingSample.Web
+{
+ public class PrefixRoute : IRouter
+ {
+ private readonly IRouteHandler _target;
+ private readonly string _prefix;
+
+ public PrefixRoute(IRouteHandler target, string prefix)
+ {
+ _target = target;
+
+ if (prefix == null)
+ {
+ prefix = "/";
+ }
+ else if (prefix.Length > 0 && prefix[0] != '/')
+ {
+ // owin.RequestPath starts with a /
+ prefix = "/" + prefix;
+ }
+
+ if (prefix.Length > 1 && prefix[prefix.Length - 1] == '/')
+ {
+ prefix = prefix.Substring(0, prefix.Length - 1);
+ }
+
+ _prefix = prefix;
+ }
+
+ public Task RouteAsync(RouteContext context)
+ {
+ var requestPath = context.HttpContext.Request.Path.Value ?? string.Empty;
+ if (requestPath.StartsWith(_prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ if (requestPath.Length > _prefix.Length)
+ {
+ var lastCharacter = requestPath[_prefix.Length];
+ if (lastCharacter != '/' && lastCharacter != '#' && lastCharacter != '?')
+ {
+ return Task.FromResult(0);
+ }
+ }
+
+ context.Handler = _target.GetRequestHandler(context.HttpContext, context.RouteData);
+ }
+
+ return Task.FromResult(0);
+ }
+
+ public VirtualPathData GetVirtualPath(VirtualPathContext context)
+ {
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/samples/RoutingSample.Web/Program.cs b/src/Routing/samples/RoutingSample.Web/Program.cs
new file mode 100644
index 0000000000..27a1a8b55e
--- /dev/null
+++ b/src/Routing/samples/RoutingSample.Web/Program.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 System;
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Constraints;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace RoutingSample.Web
+{
+ public class Program
+ {
+ private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10);
+
+ public static void Main(string[] args)
+ {
+ var webHost = GetWebHostBuilder().Build();
+ webHost.Run();
+ }
+
+ // For unit testing
+ public static IWebHostBuilder GetWebHostBuilder()
+ {
+ return new WebHostBuilder()
+ .UseKestrel()
+ .UseIISIntegration()
+ .ConfigureServices(services => services.AddRouting())
+ .Configure(app => app.UseRouter(routes =>
+ {
+ routes.DefaultHandler = new RouteHandler((httpContext) =>
+ {
+ var request = httpContext.Request;
+ return httpContext.Response.WriteAsync($"Verb = {request.Method.ToUpperInvariant()} - Path = {request.Path} - Route values - {string.Join(", ", httpContext.GetRouteData().Values)}");
+ });
+
+ routes.MapGet("api/get/{id}", (request, response, routeData) => response.WriteAsync($"API Get {routeData.Values["id"]}"))
+ .MapMiddlewareRoute("api/middleware", (appBuilder) => appBuilder.Use((httpContext, next) => httpContext.Response.WriteAsync("Middleware!")))
+ .MapRoute(
+ name: "AllVerbs",
+ template: "api/all/{name}/{lastName?}",
+ defaults: new { lastName = "Doe" },
+ constraints: new { lastName = new RegexRouteConstraint(new Regex("[a-zA-Z]{3}", RegexOptions.CultureInvariant, RegexMatchTimeout)) });
+ }));
+ }
+ }
+}
diff --git a/src/Routing/samples/RoutingSample.Web/RouteBuilderExtensions.cs b/src/Routing/samples/RoutingSample.Web/RouteBuilderExtensions.cs
new file mode 100644
index 0000000000..d81735bbc2
--- /dev/null
+++ b/src/Routing/samples/RoutingSample.Web/RouteBuilderExtensions.cs
@@ -0,0 +1,43 @@
+// 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.Routing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace RoutingSample.Web
+{
+ public static class RouteBuilderExtensions
+ {
+ public static IRouteBuilder AddPrefixRoute(
+ this IRouteBuilder routeBuilder,
+ string prefix,
+ IRouteHandler handler)
+ {
+ routeBuilder.Routes.Add(new PrefixRoute(handler, prefix));
+ return routeBuilder;
+ }
+
+ public static IRouteBuilder MapLocaleRoute(
+ this IRouteBuilder routeBuilder,
+ string locale,
+ string routeTemplate,
+ object defaults)
+ {
+ var defaultsDictionary = new RouteValueDictionary(defaults);
+ defaultsDictionary.Add("locale", locale);
+
+ var constraintResolver = routeBuilder.ServiceProvider.GetService<IInlineConstraintResolver>();
+
+ var route = new Route(
+ target: routeBuilder.DefaultHandler,
+ routeTemplate: routeTemplate,
+ defaults: defaultsDictionary,
+ constraints: null,
+ dataTokens: null,
+ inlineConstraintResolver: constraintResolver);
+ routeBuilder.Routes.Add(route);
+
+ return routeBuilder;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/samples/RoutingSample.Web/RoutingSample.Web.csproj b/src/Routing/samples/RoutingSample.Web/RoutingSample.Web.csproj
new file mode 100644
index 0000000000..9c3e55c557
--- /dev/null
+++ b/src/Routing/samples/RoutingSample.Web/RoutingSample.Web.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFrameworks>netcoreapp2.1;netcoreapp2.0</TargetFrameworks>
+ <TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">$(TargetFrameworks);net461</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterion.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterion.cs
new file mode 100644
index 0000000000..efc9c742d2
--- /dev/null
+++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterion.cs
@@ -0,0 +1,14 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Routing.DecisionTree
+{
+ internal class DecisionCriterion<TItem>
+ {
+ public string Key { get; set; }
+
+ public Dictionary<object, DecisionTreeNode<TItem>> Branches { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValue.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValue.cs
new file mode 100644
index 0000000000..e5ed7a69f8
--- /dev/null
+++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValue.cs
@@ -0,0 +1,20 @@
+// 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.Routing.DecisionTree
+{
+ internal struct DecisionCriterionValue
+ {
+ private readonly object _value;
+
+ public DecisionCriterionValue(object value)
+ {
+ _value = value;
+ }
+
+ public object Value
+ {
+ get { return _value; }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValueEqualityComparer.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValueEqualityComparer.cs
new file mode 100644
index 0000000000..949b7b613c
--- /dev/null
+++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionCriterionValueEqualityComparer.cs
@@ -0,0 +1,27 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Routing.DecisionTree
+{
+ internal class DecisionCriterionValueEqualityComparer : IEqualityComparer<DecisionCriterionValue>
+ {
+ public DecisionCriterionValueEqualityComparer(IEqualityComparer<object> innerComparer)
+ {
+ InnerComparer = innerComparer;
+ }
+
+ public IEqualityComparer<object> InnerComparer { get; private set; }
+
+ public bool Equals(DecisionCriterionValue x, DecisionCriterionValue y)
+ {
+ return InnerComparer.Equals(x.Value, y.Value);
+ }
+
+ public int GetHashCode(DecisionCriterionValue obj)
+ {
+ return InnerComparer.GetHashCode(obj.Value);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeBuilder.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeBuilder.cs
new file mode 100644
index 0000000000..76792f6793
--- /dev/null
+++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeBuilder.cs
@@ -0,0 +1,225 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Routing.DecisionTree
+{
+ // This code generates a minimal tree of decision criteria that map known categorical data
+ // (key-value-pairs) to a set of inputs. Action Selection is the best example of how this
+ // can be used, so the comments here will describe the process from the point-of-view,
+ // though the decision tree is generally applicable to like-problems.
+ //
+ // Care has been taken here to keep the performance of building the data-structure at a
+ // reasonable level, as this has an impact on startup cost for action selection. Additionally
+ // we want to hold on to the minimal amount of memory needed once we've built the tree.
+ //
+ // Ex:
+ // Given actions like the following, create a decision tree that will help action
+ // selection work efficiently.
+ //
+ // Given any set of route data it should be possible to traverse the tree using the
+ // presence our route data keys (like action), and whether or not they match any of
+ // the known values for that route data key, to find the set of actions that match
+ // the route data.
+ //
+ // Actions:
+ //
+ // { controller = "Home", action = "Index" }
+ // { controller = "Products", action = "Index" }
+ // { controller = "Products", action = "Buy" }
+ // { area = "Admin", controller = "Users", action = "AddUser" }
+ //
+ // The generated tree looks like this (json-like-notation):
+ //
+ // {
+ // action : {
+ // "AddUser" : {
+ // controller : {
+ // "Users" : {
+ // area : {
+ // "Admin" : match { area = "Admin", controller = "Users", action = "AddUser" }
+ // }
+ // }
+ // }
+ // },
+ // "Buy" : {
+ // controller : {
+ // "Products" : {
+ // area : {
+ // null : match { controller = "Products", action = "Buy" }
+ // }
+ // }
+ // }
+ // },
+ // "Index" : {
+ // controller : {
+ // "Home" : {
+ // area : {
+ // null : match { controller = "Home", action = "Index" }
+ // }
+ // }
+ // "Products" : {
+ // area : {
+ // "null" : match { controller = "Products", action = "Index" }
+ // }
+ // }
+ // }
+ // }
+ // }
+ // }
+ internal static class DecisionTreeBuilder<TItem>
+ {
+ public static DecisionTreeNode<TItem> GenerateTree(IReadOnlyList<TItem> items, IClassifier<TItem> classifier)
+ {
+ var itemDescriptors = new List<ItemDescriptor<TItem>>();
+ for (var i = 0; i < items.Count; i++)
+ {
+ itemDescriptors.Add(new ItemDescriptor<TItem>()
+ {
+ Criteria = classifier.GetCriteria(items[i]),
+ Index = i,
+ Item = items[i],
+ });
+ }
+
+ var comparer = new DecisionCriterionValueEqualityComparer(classifier.ValueComparer);
+ return GenerateNode(
+ new TreeBuilderContext(),
+ comparer,
+ itemDescriptors);
+ }
+
+ private static DecisionTreeNode<TItem> GenerateNode(
+ TreeBuilderContext context,
+ DecisionCriterionValueEqualityComparer comparer,
+ IList<ItemDescriptor<TItem>> items)
+ {
+ // The extreme use of generics here is intended to reduce the number of intermediate
+ // allocations of wrapper classes. Performance testing found that building these trees allocates
+ // significant memory that we can avoid and that it has a real impact on startup.
+ var criteria = new Dictionary<string, Criterion>(StringComparer.OrdinalIgnoreCase);
+
+ // Matches are items that have no remaining criteria - at this point in the tree
+ // they are considered accepted.
+ var matches = new List<TItem>();
+
+ // For each item in the working set, we want to map it to it's possible criteria-branch
+ // pairings, then reduce that tree to the minimal set.
+ foreach (var item in items)
+ {
+ var unsatisfiedCriteria = 0;
+
+ foreach (var kvp in item.Criteria)
+ {
+ // context.CurrentCriteria is the logical 'stack' of criteria that we've already processed
+ // on this branch of the tree.
+ if (context.CurrentCriteria.Contains(kvp.Key))
+ {
+ continue;
+ }
+
+ unsatisfiedCriteria++;
+
+ Criterion criterion;
+ if (!criteria.TryGetValue(kvp.Key, out criterion))
+ {
+ criterion = new Criterion(comparer);
+ criteria.Add(kvp.Key, criterion);
+ }
+
+ List<ItemDescriptor<TItem>> branch;
+ if (!criterion.TryGetValue(kvp.Value, out branch))
+ {
+ branch = new List<ItemDescriptor<TItem>>();
+ criterion.Add(kvp.Value, branch);
+ }
+
+ branch.Add(item);
+ }
+
+ // If all of the criteria on item are satisfied by the 'stack' then this item is a match.
+ if (unsatisfiedCriteria == 0)
+ {
+ matches.Add(item.Item);
+ }
+ }
+
+ // Iterate criteria in order of branchiness to determine which one to explore next. If a criterion
+ // has no 'new' matches under it then we can just eliminate that part of the tree.
+ var reducedCriteria = new List<DecisionCriterion<TItem>>();
+ foreach (var criterion in criteria.OrderByDescending(c => c.Value.Count))
+ {
+ var reducedBranches = new Dictionary<object, DecisionTreeNode<TItem>>(comparer.InnerComparer);
+
+ foreach (var branch in criterion.Value)
+ {
+ var reducedItems = new List<ItemDescriptor<TItem>>();
+ foreach (var item in branch.Value)
+ {
+ if (context.MatchedItems.Add(item))
+ {
+ reducedItems.Add(item);
+ }
+ }
+
+ if (reducedItems.Count > 0)
+ {
+ var childContext = new TreeBuilderContext(context);
+ childContext.CurrentCriteria.Add(criterion.Key);
+
+ var newBranch = GenerateNode(childContext, comparer, branch.Value);
+ reducedBranches.Add(branch.Key.Value, newBranch);
+ }
+ }
+
+ if (reducedBranches.Count > 0)
+ {
+ var newCriterion = new DecisionCriterion<TItem>()
+ {
+ Key = criterion.Key,
+ Branches = reducedBranches,
+ };
+
+ reducedCriteria.Add(newCriterion);
+ }
+ }
+
+ return new DecisionTreeNode<TItem>()
+ {
+ Criteria = reducedCriteria.ToList(),
+ Matches = matches,
+ };
+ }
+
+ private class TreeBuilderContext
+ {
+ public TreeBuilderContext()
+ {
+ CurrentCriteria = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ MatchedItems = new HashSet<ItemDescriptor<TItem>>();
+ }
+
+ public TreeBuilderContext(TreeBuilderContext other)
+ {
+ CurrentCriteria = new HashSet<string>(other.CurrentCriteria, StringComparer.OrdinalIgnoreCase);
+ MatchedItems = new HashSet<ItemDescriptor<TItem>>();
+ }
+
+ public HashSet<string> CurrentCriteria { get; private set; }
+
+ public HashSet<ItemDescriptor<TItem>> MatchedItems { get; private set; }
+ }
+
+ // Subclass just to give a logical name to a mess of generics
+ private class Criterion : Dictionary<DecisionCriterionValue, List<ItemDescriptor<TItem>>>
+ {
+ public Criterion(DecisionCriterionValueEqualityComparer comparer)
+ : base(comparer)
+ {
+ }
+ }
+ }
+}
diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeNode.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeNode.cs
new file mode 100644
index 0000000000..1be3064c62
--- /dev/null
+++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/DecisionTreeNode.cs
@@ -0,0 +1,20 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Routing.DecisionTree
+{
+ // Data structure representing a node in a decision tree. These are created in DecisionTreeBuilder
+ // and walked to find a set of items matching some input criteria.
+ internal class DecisionTreeNode<TItem>
+ {
+ // The list of matches for the current node. This represents a set of items that have had all
+ // of their criteria matched if control gets to this point in the tree.
+ public IList<TItem> Matches { get; set; }
+
+ // Additional criteria that further branch out from this node. Walk these to fine more items
+ // matching the input data.
+ public IList<DecisionCriterion<TItem>> Criteria { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/IClassifier.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/IClassifier.cs
new file mode 100644
index 0000000000..3008fdfa75
--- /dev/null
+++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/IClassifier.cs
@@ -0,0 +1,14 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Routing.DecisionTree
+{
+ internal interface IClassifier<TItem>
+ {
+ IDictionary<string, DecisionCriterionValue> GetCriteria(TItem item);
+
+ IEqualityComparer<object> ValueComparer { get; }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/ItemDescriptor.cs b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/ItemDescriptor.cs
new file mode 100644
index 0000000000..84a6279c27
--- /dev/null
+++ b/src/Routing/shared/Microsoft.AspNetCore.Routing.DecisionTree.Sources/ItemDescriptor.cs
@@ -0,0 +1,16 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Routing.DecisionTree
+{
+ internal class ItemDescriptor<TItem>
+ {
+ public IDictionary<string, DecisionCriterionValue> Criteria { get; set; }
+
+ public int Index { get; set; }
+
+ public TItem Item { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Directory.Build.props b/src/Routing/src/Directory.Build.props
new file mode 100644
index 0000000000..1e0980f663
--- /dev/null
+++ b/src/Routing/src/Directory.Build.props
@@ -0,0 +1,7 @@
+<Project>
+ <Import Project="..\Directory.Build.props" />
+
+ <ItemGroup>
+ <PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteConstraint.cs
new file mode 100644
index 0000000000..00eb916510
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteConstraint.cs
@@ -0,0 +1,33 @@
+// 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.Routing
+{
+ /// <summary>
+ /// Defines the contract that a class must implement in order to check whether a URL parameter
+ /// value is valid for a constraint.
+ /// </summary>
+ public interface IRouteConstraint
+ {
+ /// <summary>
+ /// Determines whether the URL parameter contains a valid value for this constraint.
+ /// </summary>
+ /// <param name="httpContext">An object that encapsulates information about the HTTP request.</param>
+ /// <param name="route">The router that this constraint belongs to.</param>
+ /// <param name="routeKey">The name of the parameter that is being checked.</param>
+ /// <param name="values">A dictionary that contains the parameters for the URL.</param>
+ /// <param name="routeDirection">
+ /// An object that indicates whether the constraint check is being performed
+ /// when an incoming request is being handled or when a URL is being generated.
+ /// </param>
+ /// <returns><c>true</c> if the URL parameter contains a valid value; otherwise, <c>false</c>.</returns>
+ bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection);
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteHandler.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteHandler.cs
new file mode 100644
index 0000000000..15aaaeda8b
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouteHandler.cs
@@ -0,0 +1,24 @@
+// 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.Routing
+{
+ /// <summary>
+ /// Defines a contract for a handler of a route.
+ /// </summary>
+ public interface IRouteHandler
+ {
+ /// <summary>
+ /// Gets a <see cref="RequestDelegate"/> to handle the request, based on the provided
+ /// <paramref name="routeData"/>.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
+ /// <param name="routeData">The <see cref="RouteData"/> associated with the current routing match.</param>
+ /// <returns>
+ /// A <see cref="RequestDelegate"/>, or <c>null</c> if the handler cannot handle this request.
+ /// </returns>
+ RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData);
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouter.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouter.cs
new file mode 100644
index 0000000000..06a62bc9ba
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRouter.cs
@@ -0,0 +1,14 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public interface IRouter
+ {
+ Task RouteAsync(RouteContext context);
+
+ VirtualPathData GetVirtualPath(VirtualPathContext context);
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRoutingFeature.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRoutingFeature.cs
new file mode 100644
index 0000000000..bf1897c8ee
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/IRoutingFeature.cs
@@ -0,0 +1,16 @@
+// 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.Routing
+{
+ /// <summary>
+ /// A feature interface for routing functionality.
+ /// </summary>
+ public interface IRoutingFeature
+ {
+ /// <summary>
+ /// Gets or sets the <see cref="Routing.RouteData"/> associated with the current request.
+ /// </summary>
+ RouteData RouteData { get; set; }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj
new file mode 100644
index 0000000000..633a49d503
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Microsoft.AspNetCore.Routing.Abstractions.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core abstractions for routing requests to application logic and for generating links.
+Commonly used types:
+Microsoft.AspNetCore.Routing.IRouter
+Microsoft.AspNetCore.Routing.RouteData</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;routing</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="$(MicrosoftAspNetCoreHttpAbstractionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.PropertyHelper.Sources" Version="$(MicrosoftExtensionsPropertyHelperSourcesPackageVersion)" PrivateAssets="All" />
+ </ItemGroup>
+</Project>
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/AssemblyInfo.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..848ab925ed
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// 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("Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..8fdf6b8715
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Properties/Resources.Designer.cs
@@ -0,0 +1,58 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Routing.Abstractions
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Routing.Abstractions.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// An element with the key '{0}' already exists in the {1}.
+ /// </summary>
+ internal static string RouteValueDictionary_DuplicateKey
+ {
+ get => GetString("RouteValueDictionary_DuplicateKey");
+ }
+
+ /// <summary>
+ /// An element with the key '{0}' already exists in the {1}.
+ /// </summary>
+ internal static string FormatRouteValueDictionary_DuplicateKey(object p0, object p1)
+ => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicateKey"), p0, p1);
+
+ /// <summary>
+ /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.
+ /// </summary>
+ internal static string RouteValueDictionary_DuplicatePropertyName
+ {
+ get => GetString("RouteValueDictionary_DuplicatePropertyName");
+ }
+
+ /// <summary>
+ /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.
+ /// </summary>
+ internal static string FormatRouteValueDictionary_DuplicatePropertyName(object p0, object p1, object p2, object p3)
+ => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicatePropertyName"), p0, p1, p2, p3);
+
+ 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/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx
new file mode 100644
index 0000000000..40e651af14
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/Resources.resx
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="RouteValueDictionary_DuplicateKey" xml:space="preserve">
+ <value>An element with the key '{0}' already exists in the {1}.</value>
+ </data>
+ <data name="RouteValueDictionary_DuplicatePropertyName" xml:space="preserve">
+ <value>The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteContext.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteContext.cs
new file mode 100644
index 0000000000..767f39b1ec
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteContext.cs
@@ -0,0 +1,58 @@
+// 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.Routing
+{
+ /// <summary>
+ /// A context object for <see cref="IRouter.RouteAsync(RouteContext)"/>.
+ /// </summary>
+ public class RouteContext
+ {
+ private RouteData _routeData;
+
+ /// <summary>
+ /// Creates a new <see cref="RouteContext"/> for the provided <paramref name="httpContext"/>.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="Http.HttpContext"/> associated with the current request.</param>
+ public RouteContext(HttpContext httpContext)
+ {
+ HttpContext = httpContext;
+
+ RouteData = new RouteData();
+ }
+
+ /// <summary>
+ /// Gets or sets the handler for the request. An <see cref="IRouter"/> should set <see cref="Handler"/>
+ /// when it matches.
+ /// </summary>
+ public RequestDelegate Handler { get; set; }
+
+ /// <summary>
+ /// Gets the <see cref="Http.HttpContext"/> associated with the current request.
+ /// </summary>
+ public HttpContext HttpContext { get; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="Routing.RouteData"/> associated with the current context.
+ /// </summary>
+ public RouteData RouteData
+ {
+ get
+ {
+ return _routeData;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(RouteData));
+ }
+
+ _routeData = value;
+ }
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteData.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteData.cs
new file mode 100644
index 0000000000..0ece2b91de
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteData.cs
@@ -0,0 +1,296 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ /// <summary>
+ /// Information about the current routing path.
+ /// </summary>
+ public class RouteData
+ {
+ private RouteValueDictionary _dataTokens;
+ private List<IRouter> _routers;
+ private RouteValueDictionary _values;
+
+ /// <summary>
+ /// Creates a new <see cref="RouteData"/> instance.
+ /// </summary>
+ public RouteData()
+ {
+ // Perf: Avoid allocating collections unless needed.
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="RouteData"/> instance with values copied from <paramref name="other"/>.
+ /// </summary>
+ /// <param name="other">The other <see cref="RouteData"/> instance to copy.</param>
+ public RouteData(RouteData other)
+ {
+ if (other == null)
+ {
+ throw new ArgumentNullException(nameof(other));
+ }
+
+ // Perf: Avoid allocating collections unless we need to make a copy.
+ if (other._routers != null)
+ {
+ _routers = new List<IRouter>(other.Routers);
+ }
+
+ if (other._dataTokens != null)
+ {
+ _dataTokens = new RouteValueDictionary(other._dataTokens);
+ }
+
+ if (other._values != null)
+ {
+ _values = new RouteValueDictionary(other._values);
+ }
+ }
+
+ /// <summary>
+ /// Gets the data tokens produced by routes on the current routing path.
+ /// </summary>
+ public RouteValueDictionary DataTokens
+ {
+ get
+ {
+ if (_dataTokens == null)
+ {
+ _dataTokens = new RouteValueDictionary();
+ }
+
+ return _dataTokens;
+ }
+ }
+
+ /// <summary>
+ /// Gets the list of <see cref="IRouter"/> instances on the current routing path.
+ /// </summary>
+ public IList<IRouter> Routers
+ {
+ get
+ {
+ if (_routers == null)
+ {
+ _routers = new List<IRouter>();
+ }
+
+ return _routers;
+ }
+ }
+
+ /// <summary>
+ /// Gets the set of values produced by routes on the current routing path.
+ /// </summary>
+ public RouteValueDictionary Values
+ {
+ get
+ {
+ if (_values == null)
+ {
+ _values = new RouteValueDictionary();
+ }
+
+ return _values;
+ }
+ }
+
+ /// <summary>
+ /// <para>
+ /// Creates a snapshot of the current state of the <see cref="RouteData"/> before appending
+ /// <paramref name="router"/> to <see cref="Routers"/>, merging <paramref name="values"/> into
+ /// <see cref="Values"/>, and merging <paramref name="dataTokens"/> into <see cref="DataTokens"/>.
+ /// </para>
+ /// <para>
+ /// Call <see cref="RouteDataSnapshot.Restore"/> to restore the state of this <see cref="RouteData"/>
+ /// to the state at the time of calling
+ /// <see cref="PushState(IRouter, RouteValueDictionary, RouteValueDictionary)"/>.
+ /// </para>
+ /// </summary>
+ /// <param name="router">
+ /// An <see cref="IRouter"/> to append to <see cref="Routers"/>. If <c>null</c>, then <see cref="Routers"/>
+ /// will not be changed.
+ /// </param>
+ /// <param name="values">
+ /// A <see cref="RouteValueDictionary"/> to merge into <see cref="Values"/>. If <c>null</c>, then
+ /// <see cref="Values"/> will not be changed.
+ /// </param>
+ /// <param name="dataTokens">
+ /// A <see cref="RouteValueDictionary"/> to merge into <see cref="DataTokens"/>. If <c>null</c>, then
+ /// <see cref="DataTokens"/> will not be changed.
+ /// </param>
+ /// <returns>A <see cref="RouteDataSnapshot"/> that captures the current state.</returns>
+ public RouteDataSnapshot PushState(IRouter router, RouteValueDictionary values, RouteValueDictionary dataTokens)
+ {
+ // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in
+ // Array.CopyTo inside the List(IEnumerable<T>) constructor.
+ List<IRouter> routers = null;
+ var count = _routers?.Count;
+ if (count > 0)
+ {
+ routers = new List<IRouter>(count.Value);
+ for (var i = 0; i < count.Value; i++)
+ {
+ routers.Add(_routers[i]);
+ }
+ }
+
+ var snapshot = new RouteDataSnapshot(
+ this,
+ _dataTokens?.Count > 0 ? new RouteValueDictionary(_dataTokens) : null,
+ routers,
+ _values?.Count > 0 ? new RouteValueDictionary(_values) : null);
+
+ if (router != null)
+ {
+ Routers.Add(router);
+ }
+
+ if (values != null)
+ {
+ foreach (var kvp in values)
+ {
+ if (kvp.Value != null)
+ {
+ Values[kvp.Key] = kvp.Value;
+ }
+ }
+ }
+
+ if (dataTokens != null)
+ {
+ foreach (var kvp in dataTokens)
+ {
+ DataTokens[kvp.Key] = kvp.Value;
+ }
+ }
+
+ return snapshot;
+ }
+
+ /// <summary>
+ /// A snapshot of the state of a <see cref="RouteData"/> instance.
+ /// </summary>
+ public struct RouteDataSnapshot
+ {
+ private readonly RouteData _routeData;
+ private readonly RouteValueDictionary _dataTokens;
+ private readonly IList<IRouter> _routers;
+ private readonly RouteValueDictionary _values;
+
+ /// <summary>
+ /// Creates a new <see cref="RouteDataSnapshot"/> for <paramref name="routeData"/>.
+ /// </summary>
+ /// <param name="routeData">The <see cref="RouteData"/>.</param>
+ /// <param name="dataTokens">The data tokens.</param>
+ /// <param name="routers">The routers.</param>
+ /// <param name="values">The route values.</param>
+ public RouteDataSnapshot(
+ RouteData routeData,
+ RouteValueDictionary dataTokens,
+ IList<IRouter> routers,
+ RouteValueDictionary values)
+ {
+ if (routeData == null)
+ {
+ throw new ArgumentNullException(nameof(routeData));
+ }
+
+ _routeData = routeData;
+ _dataTokens = dataTokens;
+ _routers = routers;
+ _values = values;
+ }
+
+ /// <summary>
+ /// Restores the <see cref="RouteData"/> to the captured state.
+ /// </summary>
+ public void Restore()
+ {
+ if (_routeData._dataTokens == null && _dataTokens == null)
+ {
+ // Do nothing
+ }
+ else if (_dataTokens == null)
+ {
+ _routeData._dataTokens.Clear();
+ }
+ else
+ {
+ _routeData._dataTokens.Clear();
+
+ foreach (var kvp in _dataTokens)
+ {
+ _routeData._dataTokens.Add(kvp.Key, kvp.Value);
+ }
+ }
+
+ if (_routeData._routers == null && _routers == null)
+ {
+ // Do nothing
+ }
+ else if (_routers == null)
+ {
+ // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in
+ // Array.Clear inside the List.Clear() method.
+ var routers = _routeData._routers;
+ for (var i = routers.Count - 1; i >= 0 ; i--)
+ {
+ routers.RemoveAt(i);
+ }
+ }
+ else
+ {
+ // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in
+ // Array.Clear inside the List.Clear() method.
+ //
+ // We want to basically copy the contents of _routers in _routeData._routers - this change does
+ // that with the minimal number of reads/writes and without calling Clear().
+ var routers = _routeData._routers;
+ var snapshotRouters = _routers;
+
+ // This is made more complicated by the fact that List[int] throws if i == Count, so we have
+ // to do two loops and call Add for those cases.
+ var i = 0;
+ for (; i < snapshotRouters.Count && i < routers.Count; i++)
+ {
+ routers[i] = snapshotRouters[i];
+ }
+
+ for (; i < snapshotRouters.Count; i++)
+ {
+ routers.Add(snapshotRouters[i]);
+ }
+
+ // Trim excess - again avoiding RemoveRange because it uses native methods.
+ for (i = routers.Count - 1; i >= snapshotRouters.Count; i--)
+ {
+ routers.RemoveAt(i);
+ }
+ }
+
+ if (_routeData._values == null && _values == null)
+ {
+ // Do nothing
+ }
+ else if (_values == null)
+ {
+ _routeData._values.Clear();
+ }
+ else
+ {
+ _routeData._values.Clear();
+
+ foreach (var kvp in _values)
+ {
+ _routeData._values.Add(kvp.Key, kvp.Value);
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteDirection.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteDirection.cs
new file mode 100644
index 0000000000..f19ac2b4ec
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteDirection.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.
+
+namespace Microsoft.AspNetCore.Routing
+{
+ /// <summary>
+ /// Indicates whether ASP.NET routing is processing a URL from an HTTP request or generating a URL.
+ /// </summary>
+ public enum RouteDirection
+ {
+ /// <summary>
+ /// A URL from a client is being processed.
+ /// </summary>
+ IncomingRequest,
+
+ /// <summary>
+ /// A URL is being created based on the route definition.
+ /// </summary>
+ UrlGeneration,
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs
new file mode 100644
index 0000000000..d0bc8e9ecf
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs
@@ -0,0 +1,790 @@
+// 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;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.AspNetCore.Routing.Abstractions;
+using Microsoft.Extensions.Internal;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ /// <summary>
+ /// An <see cref="IDictionary{String, Object}"/> type for route values.
+ /// </summary>
+ public class RouteValueDictionary : IDictionary<string, object>, IReadOnlyDictionary<string, object>
+ {
+ internal Storage _storage;
+
+ /// <summary>
+ /// Creates an empty <see cref="RouteValueDictionary"/>.
+ /// </summary>
+ public RouteValueDictionary()
+ {
+ _storage = EmptyStorage.Instance;
+ }
+
+ /// <summary>
+ /// Creates a <see cref="RouteValueDictionary"/> initialized with the specified <paramref name="values"/>.
+ /// </summary>
+ /// <param name="values">An object to initialize the dictionary. The value can be of type
+ /// <see cref="IDictionary{TKey, TValue}"/> or <see cref="IReadOnlyDictionary{TKey, TValue}"/>
+ /// or an object with public properties as key-value pairs.
+ /// </param>
+ /// <remarks>
+ /// If the value is a dictionary or other <see cref="IEnumerable{T}"/> of <see cref="KeyValuePair{String, Object}"/>,
+ /// then its entries are copied. Otherwise the object is interpreted as a set of key-value pairs where the
+ /// property names are keys, and property values are the values, and copied into the dictionary.
+ /// Only public instance non-index properties are considered.
+ /// </remarks>
+ public RouteValueDictionary(object values)
+ {
+ var dictionary = values as RouteValueDictionary;
+ if (dictionary != null)
+ {
+ var listStorage = dictionary._storage as ListStorage;
+ if (listStorage != null)
+ {
+ _storage = new ListStorage(listStorage);
+ return;
+ }
+
+ var propertyStorage = dictionary._storage as PropertyStorage;
+ if (propertyStorage != null)
+ {
+ // PropertyStorage is immutable so we can just copy it.
+ _storage = dictionary._storage;
+ return;
+ }
+
+ // If we get here, it's an EmptyStorage.
+ _storage = EmptyStorage.Instance;
+ return;
+ }
+
+ var keyValueEnumerable = values as IEnumerable<KeyValuePair<string, object>>;
+ if (keyValueEnumerable != null)
+ {
+ var listStorage = new ListStorage();
+ _storage = listStorage;
+ foreach (var kvp in keyValueEnumerable)
+ {
+ if (listStorage.ContainsKey(kvp.Key))
+ {
+ var message = Resources.FormatRouteValueDictionary_DuplicateKey(kvp.Key, nameof(RouteValueDictionary));
+ throw new ArgumentException(message, nameof(values));
+ }
+
+ listStorage.Add(kvp);
+ }
+
+ return;
+ }
+
+ var stringValueEnumerable = values as IEnumerable<KeyValuePair<string, string>>;
+ if (stringValueEnumerable != null)
+ {
+ var listStorage = new ListStorage();
+ _storage = listStorage;
+ foreach (var kvp in stringValueEnumerable)
+ {
+ if (listStorage.ContainsKey(kvp.Key))
+ {
+ var message = Resources.FormatRouteValueDictionary_DuplicateKey(kvp.Key, nameof(RouteValueDictionary));
+ throw new ArgumentException(message, nameof(values));
+ }
+
+ listStorage.Add(new KeyValuePair<string, object>(kvp.Key, kvp.Value));
+ }
+
+ return;
+ }
+
+ if (values != null)
+ {
+ _storage = new PropertyStorage(values);
+ return;
+ }
+
+ _storage = EmptyStorage.Instance;
+ }
+
+ /// <inheritdoc />
+ public object this[string key]
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(key))
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ object value;
+ TryGetValue(key, out value);
+ return value;
+ }
+
+ set
+ {
+ if (string.IsNullOrEmpty(key))
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (!_storage.TrySetValue(key, value))
+ {
+ Upgrade();
+ _storage.TrySetValue(key, value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the comparer for this dictionary.
+ /// </summary>
+ /// <remarks>
+ /// This will always be a reference to <see cref="StringComparer.OrdinalIgnoreCase"/>
+ /// </remarks>
+ public IEqualityComparer<string> Comparer => StringComparer.OrdinalIgnoreCase;
+
+ /// <inheritdoc />
+ public int Count => _storage.Count;
+
+ /// <inheritdoc />
+ bool ICollection<KeyValuePair<string, object>>.IsReadOnly => false;
+
+ /// <inheritdoc />
+ public ICollection<string> Keys
+ {
+ get
+ {
+ Upgrade();
+
+ var list = (ListStorage)_storage;
+ var keys = new string[list.Count];
+ for (var i = 0; i < keys.Length; i++)
+ {
+ keys[i] = list[i].Key;
+ }
+
+ return keys;
+ }
+ }
+
+ IEnumerable<string> IReadOnlyDictionary<string, object>.Keys
+ {
+ get
+ {
+ return Keys;
+ }
+ }
+
+ /// <inheritdoc />
+ public ICollection<object> Values
+ {
+ get
+ {
+ Upgrade();
+
+ var list = (ListStorage)_storage;
+ var values = new object[list.Count];
+ for (var i = 0; i < values.Length; i++)
+ {
+ values[i] = list[i].Value;
+ }
+
+ return values;
+ }
+ }
+
+ IEnumerable<object> IReadOnlyDictionary<string, object>.Values
+ {
+ get
+ {
+ return Values;
+ }
+ }
+
+ /// <inheritdoc />
+ void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
+ {
+ Add(item.Key, item.Value);
+ }
+
+ /// <inheritdoc />
+ public void Add(string key, object value)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ Upgrade();
+
+ var list = (ListStorage)_storage;
+ for (var i = 0; i < list.Count; i++)
+ {
+ if (string.Equals(list[i].Key, key, StringComparison.OrdinalIgnoreCase))
+ {
+ var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary));
+ throw new ArgumentException(message, nameof(key));
+ }
+ }
+
+ list.Add(new KeyValuePair<string, object>(key, value));
+ }
+
+ /// <inheritdoc />
+ public void Clear()
+ {
+ if (_storage.Count == 0)
+ {
+ return;
+ }
+
+ Upgrade();
+
+ var list = (ListStorage)_storage;
+ list.Clear();
+ }
+
+ /// <inheritdoc />
+ bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
+ {
+ if (_storage.Count == 0)
+ {
+ return false;
+ }
+
+ Upgrade();
+
+ var list = (ListStorage)_storage;
+ for (var i = 0; i < list.Count; i++)
+ {
+ if (string.Equals(list[i].Key, item.Key, StringComparison.OrdinalIgnoreCase))
+ {
+ return EqualityComparer<object>.Default.Equals(list[i].Value, item.Value);
+ }
+ }
+
+ return false;
+ }
+
+ /// <inheritdoc />
+ public bool ContainsKey(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ return _storage.ContainsKey(key);
+ }
+
+ /// <inheritdoc />
+ void ICollection<KeyValuePair<string, object>>.CopyTo(
+ KeyValuePair<string, object>[] array,
+ int arrayIndex)
+ {
+ if (array == null)
+ {
+ throw new ArgumentNullException(nameof(array));
+ }
+
+ if (arrayIndex < 0 || arrayIndex > array.Length || array.Length - arrayIndex < this.Count)
+ {
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex));
+ }
+
+ if (_storage.Count == 0)
+ {
+ return;
+ }
+
+ Upgrade();
+
+ var list = (ListStorage)_storage;
+ list.CopyTo(array, arrayIndex);
+ }
+
+ /// <inheritdoc />
+ public Enumerator GetEnumerator()
+ {
+ return new Enumerator(this);
+ }
+
+ /// <inheritdoc />
+ IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ /// <inheritdoc />
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ /// <inheritdoc />
+ bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
+ {
+ if (_storage.Count == 0)
+ {
+ return false;
+ }
+
+ Upgrade();
+
+ var list = (ListStorage)_storage;
+ for (var i = 0; i < list.Count; i++)
+ {
+ if (string.Equals(list[i].Key, item.Key, StringComparison.OrdinalIgnoreCase) &&
+ EqualityComparer<object>.Default.Equals(list[i].Value, item.Value))
+ {
+ list.RemoveAt(i);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <inheritdoc />
+ public bool Remove(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (_storage.Count == 0)
+ {
+ return false;
+ }
+
+ Upgrade();
+
+ var list = (ListStorage)_storage;
+ for (var i = 0; i < list.Count; i++)
+ {
+ if (string.Equals(list[i].Key, key, StringComparison.OrdinalIgnoreCase))
+ {
+ list.RemoveAt(i);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <inheritdoc />
+ public bool TryGetValue(string key, out object value)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ return _storage.TryGetValue(key, out value);
+ }
+
+ private void Upgrade()
+ {
+ _storage.Upgrade(ref _storage);
+ }
+
+ public struct Enumerator : IEnumerator<KeyValuePair<string, object>>
+ {
+ private readonly Storage _storage;
+ private int _index;
+
+ public Enumerator(RouteValueDictionary dictionary)
+ {
+ if (dictionary == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ _storage = dictionary._storage;
+
+ Current = default(KeyValuePair<string, object>);
+ _index = -1;
+ }
+
+ public KeyValuePair<string, object> Current { get; private set; }
+
+ object IEnumerator.Current => Current;
+
+ public void Dispose()
+ {
+ }
+
+ public bool MoveNext()
+ {
+ if (++_index < _storage.Count)
+ {
+ Current = _storage[_index];
+ return true;
+ }
+
+ Current = default(KeyValuePair<string, object>);
+ return false;
+ }
+
+ public void Reset()
+ {
+ Current = default(KeyValuePair<string, object>);
+ _index = -1;
+ }
+ }
+
+ // Storage and its subclasses are internal for testing.
+ internal abstract class Storage
+ {
+ public abstract int Count { get; }
+
+ public abstract KeyValuePair<string, object> this[int index] { get; set; }
+
+ public abstract void Upgrade(ref Storage storage);
+
+ public abstract bool TryGetValue(string key, out object value);
+
+ public abstract bool ContainsKey(string key);
+
+ public abstract bool TrySetValue(string key, object value);
+ }
+
+ internal class ListStorage : Storage
+ {
+ private KeyValuePair<string, object>[] _items;
+ private int _count;
+
+ private static readonly KeyValuePair<string, object>[] _emptyArray = new KeyValuePair<string, object>[0];
+
+ public ListStorage()
+ {
+ _items = _emptyArray;
+ }
+
+ public ListStorage(int capacity)
+ {
+ if (capacity == 0)
+ {
+ _items = _emptyArray;
+ }
+ else
+ {
+ _items = new KeyValuePair<string, object>[capacity];
+ }
+ }
+
+ public ListStorage(ListStorage other)
+ {
+ if (other.Count == 0)
+ {
+ _items = _emptyArray;
+ }
+ else
+ {
+ _items = new KeyValuePair<string, object>[other.Count];
+ for (var i = 0; i < other.Count; i++)
+ {
+ this.Add(other[i]);
+ }
+ }
+ }
+
+ public int Capacity => _items.Length;
+
+ public override int Count => _count;
+
+ public override KeyValuePair<string, object> this[int index]
+ {
+ get
+ {
+ if (index < 0 || index >= _count)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ return _items[index];
+ }
+ set
+ {
+ if (index < 0 || index >= _count)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ _items[index] = value;
+ }
+ }
+
+ public void Add(KeyValuePair<string, object> item)
+ {
+ if (_count == _items.Length)
+ {
+ EnsureCapacity(_count + 1);
+ }
+
+ _items[_count++] = item;
+ }
+
+ public void RemoveAt(int index)
+ {
+ _count--;
+
+ for (var i = index; i < _count; i++)
+ {
+ _items[i] = _items[i + 1];
+ }
+
+ _items[_count] = default(KeyValuePair<string, object>);
+ }
+
+ public void Clear()
+ {
+ for (var i = 0; i < _count; i++)
+ {
+ _items[i] = default(KeyValuePair<string, object>);
+ }
+
+ _count = 0;
+ }
+
+ public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
+ {
+ for (var i = 0; i < _count; i++)
+ {
+ array[arrayIndex++] = _items[i];
+ }
+ }
+
+ public override bool ContainsKey(string key)
+ {
+ for (var i = 0; i < Count; i++)
+ {
+ var kvp = _items[i];
+ if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public override bool TrySetValue(string key, object value)
+ {
+ for (var i = 0; i < Count; i++)
+ {
+ var kvp = _items[i];
+ if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase))
+ {
+ _items[i] = new KeyValuePair<string, object>(key, value);
+ return true;
+ }
+ }
+
+ Add(new KeyValuePair<string, object>(key, value));
+ return true;
+ }
+
+ public override bool TryGetValue(string key, out object value)
+ {
+ for (var i = 0; i < Count; i++)
+ {
+ var kvp = _items[i];
+ if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase))
+ {
+ value = kvp.Value;
+ return true;
+ }
+ }
+
+ value = null;
+ return false;
+ }
+
+ public override void Upgrade(ref Storage storage)
+ {
+ // Do nothing.
+ }
+
+ private void EnsureCapacity(int min)
+ {
+ var newLength = _items.Length == 0 ? 4 : _items.Length * 2;
+ var newItems = new KeyValuePair<string, object>[newLength];
+ for (var i = 0; i < _count; i++)
+ {
+ newItems[i] = _items[i];
+ }
+
+ _items = newItems;
+ }
+ }
+
+ internal class PropertyStorage : Storage
+ {
+ private static readonly PropertyCache _propertyCache = new PropertyCache();
+
+ internal readonly object _value;
+ internal readonly PropertyHelper[] _properties;
+
+ public PropertyStorage(object value)
+ {
+ Debug.Assert(value != null);
+ _value = value;
+
+ // Cache the properties so we can know if we've already validated them for duplicates.
+ var type = _value.GetType();
+ if (!_propertyCache.TryGetValue(type, out _properties))
+ {
+ _properties = PropertyHelper.GetVisibleProperties(type);
+ ValidatePropertyNames(type, _properties);
+ _propertyCache.TryAdd(type, _properties);
+ }
+ }
+
+ public PropertyStorage(PropertyStorage propertyStorage)
+ {
+ _value = propertyStorage._value;
+ _properties = propertyStorage._properties;
+ }
+
+ public override int Count => _properties.Length;
+
+ public override KeyValuePair<string, object> this[int index]
+ {
+ get
+ {
+ var property = _properties[index];
+ return new KeyValuePair<string, object>(property.Name, property.GetValue(_value));
+ }
+ set
+ {
+ // PropertyStorage never sets a value.
+ throw new NotImplementedException();
+ }
+ }
+
+ public override bool TryGetValue(string key, out object value)
+ {
+ for (var i = 0; i < _properties.Length; i++)
+ {
+ var property = _properties[i];
+ if (string.Equals(key, property.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ value = property.GetValue(_value);
+ return true;
+ }
+ }
+
+ value = null;
+ return false;
+ }
+
+ public override bool ContainsKey(string key)
+ {
+ for (var i = 0; i < _properties.Length; i++)
+ {
+ var property = _properties[i];
+ if (string.Equals(key, property.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public override bool TrySetValue(string key, object value)
+ {
+ // PropertyStorage never sets a value.
+ return false;
+ }
+
+ public override void Upgrade(ref Storage storage)
+ {
+ storage = new ListStorage(Count);
+ for (var i = 0; i < _properties.Length; i++)
+ {
+ var property = _properties[i];
+ storage.TrySetValue(property.Name, property.GetValue(_value));
+ }
+ }
+
+ private static void ValidatePropertyNames(Type type, PropertyHelper[] properties)
+ {
+ var names = new Dictionary<string, PropertyHelper>(StringComparer.OrdinalIgnoreCase);
+ for (var i = 0; i < properties.Length; i++)
+ {
+ var property = properties[i];
+
+ PropertyHelper duplicate;
+ if (names.TryGetValue(property.Name, out duplicate))
+ {
+ var message = Resources.FormatRouteValueDictionary_DuplicatePropertyName(
+ type.FullName,
+ property.Name,
+ duplicate.Name,
+ nameof(RouteValueDictionary));
+ throw new InvalidOperationException(message);
+ }
+
+ names.Add(property.Name, property);
+ }
+ }
+ }
+
+ internal class EmptyStorage : Storage
+ {
+ public static readonly EmptyStorage Instance = new EmptyStorage();
+
+ private EmptyStorage()
+ {
+ }
+
+ public override int Count => 0;
+
+ public override KeyValuePair<string, object> this[int index]
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ set
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override bool ContainsKey(string key)
+ {
+ return false;
+ }
+
+ public override bool TryGetValue(string key, out object value)
+ {
+ value = null;
+ return false;
+ }
+
+ public override bool TrySetValue(string key, object value)
+ {
+ return false;
+ }
+
+ public override void Upgrade(ref Storage storage)
+ {
+ storage = new ListStorage();
+ }
+ }
+
+ private class PropertyCache : ConcurrentDictionary<Type, PropertyHelper[]>
+ {
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RoutingHttpContextExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RoutingHttpContextExtensions.cs
new file mode 100644
index 0000000000..de121852ab
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/RoutingHttpContextExtensions.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.
+
+using System;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ /// <summary>
+ /// Extension methods for <see cref="HttpContext"/> related to routing.
+ /// </summary>
+ public static class RoutingHttpContextExtensions
+ {
+ /// <summary>
+ /// Gets the <see cref="RouteData"/> associated with the provided <paramref name="httpContext"/>.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
+ /// <returns>The <see cref="RouteData"/>, or null.</returns>
+ public static RouteData GetRouteData(this HttpContext httpContext)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ var routingFeature = httpContext.Features[typeof(IRoutingFeature)] as IRoutingFeature;
+ return routingFeature?.RouteData;
+ }
+
+ /// <summary>
+ /// Gets a route value from <see cref="RouteData.Values"/> associated with the provided
+ /// <paramref name="httpContext"/>.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
+ /// <param name="key">The key of the route value.</param>
+ /// <returns>The corresponding route value, or null.</returns>
+ public static object GetRouteValue(this HttpContext httpContext, string key)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ var routingFeature = httpContext.Features[typeof(IRoutingFeature)] as IRoutingFeature;
+ return routingFeature?.RouteData.Values[key];
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathContext.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathContext.cs
new file mode 100644
index 0000000000..036aa445f8
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathContext.cs
@@ -0,0 +1,66 @@
+// 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.Routing
+{
+ /// <summary>
+ /// A context for virtual path generation operations.
+ /// </summary>
+ public class VirtualPathContext
+ {
+ /// <summary>
+ /// Creates a new <see cref="VirtualPathContext"/>.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="Http.HttpContext"/> associated with the current request.</param>
+ /// <param name="ambientValues">The set of route values associated with the current request.</param>
+ /// <param name="values">The set of new values provided for virtual path generation.</param>
+ public VirtualPathContext(
+ HttpContext httpContext,
+ RouteValueDictionary ambientValues,
+ RouteValueDictionary values)
+ : this(httpContext, ambientValues, values, null)
+ {
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="VirtualPathContext"/>.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="Http.HttpContext"/> associated with the current request.</param>
+ /// <param name="ambientValues">The set of route values associated with the current request.</param>
+ /// <param name="values">The set of new values provided for virtual path generation.</param>
+ /// <param name="routeName">The name of the route to use for virtual path generation.</param>
+ public VirtualPathContext(
+ HttpContext httpContext,
+ RouteValueDictionary ambientValues,
+ RouteValueDictionary values,
+ string routeName)
+ {
+ HttpContext = httpContext;
+ AmbientValues = ambientValues;
+ Values = values;
+ RouteName = routeName;
+ }
+
+ /// <summary>
+ /// Gets the set of route values associated with the current request.
+ /// </summary>
+ public RouteValueDictionary AmbientValues { get; }
+
+ /// <summary>
+ /// Gets the <see cref="Http.HttpContext"/> associated with the current request.
+ /// </summary>
+ public HttpContext HttpContext { get; }
+
+ /// <summary>
+ /// Gets the name of the route to use for virtual path generation.
+ /// </summary>
+ public string RouteName { get; }
+
+ /// <summary>
+ /// Gets or sets the set of new values provided for virtual path generation.
+ /// </summary>
+ public RouteValueDictionary Values { get; set; }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathData.cs b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathData.cs
new file mode 100644
index 0000000000..d6a2db1232
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathData.cs
@@ -0,0 +1,99 @@
+// 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.Routing
+{
+ /// <summary>
+ /// Represents information about the route and virtual path that are the result of
+ /// generating a URL with the ASP.NET routing middleware.
+ /// </summary>
+ public class VirtualPathData
+ {
+ private RouteValueDictionary _dataTokens;
+ private string _virtualPath;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VirtualPathData"/> class.
+ /// </summary>
+ /// <param name="router">The object that is used to generate the URL.</param>
+ /// <param name="virtualPath">The generated URL.</param>
+ public VirtualPathData(IRouter router, string virtualPath)
+ : this(router, virtualPath, dataTokens: null)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VirtualPathData"/> class.
+ /// </summary>
+ /// <param name="router">The object that is used to generate the URL.</param>
+ /// <param name="virtualPath">The generated URL.</param>
+ /// <param name="dataTokens">The collection of custom values.</param>
+ public VirtualPathData(
+ IRouter router,
+ string virtualPath,
+ RouteValueDictionary dataTokens)
+ {
+ if (router == null)
+ {
+ throw new ArgumentNullException(nameof(router));
+ }
+
+ Router = router;
+ VirtualPath = virtualPath;
+ _dataTokens = dataTokens == null ? null : new RouteValueDictionary(dataTokens);
+ }
+
+ /// <summary>
+ /// Gets the collection of custom values for the <see cref="Router"/>.
+ /// </summary>
+ public RouteValueDictionary DataTokens
+ {
+ get
+ {
+ if (_dataTokens == null)
+ {
+ _dataTokens = new RouteValueDictionary();
+ }
+
+ return _dataTokens;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the <see cref="IRouter"/> that was used to generate the URL.
+ /// </summary>
+ public IRouter Router { get; set; }
+
+ /// <summary>
+ /// Gets or sets the URL that was generated from the <see cref="Router"/>.
+ /// </summary>
+ public string VirtualPath
+ {
+ get
+ {
+ return _virtualPath;
+ }
+ set
+ {
+ _virtualPath = NormalizePath(value);
+ }
+ }
+
+ private static string NormalizePath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ return string.Empty;
+ }
+
+ if (!path.StartsWith("/", StringComparison.Ordinal))
+ {
+ return "/" + path;
+ }
+
+ return path;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/baseline.netcore.json b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/baseline.netcore.json
new file mode 100644
index 0000000000..8f8a0cc67d
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing.Abstractions/baseline.netcore.json
@@ -0,0 +1,849 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Routing.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.IRouteHandler",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetRequestHandler",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "routeData",
+ "Type": "Microsoft.AspNetCore.Routing.RouteData"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Http.RequestDelegate",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.IRouter",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "RouteAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.RouteContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetVirtualPath",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.IRoutingFeature",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_RouteData",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteData",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RouteData",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.RouteData"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Handler",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.RequestDelegate",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Handler",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_HttpContext",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.HttpContext",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RouteData",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteData",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RouteData",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.RouteData"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteData",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_DataTokens",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Routers",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<Microsoft.AspNetCore.Routing.IRouter>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Values",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "PushState",
+ "Parameters": [
+ {
+ "Name": "router",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "dataTokens",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteData+RouteDataSnapshot",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "other",
+ "Type": "Microsoft.AspNetCore.Routing.RouteData"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteDirection",
+ "Visibility": "Public",
+ "Kind": "Enumeration",
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "IncomingRequest",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "0"
+ },
+ {
+ "Kind": "Field",
+ "Name": "UrlGeneration",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "1"
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "System.Collections.Generic.IDictionary<System.String, System.Object>",
+ "System.Collections.Generic.IReadOnlyDictionary<System.String, System.Object>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Count",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<System.String, System.Object>>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Clear",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<System.String, System.Object>>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Item",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Object",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, System.Object>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Item",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, System.Object>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Comparer",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IEqualityComparer<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Keys",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.ICollection<System.String>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, System.Object>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Values",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.ICollection<System.Object>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, System.Object>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Add",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, System.Object>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ContainsKey",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, System.Object>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetEnumerator",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary+Enumerator",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Remove",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, System.Object>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "TryGetValue",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.Object",
+ "Direction": "Out"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.IDictionary<System.String, System.Object>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "values",
+ "Type": "System.Object"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RoutingHttpContextExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetRouteData",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteData",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetRouteValue",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "key",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Object",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.VirtualPathContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_AmbientValues",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_HttpContext",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.HttpContext",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RouteName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Values",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Values",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "ambientValues",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "ambientValues",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeName",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.VirtualPathData",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_DataTokens",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Router",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Router",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_VirtualPath",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_VirtualPath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "router",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "virtualPath",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "router",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "virtualPath",
+ "Type": "System.String"
+ },
+ {
+ "Name": "dataTokens",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteData+RouteDataSnapshot",
+ "Visibility": "Public",
+ "Kind": "Struct",
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Restore",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "routeData",
+ "Type": "Microsoft.AspNetCore.Routing.RouteData"
+ },
+ {
+ "Name": "dataTokens",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routers",
+ "Type": "System.Collections.Generic.IList<Microsoft.AspNetCore.Routing.IRouter>"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteValueDictionary+Enumerator",
+ "Visibility": "Public",
+ "Kind": "Struct",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<System.String, System.Object>>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Dispose",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.IDisposable",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MoveNext",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.IEnumerator",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Reset",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.IEnumerator",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Current",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.KeyValuePair<System.String, System.Object>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<System.String, System.Object>>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "dictionary",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/AlphaRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/AlphaRouteConstraint.cs
new file mode 100644
index 0000000000..7f2748e5c3
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/AlphaRouteConstraint.cs
@@ -0,0 +1,18 @@
+// 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.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to contain only lowercase or uppercase letters A through Z in the English alphabet.
+ /// </summary>
+ public class AlphaRouteConstraint : RegexRouteConstraint
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AlphaRouteConstraint" /> class.
+ /// </summary>
+ public AlphaRouteConstraint() : base(@"^[a-z]*$")
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/BoolRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/BoolRouteConstraint.cs
new file mode 100644
index 0000000000..a65e88ef67
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/BoolRouteConstraint.cs
@@ -0,0 +1,59 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to represent only Boolean values.
+ /// </summary>
+ public class BoolRouteConstraint : IRouteConstraint
+ {
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ if (value is bool)
+ {
+ return true;
+ }
+
+ bool result;
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ return bool.TryParse(valueString, out result);
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/CompositeRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/CompositeRouteConstraint.cs
new file mode 100644
index 0000000000..5acff08294
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/CompositeRouteConstraint.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 System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route by several child constraints.
+ /// </summary>
+ public class CompositeRouteConstraint : IRouteConstraint
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CompositeRouteConstraint" /> class.
+ /// </summary>
+ /// <param name="constraints">The child constraints that must match for this constraint to match.</param>
+ public CompositeRouteConstraint(IEnumerable<IRouteConstraint> constraints)
+ {
+ if (constraints == null)
+ {
+ throw new ArgumentNullException(nameof(constraints));
+ }
+
+ Constraints = constraints;
+ }
+
+ /// <summary>
+ /// Gets the child constraints that must match for this constraint to match.
+ /// </summary>
+ public IEnumerable<IRouteConstraint> Constraints { get; private set; }
+
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ foreach (var constraint in Constraints)
+ {
+ if (!constraint.Match(httpContext, route, routeKey, values, routeDirection))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DateTimeRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DateTimeRouteConstraint.cs
new file mode 100644
index 0000000000..9015dc64be
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DateTimeRouteConstraint.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;
+using System.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to represent only <see cref="DateTime"/> values.
+ /// </summary>
+ /// <remarks>
+ /// This constraint tries to parse strings by using all of the formats returned by the
+ /// CultureInfo.InvariantCulture.DateTimeFormat.GetAllDateTimePatterns() method.
+ /// For a sample on how to list all formats which are considered, please visit
+ /// http://msdn.microsoft.com/en-us/library/aszyst2c(v=vs.110).aspx
+ /// </remarks>
+ public class DateTimeRouteConstraint : IRouteConstraint
+ {
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ if (value is DateTime)
+ {
+ return true;
+ }
+
+ DateTime result;
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ return DateTime.TryParse(valueString, CultureInfo.InvariantCulture, DateTimeStyles.None, out result);
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DecimalRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DecimalRouteConstraint.cs
new file mode 100644
index 0000000000..ff29d98574
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DecimalRouteConstraint.cs
@@ -0,0 +1,59 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to represent only decimal values.
+ /// </summary>
+ public class DecimalRouteConstraint : IRouteConstraint
+ {
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ if (value is decimal)
+ {
+ return true;
+ }
+
+ decimal result;
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ return decimal.TryParse(valueString, NumberStyles.Number, CultureInfo.InvariantCulture, out result);
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DoubleRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DoubleRouteConstraint.cs
new file mode 100644
index 0000000000..e7259d0bf3
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/DoubleRouteConstraint.cs
@@ -0,0 +1,63 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to represent only 64-bit floating-point values.
+ /// </summary>
+ public class DoubleRouteConstraint : IRouteConstraint
+ {
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ if (value is double)
+ {
+ return true;
+ }
+
+ double result;
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ return double.TryParse(
+ valueString,
+ NumberStyles.Float | NumberStyles.AllowThousands,
+ CultureInfo.InvariantCulture,
+ out result);
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/FloatRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/FloatRouteConstraint.cs
new file mode 100644
index 0000000000..5db0e65c28
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/FloatRouteConstraint.cs
@@ -0,0 +1,63 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to represent only 32-bit floating-point values.
+ /// </summary>
+ public class FloatRouteConstraint : IRouteConstraint
+ {
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ if (value is float)
+ {
+ return true;
+ }
+
+ float result;
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ return float.TryParse(
+ valueString,
+ NumberStyles.Float | NumberStyles.AllowThousands,
+ CultureInfo.InvariantCulture,
+ out result);
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/GuidRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/GuidRouteConstraint.cs
new file mode 100644
index 0000000000..00d451767f
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/GuidRouteConstraint.cs
@@ -0,0 +1,61 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to represent only <see cref="Guid"/> values.
+ /// Matches values specified in any of the five formats "N", "D", "B", "P", or "X",
+ /// supported by Guid.ToString(string) and Guid.ToString(String, IFormatProvider) methods.
+ /// </summary>
+ public class GuidRouteConstraint : IRouteConstraint
+ {
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ if (value is Guid)
+ {
+ return true;
+ }
+
+ Guid result;
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ return Guid.TryParse(valueString, out result);
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/HttpMethodRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/HttpMethodRouteConstraint.cs
new file mode 100644
index 0000000000..d01140157d
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/HttpMethodRouteConstraint.cs
@@ -0,0 +1,96 @@
+// 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.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains the HTTP method of request or a route.
+ /// </summary>
+ public class HttpMethodRouteConstraint : IRouteConstraint
+ {
+ /// <summary>
+ /// Creates a new <see cref="HttpMethodRouteConstraint"/> that accepts the HTTP methods specified
+ /// by <paramref name="allowedMethods"/>.
+ /// </summary>
+ /// <param name="allowedMethods">The allowed HTTP methods.</param>
+ public HttpMethodRouteConstraint(params string[] allowedMethods)
+ {
+ if (allowedMethods == null)
+ {
+ throw new ArgumentNullException(nameof(allowedMethods));
+ }
+
+ AllowedMethods = new List<string>(allowedMethods);
+ }
+
+ /// <summary>
+ /// Gets the HTTP methods allowed by the constraint.
+ /// </summary>
+ public IList<string> AllowedMethods { get; }
+
+ /// <inheritdoc />
+ public virtual bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ switch (routeDirection)
+ {
+ case RouteDirection.IncomingRequest:
+ return AllowedMethods.Contains(httpContext.Request.Method, StringComparer.OrdinalIgnoreCase);
+
+ case RouteDirection.UrlGeneration:
+ // We need to see if the user specified the HTTP method explicitly. Consider these two routes:
+ //
+ // a) Route: template = "/{foo}", Constraints = { httpMethod = new HttpMethodRouteConstraint("GET") }
+ // b) Route: template = "/{foo}", Constraints = { httpMethod = new HttpMethodRouteConstraint("POST") }
+ //
+ // A user might know ahead of time that a URI he/she is generating might be used with a particular HTTP
+ // method. If a URI will be used for an HTTP POST but we match on (a) while generating the URI, then
+ // the HTTP GET-specific route will be used for URI generation, which might have undesired behavior.
+ //
+ // To prevent this, a user might call GetVirtualPath(..., { httpMethod = "POST" }) to
+ // signal that he is generating a URI that will be used for an HTTP POST, so he wants the URI
+ // generation to be performed by the (b) route instead of the (a) route, consistent with what would
+ // happen on incoming requests.
+ object obj;
+ if (!values.TryGetValue(routeKey, out obj))
+ {
+ return true;
+ }
+
+ return AllowedMethods.Contains(Convert.ToString(obj), StringComparer.OrdinalIgnoreCase);
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(routeDirection));
+ }
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/IntRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/IntRouteConstraint.cs
new file mode 100644
index 0000000000..83b08533bd
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/IntRouteConstraint.cs
@@ -0,0 +1,59 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to represent only 32-bit integer values.
+ /// </summary>
+ public class IntRouteConstraint : IRouteConstraint
+ {
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ if (value is int)
+ {
+ return true;
+ }
+
+ int result;
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ return int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out result);
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LengthRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LengthRouteConstraint.cs
new file mode 100644
index 0000000000..f876c03cbc
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LengthRouteConstraint.cs
@@ -0,0 +1,111 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to be a string of a given length or within a given range of lengths.
+ /// </summary>
+ public class LengthRouteConstraint : IRouteConstraint
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LengthRouteConstraint" /> class that constrains
+ /// a route parameter to be a string of a given length.
+ /// </summary>
+ /// <param name="length">The length of the route parameter.</param>
+ public LengthRouteConstraint(int length)
+ {
+ if (length < 0)
+ {
+ var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0);
+ throw new ArgumentOutOfRangeException(nameof(length), length, errorMessage);
+ }
+
+ MinLength = MaxLength = length;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LengthRouteConstraint" /> class that constrains
+ /// a route parameter to be a string of a given length.
+ /// </summary>
+ /// <param name="minLength">The minimum length allowed for the route parameter.</param>
+ /// <param name="maxLength">The maximum length allowed for the route parameter.</param>
+ public LengthRouteConstraint(int minLength, int maxLength)
+ {
+ if (minLength < 0)
+ {
+ var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0);
+ throw new ArgumentOutOfRangeException(nameof(minLength), minLength, errorMessage);
+ }
+
+ if (maxLength < 0)
+ {
+ var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0);
+ throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, errorMessage);
+ }
+
+ if (minLength > maxLength)
+ {
+ var errorMessage =
+ Resources.FormatRangeConstraint_MinShouldBeLessThanOrEqualToMax("minLength", "maxLength");
+ throw new ArgumentOutOfRangeException(nameof(minLength), minLength, errorMessage);
+ }
+
+ MinLength = minLength;
+ MaxLength = maxLength;
+ }
+
+ /// <summary>
+ /// Gets the minimum length allowed for the route parameter.
+ /// </summary>
+ public int MinLength { get; }
+
+ /// <summary>
+ /// Gets the maximum length allowed for the route parameter.
+ /// </summary>
+ public int MaxLength { get; }
+
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ var length = valueString.Length;
+ return length >= MinLength && length <= MaxLength;
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LongRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LongRouteConstraint.cs
new file mode 100644
index 0000000000..a76a4de885
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/LongRouteConstraint.cs
@@ -0,0 +1,59 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to represent only 64-bit integer values.
+ /// </summary>
+ public class LongRouteConstraint : IRouteConstraint
+ {
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ if (value is long)
+ {
+ return true;
+ }
+
+ long result;
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ return long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out result);
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxLengthRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxLengthRouteConstraint.cs
new file mode 100644
index 0000000000..42dde182ed
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxLengthRouteConstraint.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 System;
+using System.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to be a string with a maximum length.
+ /// </summary>
+ public class MaxLengthRouteConstraint : IRouteConstraint
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MaxLengthRouteConstraint" /> class.
+ /// </summary>
+ /// <param name="maxLength">The maximum length allowed for the route parameter.</param>
+ public MaxLengthRouteConstraint(int maxLength)
+ {
+ if (maxLength < 0)
+ {
+ var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0);
+ throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, errorMessage);
+ }
+
+ MaxLength = maxLength;
+ }
+
+ /// <summary>
+ /// Gets the maximum length allowed for the route parameter.
+ /// </summary>
+ public int MaxLength { get; }
+
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ return valueString.Length <= MaxLength;
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxRouteConstraint.cs
new file mode 100644
index 0000000000..e43dac85fe
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MaxRouteConstraint.cs
@@ -0,0 +1,71 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to be an integer with a maximum value.
+ /// </summary>
+ public class MaxRouteConstraint : IRouteConstraint
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MaxRouteConstraint" /> class.
+ /// </summary>
+ /// <param name="max">The maximum value allowed for the route parameter.</param>
+ public MaxRouteConstraint(long max)
+ {
+ Max = max;
+ }
+
+ /// <summary>
+ /// Gets the maximum allowed value of the route parameter.
+ /// </summary>
+ public long Max { get; private set; }
+
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ long longValue;
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue))
+ {
+ return longValue <= Max;
+ }
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinLengthRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinLengthRouteConstraint.cs
new file mode 100644
index 0000000000..1ea64ae216
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinLengthRouteConstraint.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 System;
+using System.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to be a string with a minimum length.
+ /// </summary>
+ public class MinLengthRouteConstraint : IRouteConstraint
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MinLengthRouteConstraint" /> class.
+ /// </summary>
+ /// <param name="minLength">The minimum length allowed for the route parameter.</param>
+ public MinLengthRouteConstraint(int minLength)
+ {
+ if (minLength < 0)
+ {
+ var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0);
+ throw new ArgumentOutOfRangeException(nameof(minLength), minLength, errorMessage);
+ }
+
+ MinLength = minLength;
+ }
+
+ /// <summary>
+ /// Gets the minimum length allowed for the route parameter.
+ /// </summary>
+ public int MinLength { get; private set; }
+
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ return valueString.Length >= MinLength;
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinRouteConstraint.cs
new file mode 100644
index 0000000000..68357c59e7
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/MinRouteConstraint.cs
@@ -0,0 +1,71 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to be a long with a minimum value.
+ /// </summary>
+ public class MinRouteConstraint : IRouteConstraint
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MinRouteConstraint" /> class.
+ /// </summary>
+ /// <param name="min">The minimum value allowed for the route parameter.</param>
+ public MinRouteConstraint(long min)
+ {
+ Min = min;
+ }
+
+ /// <summary>
+ /// Gets the minimum allowed value of the route parameter.
+ /// </summary>
+ public long Min { get; }
+
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ long longValue;
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue))
+ {
+ return longValue >= Min;
+ }
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/OptionalRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/OptionalRouteConstraint.cs
new file mode 100644
index 0000000000..3990376410
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/OptionalRouteConstraint.cs
@@ -0,0 +1,66 @@
+// 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.Routing.Constraints
+{
+ /// <summary>
+ /// Defines a constraint on an optional parameter. If the parameter is present, then it is constrained by InnerConstraint.
+ /// </summary>
+ public class OptionalRouteConstraint : IRouteConstraint
+ {
+ public OptionalRouteConstraint(IRouteConstraint innerConstraint)
+ {
+ if (innerConstraint == null)
+ {
+ throw new ArgumentNullException(nameof(innerConstraint));
+ }
+
+ InnerConstraint = innerConstraint;
+ }
+
+ public IRouteConstraint InnerConstraint { get; }
+
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value))
+ {
+ return InnerConstraint.Match(httpContext,
+ route,
+ routeKey,
+ values,
+ routeDirection);
+ }
+
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RangeRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RangeRouteConstraint.cs
new file mode 100644
index 0000000000..301a75f15a
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RangeRouteConstraint.cs
@@ -0,0 +1,85 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constraints a route parameter to be an integer within a given range of values.
+ /// </summary>
+ public class RangeRouteConstraint : IRouteConstraint
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RangeRouteConstraint" /> class.
+ /// </summary>
+ /// <param name="min">The minimum value.</param>
+ /// <param name="max">The maximum value.</param>
+ /// <remarks>The minimum value should be less than or equal to the maximum value.</remarks>
+ public RangeRouteConstraint(long min, long max)
+ {
+ if (min > max)
+ {
+ var errorMessage = Resources.FormatRangeConstraint_MinShouldBeLessThanOrEqualToMax("min", "max");
+ throw new ArgumentOutOfRangeException(nameof(min), min, errorMessage);
+ }
+
+ Min = min;
+ Max = max;
+ }
+
+ /// <summary>
+ /// Gets the minimum allowed value of the route parameter.
+ /// </summary>
+ public long Min { get; private set; }
+
+ /// <summary>
+ /// Gets the maximum allowed value of the route parameter.
+ /// </summary>
+ public long Max { get; private set; }
+
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ long longValue;
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ if (Int64.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue))
+ {
+ return longValue >= Min && longValue <= Max;
+ }
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexInlineRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexInlineRouteConstraint.cs
new file mode 100644
index 0000000000..f0a9bc9875
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexInlineRouteConstraint.cs
@@ -0,0 +1,20 @@
+// 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.Routing.Constraints
+{
+ /// <summary>
+ /// Represents a regex constraint which can be used as an inlineConstraint.
+ /// </summary>
+ public class RegexInlineRouteConstraint : RegexRouteConstraint
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RegexInlineRouteConstraint" /> class.
+ /// </summary>
+ /// <param name="regexPattern">The regular expression pattern to match.</param>
+ public RegexInlineRouteConstraint(string regexPattern)
+ : base(regexPattern)
+ {
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexRouteConstraint.cs
new file mode 100644
index 0000000000..fb3d2390fe
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RegexRouteConstraint.cs
@@ -0,0 +1,80 @@
+// 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.Globalization;
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ public class RegexRouteConstraint : IRouteConstraint
+ {
+ private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10);
+
+ public RegexRouteConstraint(Regex regex)
+ {
+ if (regex == null)
+ {
+ throw new ArgumentNullException(nameof(regex));
+ }
+
+ Constraint = regex;
+ }
+
+ public RegexRouteConstraint(string regexPattern)
+ {
+ if (regexPattern == null)
+ {
+ throw new ArgumentNullException(nameof(regexPattern));
+ }
+
+ Constraint = new Regex(
+ regexPattern,
+ RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
+ RegexMatchTimeout);
+ }
+
+ public Regex Constraint { get; private set; }
+
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object routeValue;
+
+ if (values.TryGetValue(routeKey, out routeValue)
+ && routeValue != null)
+ {
+ var parameterValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
+
+ return Constraint.IsMatch(parameterValueString);
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RequiredRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RequiredRouteConstraint.cs
new file mode 100644
index 0000000000..e03d618565
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/RequiredRouteConstraint.cs
@@ -0,0 +1,58 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constraints a route parameter that must have a value.
+ /// </summary>
+ /// <remarks>
+ /// This constraint is primarily used to enforce that a non-parameter value is present during
+ /// URL generation.
+ /// </remarks>
+ public class RequiredRouteConstraint : IRouteConstraint
+ {
+ /// <inheritdoc />
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object value;
+ if (values.TryGetValue(routeKey, out value) && value != null)
+ {
+ // In routing the empty string is equivalent to null, which is equivalent to an unset value.
+ var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
+ return !string.IsNullOrEmpty(valueString);
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/StringRouteConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/StringRouteConstraint.cs
new file mode 100644
index 0000000000..e7d92ef13c
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Constraints/StringRouteConstraint.cs
@@ -0,0 +1,67 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ /// <summary>
+ /// Constrains a route parameter to contain only a specified strign.
+ /// </summary>
+ public class StringRouteConstraint : IRouteConstraint
+ {
+ private string _value;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StringRouteConstraint"/> class.
+ /// </summary>
+ /// <param name="value">The constraint value to match.</param>
+ public StringRouteConstraint(string value)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ _value = value;
+ }
+
+ /// <inheritdoc />
+ public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (routeKey == null)
+ {
+ throw new ArgumentNullException(nameof(routeKey));
+ }
+
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ object routeValue;
+
+ if (values.TryGetValue(routeKey, out routeValue)
+ && routeValue != null)
+ {
+ var parameterValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
+
+ return parameterValueString.Equals(_value, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs
new file mode 100644
index 0000000000..7426516db2
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/DefaultInlineConstraintResolver.cs
@@ -0,0 +1,156 @@
+// 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.Globalization;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ /// <summary>
+ /// The default implementation of <see cref="IInlineConstraintResolver"/>. Resolves constraints by parsing
+ /// a constraint key and constraint arguments, using a map to resolve the constraint type, and calling an
+ /// appropriate constructor for the constraint type.
+ /// </summary>
+ public class DefaultInlineConstraintResolver : IInlineConstraintResolver
+ {
+ private readonly IDictionary<string, Type> _inlineConstraintMap;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DefaultInlineConstraintResolver"/> class.
+ /// </summary>
+ /// <param name="routeOptions">
+ /// Accessor for <see cref="RouteOptions"/> containing the constraints of interest.
+ /// </param>
+ public DefaultInlineConstraintResolver(IOptions<RouteOptions> routeOptions)
+ {
+ _inlineConstraintMap = routeOptions.Value.ConstraintMap;
+ }
+
+ /// <inheritdoc />
+ /// <example>
+ /// A typical constraint looks like the following
+ /// "exampleConstraint(arg1, arg2, 12)".
+ /// Here if the type registered for exampleConstraint has a single constructor with one argument,
+ /// The entire string "arg1, arg2, 12" will be treated as a single argument.
+ /// In all other cases arguments are split at comma.
+ /// </example>
+ public virtual IRouteConstraint ResolveConstraint(string inlineConstraint)
+ {
+ if (inlineConstraint == null)
+ {
+ throw new ArgumentNullException(nameof(inlineConstraint));
+ }
+
+ string constraintKey;
+ string argumentString;
+ var indexOfFirstOpenParens = inlineConstraint.IndexOf('(');
+ if (indexOfFirstOpenParens >= 0 && inlineConstraint.EndsWith(")", StringComparison.Ordinal))
+ {
+ constraintKey = inlineConstraint.Substring(0, indexOfFirstOpenParens);
+ argumentString = inlineConstraint.Substring(indexOfFirstOpenParens + 1,
+ inlineConstraint.Length - indexOfFirstOpenParens - 2);
+ }
+ else
+ {
+ constraintKey = inlineConstraint;
+ argumentString = null;
+ }
+
+ Type constraintType;
+ if (!_inlineConstraintMap.TryGetValue(constraintKey, out constraintType))
+ {
+ // Cannot resolve the constraint key
+ return null;
+ }
+
+ if (!typeof(IRouteConstraint).GetTypeInfo().IsAssignableFrom(constraintType.GetTypeInfo()))
+ {
+ throw new RouteCreationException(
+ Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint(
+ constraintType, constraintKey, typeof(IRouteConstraint).Name));
+ }
+
+ try
+ {
+ return CreateConstraint(constraintType, argumentString);
+ }
+ catch (RouteCreationException)
+ {
+ throw;
+ }
+ catch (Exception exception)
+ {
+ throw new RouteCreationException(
+ $"An error occurred while trying to create an instance of route constraint '{constraintType.FullName}'.",
+ exception);
+ }
+ }
+
+ private static IRouteConstraint CreateConstraint(Type constraintType, string argumentString)
+ {
+ // No arguments - call the default constructor
+ if (argumentString == null)
+ {
+ return (IRouteConstraint)Activator.CreateInstance(constraintType);
+ }
+
+ var constraintTypeInfo = constraintType.GetTypeInfo();
+ ConstructorInfo activationConstructor = null;
+ object[] parameters = null;
+ var constructors = constraintTypeInfo.DeclaredConstructors.ToArray();
+
+ // If there is only one constructor and it has a single parameter, pass the argument string directly
+ // This is necessary for the Regex RouteConstraint to ensure that patterns are not split on commas.
+ if (constructors.Length == 1 && constructors[0].GetParameters().Length == 1)
+ {
+ activationConstructor = constructors[0];
+ parameters = ConvertArguments(activationConstructor.GetParameters(), new string[] { argumentString });
+ }
+ else
+ {
+ var arguments = argumentString.Split(',').Select(argument => argument.Trim()).ToArray();
+
+ var matchingConstructors = constructors.Where(ci => ci.GetParameters().Length == arguments.Length)
+ .ToArray();
+ var constructorMatches = matchingConstructors.Length;
+
+ if (constructorMatches == 0)
+ {
+ throw new RouteCreationException(
+ Resources.FormatDefaultInlineConstraintResolver_CouldNotFindCtor(
+ constraintTypeInfo.Name, arguments.Length));
+ }
+ else if (constructorMatches == 1)
+ {
+ activationConstructor = matchingConstructors[0];
+ parameters = ConvertArguments(activationConstructor.GetParameters(), arguments);
+ }
+ else
+ {
+ throw new RouteCreationException(
+ Resources.FormatDefaultInlineConstraintResolver_AmbiguousCtors(
+ constraintTypeInfo.Name, arguments.Length));
+ }
+ }
+
+ return (IRouteConstraint)activationConstructor.Invoke(parameters);
+ }
+
+ private static object[] ConvertArguments(ParameterInfo[] parameterInfos, string[] arguments)
+ {
+ var parameters = new object[parameterInfos.Length];
+ for (var i = 0; i < parameterInfos.Length; i++)
+ {
+ var parameter = parameterInfos[i];
+ var parameterType = parameter.ParameterType;
+ parameters[i] = Convert.ChangeType(arguments[i], parameterType, CultureInfo.InvariantCulture);
+ }
+
+ return parameters;
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..d6883f6c95
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs
@@ -0,0 +1,79 @@
+// 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.Routing;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.AspNetCore.Routing.Tree;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ /// <summary>
+ /// Contains extension methods to <see cref="IServiceCollection"/>.
+ /// </summary>
+ public static class RoutingServiceCollectionExtensions
+ {
+ /// <summary>
+ /// Adds services required for routing requests.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
+ /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
+ public static IServiceCollection AddRouting(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.TryAddTransient<IInlineConstraintResolver, DefaultInlineConstraintResolver>();
+ services.TryAddSingleton<ObjectPool<UriBuildingContext>>(s =>
+ {
+ var provider = s.GetRequiredService<ObjectPoolProvider>();
+ return provider.Create<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy());
+ });
+
+ // The TreeRouteBuilder is a builder for creating routes, it should stay transient because it's
+ // stateful.
+ services.TryAdd(ServiceDescriptor.Transient<TreeRouteBuilder>(s =>
+ {
+ var loggerFactory = s.GetRequiredService<ILoggerFactory>();
+ var objectPool = s.GetRequiredService<ObjectPool<UriBuildingContext>>();
+ var constraintResolver = s.GetRequiredService<IInlineConstraintResolver>();
+ return new TreeRouteBuilder(loggerFactory, objectPool, constraintResolver);
+ }));
+
+ services.TryAddSingleton(typeof(RoutingMarkerService));
+
+ return services;
+ }
+
+ /// <summary>
+ /// Adds services required for routing requests.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
+ /// <param name="configureOptions">The routing options to configure the middleware with.</param>
+ /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
+ public static IServiceCollection AddRouting(
+ this IServiceCollection services,
+ Action<RouteOptions> configureOptions)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (configureOptions == null)
+ {
+ throw new ArgumentNullException(nameof(configureOptions));
+ }
+
+ services.Configure(configureOptions);
+ services.AddRouting();
+
+ return services;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/IInlineConstraintResolver.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/IInlineConstraintResolver.cs
new file mode 100644
index 0000000000..d4e0fd028f
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/IInlineConstraintResolver.cs
@@ -0,0 +1,18 @@
+// 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.Routing
+{
+ /// <summary>
+ /// Defines an abstraction for resolving inline constraints as instances of <see cref="IRouteConstraint"/>.
+ /// </summary>
+ public interface IInlineConstraintResolver
+ {
+ /// <summary>
+ /// Resolves the inline constraint.
+ /// </summary>
+ /// <param name="inlineConstraint">The inline constraint to resolve.</param>
+ /// <returns>The <see cref="IRouteConstraint"/> the inline constraint was resolved to.</returns>
+ IRouteConstraint ResolveConstraint(string inlineConstraint);
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/INamedRouter.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/INamedRouter.cs
new file mode 100644
index 0000000000..b04b7d2f56
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/INamedRouter.cs
@@ -0,0 +1,10 @@
+// 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.Routing
+{
+ public interface INamedRouter : IRouter
+ {
+ string Name { get; }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteBuilder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteBuilder.cs
new file mode 100644
index 0000000000..2969342724
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteBuilder.cs
@@ -0,0 +1,42 @@
+// 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 Microsoft.AspNetCore.Builder;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ /// <summary>
+ /// Defines a contract for a route builder in an application. A route builder specifies the routes for
+ /// an application.
+ /// </summary>
+ public interface IRouteBuilder
+ {
+ /// <summary>
+ /// Gets the <see cref="IApplicationBuilder"/>.
+ /// </summary>
+ IApplicationBuilder ApplicationBuilder { get; }
+
+ /// <summary>
+ /// Gets or sets the default <see cref="IRouter"/> that is used as a handler if an <see cref="IRouter"/>
+ /// is added to the list of routes but does not specify its own.
+ /// </summary>
+ IRouter DefaultHandler { get; set; }
+
+ /// <summary>
+ /// Gets the sets the <see cref="IServiceProvider"/> used to resolve services for routes.
+ /// </summary>
+ IServiceProvider ServiceProvider { get; }
+
+ /// <summary>
+ /// Gets the routes configured in the builder.
+ /// </summary>
+ IList<IRouter> Routes { get; }
+
+ /// <summary>
+ /// Builds an <see cref="IRouter"/> that routes the routes specified in the <see cref="Routes"/> property.
+ /// </summary>
+ IRouter Build();
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteCollection.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteCollection.cs
new file mode 100644
index 0000000000..084f0aef67
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/IRouteCollection.cs
@@ -0,0 +1,10 @@
+// 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.Routing
+{
+ public interface IRouteCollection : IRouter
+ {
+ void Add(IRouter router);
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs
new file mode 100644
index 0000000000..78131eba97
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/InlineRouteParameterParser.cs
@@ -0,0 +1,243 @@
+// 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 Microsoft.AspNetCore.Routing.Template;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public static class InlineRouteParameterParser
+ {
+ public static TemplatePart ParseRouteParameter(string routeParameter)
+ {
+ if (routeParameter == null)
+ {
+ throw new ArgumentNullException(nameof(routeParameter));
+ }
+
+ if (routeParameter.Length == 0)
+ {
+ return TemplatePart.CreateParameter(
+ name: string.Empty,
+ isCatchAll: false,
+ isOptional: false,
+ defaultValue: null,
+ inlineConstraints: null);
+ }
+
+ var startIndex = 0;
+ var endIndex = routeParameter.Length - 1;
+
+ var isCatchAll = false;
+ var isOptional = false;
+
+ if (routeParameter[0] == '*')
+ {
+ isCatchAll = true;
+ startIndex++;
+ }
+
+ if (routeParameter[endIndex] == '?')
+ {
+ isOptional = true;
+ endIndex--;
+ }
+
+ var currentIndex = startIndex;
+
+ // Parse parameter name
+ var parameterName = string.Empty;
+
+ while (currentIndex <= endIndex)
+ {
+ var currentChar = routeParameter[currentIndex];
+
+ if ((currentChar == ':' || currentChar == '=') && startIndex != currentIndex)
+ {
+ // Parameter names are allowed to start with delimiters used to denote constraints or default values.
+ // i.e. "=foo" or ":bar" would be treated as parameter names rather than default value or constraint
+ // specifications.
+ parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex);
+
+ // Roll the index back and move to the constraint parsing stage.
+ currentIndex--;
+ break;
+ }
+ else if (currentIndex == endIndex)
+ {
+ parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex + 1);
+ }
+
+ currentIndex++;
+ }
+
+ var parseResults = ParseConstraints(routeParameter, currentIndex, endIndex);
+ currentIndex = parseResults.CurrentIndex;
+
+ string defaultValue = null;
+ if (currentIndex <= endIndex &&
+ routeParameter[currentIndex] == '=')
+ {
+ defaultValue = routeParameter.Substring(currentIndex + 1, endIndex - currentIndex);
+ }
+
+ return TemplatePart.CreateParameter(parameterName,
+ isCatchAll,
+ isOptional,
+ defaultValue,
+ parseResults.Constraints);
+ }
+
+ private static ConstraintParseResults ParseConstraints(
+ string routeParameter,
+ int currentIndex,
+ int endIndex)
+ {
+ var inlineConstraints = new List<InlineConstraint>();
+ var state = ParseState.Start;
+ var startIndex = currentIndex;
+ do
+ {
+ var currentChar = currentIndex > endIndex ? null : (char?)routeParameter[currentIndex];
+ switch (state)
+ {
+ case ParseState.Start:
+ switch (currentChar)
+ {
+ case null:
+ state = ParseState.End;
+ break;
+ case ':':
+ state = ParseState.ParsingName;
+ startIndex = currentIndex + 1;
+ break;
+ case '(':
+ state = ParseState.InsideParenthesis;
+ break;
+ case '=':
+ state = ParseState.End;
+ currentIndex--;
+ break;
+ }
+ break;
+ case ParseState.InsideParenthesis:
+ switch (currentChar)
+ {
+ case null:
+ state = ParseState.End;
+ var constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex);
+ inlineConstraints.Add(new InlineConstraint(constraintText));
+ break;
+ case ')':
+ // Only consume a ')' token if
+ // (a) it is the last token
+ // (b) the next character is the start of the new constraint ':'
+ // (c) the next character is the start of the default value.
+
+ var nextChar = currentIndex + 1 > endIndex ? null : (char?)routeParameter[currentIndex + 1];
+ switch (nextChar)
+ {
+ case null:
+ state = ParseState.End;
+ constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1);
+ inlineConstraints.Add(new InlineConstraint(constraintText));
+ break;
+ case ':':
+ state = ParseState.Start;
+ constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1);
+ inlineConstraints.Add(new InlineConstraint(constraintText));
+ startIndex = currentIndex + 1;
+ break;
+ case '=':
+ state = ParseState.End;
+ constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1);
+ inlineConstraints.Add(new InlineConstraint(constraintText));
+ break;
+ }
+ break;
+ case ':':
+ case '=':
+ // In the original implementation, the Regex would've backtracked if it encountered an
+ // unbalanced opening bracket followed by (not necessarily immediatiely) a delimiter.
+ // Simply verifying that the parantheses will eventually be closed should suffice to
+ // determine if the terminator needs to be consumed as part of the current constraint
+ // specification.
+ var indexOfClosingParantheses = routeParameter.IndexOf(')', currentIndex + 1);
+ if (indexOfClosingParantheses == -1)
+ {
+ constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex);
+ inlineConstraints.Add(new InlineConstraint(constraintText));
+
+ if (currentChar == ':')
+ {
+ state = ParseState.ParsingName;
+ startIndex = currentIndex + 1;
+ }
+ else
+ {
+ state = ParseState.End;
+ currentIndex--;
+ }
+ }
+ else
+ {
+ currentIndex = indexOfClosingParantheses;
+ }
+
+ break;
+ }
+ break;
+ case ParseState.ParsingName:
+ switch (currentChar)
+ {
+ case null:
+ state = ParseState.End;
+ var constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex);
+ inlineConstraints.Add(new InlineConstraint(constraintText));
+ break;
+ case ':':
+ constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex);
+ inlineConstraints.Add(new InlineConstraint(constraintText));
+ startIndex = currentIndex + 1;
+ break;
+ case '(':
+ state = ParseState.InsideParenthesis;
+ break;
+ case '=':
+ state = ParseState.End;
+ constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex);
+ inlineConstraints.Add(new InlineConstraint(constraintText));
+ currentIndex--;
+ break;
+ }
+ break;
+ }
+
+ currentIndex++;
+
+ } while (state != ParseState.End);
+
+ return new ConstraintParseResults
+ {
+ CurrentIndex = currentIndex,
+ Constraints = inlineConstraints
+ };
+ }
+
+ private enum ParseState
+ {
+ Start,
+ ParsingName,
+ InsideParenthesis,
+ End
+ }
+
+ private struct ConstraintParseResults
+ {
+ public int CurrentIndex;
+
+ public IEnumerable<InlineConstraint> Constraints;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/BufferValue.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/BufferValue.cs
new file mode 100644
index 0000000000..578c396b4d
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/BufferValue.cs
@@ -0,0 +1,18 @@
+// 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.Routing.Internal
+{
+ public struct BufferValue
+ {
+ public BufferValue(string value, bool requiresEncoding)
+ {
+ Value = value;
+ RequiresEncoding = requiresEncoding;
+ }
+
+ public bool RequiresEncoding { get; }
+
+ public string Value { get; }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs
new file mode 100644
index 0000000000..f71e6dd74d
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs
@@ -0,0 +1,163 @@
+// 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 Microsoft.AspNetCore.Routing.DecisionTree;
+using Microsoft.AspNetCore.Routing.Tree;
+
+namespace Microsoft.AspNetCore.Routing.Internal
+{
+ // A decision tree that matches link generation entries based on route data.
+ public class LinkGenerationDecisionTree
+ {
+ private readonly DecisionTreeNode<OutboundMatch> _root;
+
+ public LinkGenerationDecisionTree(IReadOnlyList<OutboundMatch> entries)
+ {
+ _root = DecisionTreeBuilder<OutboundMatch>.GenerateTree(
+ entries,
+ new OutboundMatchClassifier());
+ }
+
+ public IList<OutboundMatchResult> GetMatches(VirtualPathContext context)
+ {
+ // Perf: Avoid allocation for List if there aren't any Matches or Criteria
+ if (_root.Matches.Count > 0 || _root.Criteria.Count > 0)
+ {
+ var results = new List<OutboundMatchResult>();
+ Walk(results, context, _root, isFallbackPath: false);
+ results.Sort(OutboundMatchResultComparer.Instance);
+ return results;
+ }
+
+ return null;
+ }
+
+ // We need to recursively walk the decision tree based on the provided route data
+ // (context.Values + context.AmbientValues) to find all entries that match. This process is
+ // virtually identical to action selection.
+ //
+ // Each entry has a collection of 'required link values' that must be satisfied. These are
+ // key-value pairs that make up the decision tree.
+ //
+ // A 'require link value' is considered satisfied IF:
+ // 1. The value in context.Values matches the required value OR
+ // 2. There is no value in context.Values and the value in context.AmbientValues matches OR
+ // 3. The required value is 'null' and there is no value in context.Values.
+ //
+ // Ex:
+ // entry requires { area = null, controller = Store, action = Buy }
+ // context.Values = { controller = Store, action = Buy }
+ // context.AmbientValues = { area = Help, controller = AboutStore, action = HowToBuyThings }
+ //
+ // In this case the entry is a match. The 'controller' and 'action' are both supplied by context.Values,
+ // and the 'area' is satisfied because there's NOT a value in context.Values. It's OK to ignore ambient
+ // values in link generation.
+ //
+ // If another entry existed like { area = Help, controller = Store, action = Buy }, this would also
+ // match.
+ //
+ // The decision tree uses a tree data structure to execute these rules across all candidates at once.
+ private void Walk(
+ List<OutboundMatchResult> results,
+ VirtualPathContext context,
+ DecisionTreeNode<OutboundMatch> node,
+ bool isFallbackPath)
+ {
+ // Any entries in node.Matches have had all their required values satisfied, so add them
+ // to the results.
+ for (var i = 0; i < node.Matches.Count; i++)
+ {
+ results.Add(new OutboundMatchResult(node.Matches[i], isFallbackPath));
+ }
+
+ for (var i = 0; i < node.Criteria.Count; i++)
+ {
+ var criterion = node.Criteria[i];
+ var key = criterion.Key;
+
+ object value;
+ if (context.Values.TryGetValue(key, out value))
+ {
+ DecisionTreeNode<OutboundMatch> branch;
+ if (criterion.Branches.TryGetValue(value ?? string.Empty, out branch))
+ {
+ Walk(results, context, branch, isFallbackPath);
+ }
+ }
+ else
+ {
+ // If a value wasn't explicitly supplied, match BOTH the ambient value and the empty value
+ // if an ambient value was supplied. The path explored with the empty value is considered
+ // the fallback path.
+ DecisionTreeNode<OutboundMatch> branch;
+ if (context.AmbientValues.TryGetValue(key, out value) &&
+ !criterion.Branches.Comparer.Equals(value, string.Empty))
+ {
+ if (criterion.Branches.TryGetValue(value, out branch))
+ {
+ Walk(results, context, branch, isFallbackPath);
+ }
+ }
+
+ if (criterion.Branches.TryGetValue(string.Empty, out branch))
+ {
+ Walk(results, context, branch, isFallbackPath: true);
+ }
+ }
+ }
+ }
+
+ private class OutboundMatchClassifier : IClassifier<OutboundMatch>
+ {
+ public OutboundMatchClassifier()
+ {
+ ValueComparer = new RouteValueEqualityComparer();
+ }
+
+ public IEqualityComparer<object> ValueComparer { get; private set; }
+
+ public IDictionary<string, DecisionCriterionValue> GetCriteria(OutboundMatch item)
+ {
+ var results = new Dictionary<string, DecisionCriterionValue>(StringComparer.OrdinalIgnoreCase);
+ foreach (var kvp in item.Entry.RequiredLinkValues)
+ {
+ results.Add(kvp.Key, new DecisionCriterionValue(kvp.Value ?? string.Empty));
+ }
+
+ return results;
+ }
+ }
+
+ private class OutboundMatchResultComparer : IComparer<OutboundMatchResult>
+ {
+ public static readonly OutboundMatchResultComparer Instance = new OutboundMatchResultComparer();
+
+ public int Compare(OutboundMatchResult x, OutboundMatchResult y)
+ {
+ // For this comparison lower is better.
+ if (x.Match.Entry.Order != y.Match.Entry.Order)
+ {
+ return x.Match.Entry.Order.CompareTo(y.Match.Entry.Order);
+ }
+
+ if (x.Match.Entry.Precedence != y.Match.Entry.Precedence)
+ {
+ // Reversed because higher is better
+ return y.Match.Entry.Precedence.CompareTo(x.Match.Entry.Precedence);
+ }
+
+ if (x.IsFallbackMatch != y.IsFallbackMatch)
+ {
+ // A fallback match is worse than a non-fallback
+ return x.IsFallbackMatch.CompareTo(y.IsFallbackMatch);
+ }
+
+ return StringComparer.Ordinal.Compare(
+ x.Match.Entry.RouteTemplate.TemplateText,
+ y.Match.Entry.RouteTemplate.TemplateText);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/OutboundMatchResult.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/OutboundMatchResult.cs
new file mode 100644
index 0000000000..aee505f572
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/OutboundMatchResult.cs
@@ -0,0 +1,20 @@
+// 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.Routing.Tree;
+
+namespace Microsoft.AspNetCore.Routing.Internal
+{
+ public struct OutboundMatchResult
+ {
+ public OutboundMatchResult(OutboundMatch match, bool isFallbackMatch)
+ {
+ Match = match;
+ IsFallbackMatch = isFallbackMatch;
+ }
+
+ public OutboundMatch Match { get; }
+
+ public bool IsFallbackMatch { get; }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/PathTokenizer.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/PathTokenizer.cs
new file mode 100644
index 0000000000..9418989fdb
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/PathTokenizer.cs
@@ -0,0 +1,205 @@
+// 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;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.Routing.Internal
+{
+ public struct PathTokenizer : IReadOnlyList<StringSegment>
+ {
+ private readonly string _path;
+ private int _count;
+
+ public PathTokenizer(PathString path)
+ {
+ _path = path.Value;
+ _count = -1;
+ }
+
+ public int Count
+ {
+ get
+ {
+ if (_count == -1)
+ {
+ // We haven't computed the real count of segments yet.
+ if (_path.Length == 0)
+ {
+ // The empty string has length of 0.
+ _count = 0;
+ return _count;
+ }
+
+ // A string of length 1 must be "/" - all PathStrings start with '/'
+ if (_path.Length == 1)
+ {
+ // We treat this as empty - there's nothing to parse here for routing, because routing ignores
+ // a trailing slash.
+ Debug.Assert(_path[0] == '/');
+ _count = 0;
+ return _count;
+ }
+
+ // This is a non-trival PathString
+ _count = 1;
+
+ // Since a non-empty PathString must begin with a `/`, we can just count the number of occurrences
+ // of `/` to find the number of segments. However, we don't look at the last character, because
+ // routing ignores a trailing slash.
+ for (var i = 1; i < _path.Length - 1; i++)
+ {
+ if (_path[i] == '/')
+ {
+ _count++;
+ }
+ }
+ }
+
+ return _count;
+ }
+ }
+
+ public StringSegment this[int index]
+ {
+ get
+ {
+ if (index >= Count)
+ {
+ throw new IndexOutOfRangeException();
+ }
+
+
+ var currentSegmentIndex = 0;
+ var currentSegmentStart = 1;
+
+ // Skip the first `/`.
+ var delimiterIndex = 1;
+ while ((delimiterIndex = _path.IndexOf('/', delimiterIndex)) != -1)
+ {
+ if (currentSegmentIndex++ == index)
+ {
+ return new StringSegment(_path, currentSegmentStart, delimiterIndex - currentSegmentStart);
+ }
+ else
+ {
+ currentSegmentStart = delimiterIndex + 1;
+ delimiterIndex++;
+ }
+ }
+
+ // If we get here we're at the end of the string. The implementation of .Count should protect us
+ // from these cases.
+ Debug.Assert(_path[_path.Length - 1] != '/');
+ Debug.Assert(currentSegmentIndex == index);
+
+ return new StringSegment(_path, currentSegmentStart, _path.Length - currentSegmentStart);
+ }
+ }
+
+ public Enumerator GetEnumerator()
+ {
+ return new Enumerator(this);
+ }
+
+ IEnumerator<StringSegment> IEnumerable<StringSegment>.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public struct Enumerator : IEnumerator<StringSegment>
+ {
+ private readonly string _path;
+
+ private int _index;
+ private int _length;
+
+ public Enumerator(PathTokenizer tokenizer)
+ {
+ _path = tokenizer._path;
+
+ _index = -1;
+ _length = -1;
+ }
+
+ public StringSegment Current
+ {
+ get
+ {
+ return new StringSegment(_path, _index, _length);
+ }
+ }
+
+ object IEnumerator.Current
+ {
+ get
+ {
+ return Current;
+ }
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public bool MoveNext()
+ {
+ if (_path == null || _path.Length <= 1)
+ {
+ return false;
+ }
+
+ if (_index == -1)
+ {
+ // Skip the first `/`.
+ _index = 1;
+ }
+ else
+ {
+ // Skip to the end of the previous segment + the separator.
+ _index += _length + 1;
+ }
+
+ if (_index >= _path.Length)
+ {
+ // We're at the end
+ return false;
+ }
+
+ var delimiterIndex = _path.IndexOf('/', _index);
+ if (delimiterIndex != -1)
+ {
+ _length = delimiterIndex - _index;
+ return true;
+ }
+
+ // We might have some trailing text after the last separator.
+ if (_path[_path.Length - 1] == '/')
+ {
+ // If the last char is a '/' then it's just a trailing slash, we don't have another segment.
+ return false;
+ }
+ else
+ {
+ _length = _path.Length - _index;
+ return true;
+ }
+ }
+
+ public void Reset()
+ {
+ _index = -1;
+ _length = -1;
+ }
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/RoutingMarkerService.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/RoutingMarkerService.cs
new file mode 100644
index 0000000000..b180294316
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/RoutingMarkerService.cs
@@ -0,0 +1,15 @@
+// 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.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Routing.Internal
+{
+ /// <summary>
+ /// A marker class used to determine if all the routing services were added
+ /// to the <see cref="IServiceCollection"/> before routing is configured.
+ /// </summary>
+ public class RoutingMarkerService
+ {
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/SegmentState.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/SegmentState.cs
new file mode 100644
index 0000000000..35076a0678
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/SegmentState.cs
@@ -0,0 +1,17 @@
+// 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.Routing.Internal
+{
+ // Segments are treated as all-or-none. We should never output a partial segment.
+ // If we add any subsegment of this segment to the generated URI, we have to add
+ // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we
+ // used a value for {p1}, we have to output the entire segment up to the next "/".
+ // Otherwise we could end up with the partial segment "v1" instead of the entire
+ // segment "v1-v2.xml".
+ public enum SegmentState
+ {
+ Beginning,
+ Inside,
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuilderContextPooledObjectPolicy.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuilderContextPooledObjectPolicy.cs
new file mode 100644
index 0000000000..953d6a86c4
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuilderContextPooledObjectPolicy.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.Text.Encodings.Web;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Microsoft.AspNetCore.Routing.Internal
+{
+ public class UriBuilderContextPooledObjectPolicy : IPooledObjectPolicy<UriBuildingContext>
+ {
+ public UriBuildingContext Create()
+ {
+ return new UriBuildingContext(UrlEncoder.Default);
+ }
+
+ public bool Return(UriBuildingContext obj)
+ {
+ obj.Clear();
+ return true;
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs
new file mode 100644
index 0000000000..3b78fe8c78
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs
@@ -0,0 +1,205 @@
+// 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.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Text.Encodings.Web;
+
+namespace Microsoft.AspNetCore.Routing.Internal
+{
+ [DebuggerDisplay("{DebuggerToString(),nq}")]
+ public class UriBuildingContext
+ {
+ // Holds the 'accepted' parts of the uri.
+ private readonly StringBuilder _uri;
+
+ // Holds the 'optional' parts of the uri. We need a secondary buffer to handle cases where an optional
+ // segment is in the middle of the uri. We don't know if we need to write it out - if it's
+ // followed by other optional segments than we will just throw it away.
+ private readonly List<BufferValue> _buffer;
+ private readonly UrlEncoder _urlEncoder;
+
+ private bool _hasEmptySegment;
+ private int _lastValueOffset;
+
+ public UriBuildingContext(UrlEncoder urlEncoder)
+ {
+ _urlEncoder = urlEncoder;
+ _uri = new StringBuilder();
+ _buffer = new List<BufferValue>();
+ Writer = new StringWriter(_uri);
+ _lastValueOffset = -1;
+
+ BufferState = SegmentState.Beginning;
+ UriState = SegmentState.Beginning;
+ }
+
+ public SegmentState BufferState { get; private set; }
+
+ public SegmentState UriState { get; private set; }
+
+ public TextWriter Writer { get; }
+
+ public bool Accept(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ if (UriState == SegmentState.Inside || BufferState == SegmentState.Inside)
+ {
+ // We can't write an 'empty' part inside a segment
+ return false;
+ }
+ else
+ {
+ _hasEmptySegment = true;
+ return true;
+ }
+ }
+ else if (_hasEmptySegment)
+ {
+ // We're trying to write text after an empty segment - this is not allowed.
+ return false;
+ }
+
+ for (var i = 0; i < _buffer.Count; i++)
+ {
+ if (_buffer[i].RequiresEncoding)
+ {
+ _urlEncoder.Encode(Writer, _buffer[i].Value);
+ }
+ else
+ {
+ _uri.Append(_buffer[i].Value);
+ }
+ }
+ _buffer.Clear();
+
+ if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning)
+ {
+ if (_uri.Length != 0)
+ {
+ _uri.Append("/");
+ }
+ }
+
+ BufferState = SegmentState.Inside;
+ UriState = SegmentState.Inside;
+
+ _lastValueOffset = _uri.Length;
+ // Allow the first segment to have a leading slash.
+ // This prevents the leading slash from PathString segments from being encoded.
+ if (_uri.Length == 0 && value.Length > 0 && value[0] == '/')
+ {
+ _uri.Append("/");
+ _urlEncoder.Encode(Writer, value, 1, value.Length - 1);
+ }
+ else
+ {
+ _urlEncoder.Encode(Writer, value);
+ }
+
+ return true;
+ }
+
+ public void Remove(string literal)
+ {
+ Debug.Assert(_lastValueOffset != -1, "Cannot invoke Remove more than once.");
+ _uri.Length = _lastValueOffset;
+ _lastValueOffset = -1;
+ }
+
+ public bool Buffer(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ if (BufferState == SegmentState.Inside)
+ {
+ // We can't write an 'empty' part inside a segment
+ return false;
+ }
+ else
+ {
+ _hasEmptySegment = true;
+ return true;
+ }
+ }
+ else if (_hasEmptySegment)
+ {
+ // We're trying to write text after an empty segment - this is not allowed.
+ return false;
+ }
+
+ if (UriState == SegmentState.Inside)
+ {
+ // We've already written part of this segment so there's no point in buffering, we need to
+ // write out the rest or give up.
+ var result = Accept(value);
+
+ // We've already checked the conditions that could result in a rejected part, so this should
+ // always be true.
+ Debug.Assert(result);
+
+ return result;
+ }
+
+ if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning)
+ {
+ if (_uri.Length != 0 || _buffer.Count != 0)
+ {
+ _buffer.Add(new BufferValue("/", requiresEncoding: false));
+ }
+
+ BufferState = SegmentState.Inside;
+ }
+
+ _buffer.Add(new BufferValue(value, requiresEncoding: true));
+ return true;
+ }
+
+ public void EndSegment()
+ {
+ BufferState = SegmentState.Beginning;
+ UriState = SegmentState.Beginning;
+ }
+
+ public void Clear()
+ {
+ _uri.Clear();
+ if (_uri.Capacity > 128)
+ {
+ // We don't want to retain too much memory if this is getting pooled.
+ _uri.Capacity = 128;
+ }
+
+ _buffer.Clear();
+ if (_buffer.Capacity > 8)
+ {
+ _buffer.Capacity = 8;
+ }
+
+ _hasEmptySegment = false;
+ _lastValueOffset = -1;
+ BufferState = SegmentState.Beginning;
+ UriState = SegmentState.Beginning;
+ }
+
+ public override string ToString()
+ {
+ // We can ignore any currently buffered segments - they are are guaranteed to be 'defaults'.
+ if (_uri.Length > 0 && _uri[0] != '/')
+ {
+ // Normalize generated paths so that they always contain a leading slash.
+ _uri.Insert(0, '/');
+ }
+
+ return _uri.ToString();
+ }
+
+ private string DebuggerToString()
+ {
+ return string.Format("{{Accepted: '{0}' Buffered: '{1}'}}", _uri, string.Join("", _buffer));
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouteConstraintMatcherExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouteConstraintMatcherExtensions.cs
new file mode 100644
index 0000000000..9a19250f30
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouteConstraintMatcherExtensions.cs
@@ -0,0 +1,31 @@
+// 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.Routing.Logging
+{
+ internal static class RouteConstraintMatcherExtensions
+ {
+ private static readonly Action<ILogger, object, string, IRouteConstraint, Exception> _routeValueDoesNotMatchConstraint;
+
+ static RouteConstraintMatcherExtensions()
+ {
+ _routeValueDoesNotMatchConstraint = LoggerMessage.Define<object, string, IRouteConstraint>(
+ LogLevel.Debug,
+ 1,
+ "Route value '{RouteValue}' with key '{RouteKey}' did not match " +
+ "the constraint '{RouteConstraint}'.");
+ }
+
+ public static void RouteValueDoesNotMatchConstraint(
+ this ILogger logger,
+ object routeValue,
+ string routeKey,
+ IRouteConstraint routeConstraint)
+ {
+ _routeValueDoesNotMatchConstraint(logger, routeValue, routeKey, routeConstraint, null);
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouterMiddlewareLoggerExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouterMiddlewareLoggerExtensions.cs
new file mode 100644
index 0000000000..35f05272cd
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/RouterMiddlewareLoggerExtensions.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 System;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Routing.Logging
+{
+ internal static class RouterMiddlewareLoggerExtensions
+ {
+ private static readonly Action<ILogger, Exception> _requestDidNotMatchRoutes;
+
+ static RouterMiddlewareLoggerExtensions()
+ {
+ _requestDidNotMatchRoutes = LoggerMessage.Define(
+ LogLevel.Debug,
+ 1,
+ "Request did not match any routes.");
+ }
+
+ public static void RequestDidNotMatchRoutes(this ILogger logger)
+ {
+ _requestDidNotMatchRoutes(logger, null);
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/TreeRouterLoggerExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/TreeRouterLoggerExtensions.cs
new file mode 100644
index 0000000000..3f6af8e5dc
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Logging/TreeRouterLoggerExtensions.cs
@@ -0,0 +1,29 @@
+// 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.Routing.Logging
+{
+ internal static class TreeRouterLoggerExtensions
+ {
+ private static readonly Action<ILogger, string, string, Exception> _matchedRoute;
+
+ static TreeRouterLoggerExtensions()
+ {
+ _matchedRoute = LoggerMessage.Define<string, string>(
+ LogLevel.Debug,
+ 1,
+ "Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'.");
+ }
+
+ public static void MatchedRoute(
+ this ILogger logger,
+ string routeName,
+ string routeTemplate)
+ {
+ _matchedRoute(logger, routeName, routeTemplate, null);
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs
new file mode 100644
index 0000000000..ef55007af3
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/MapRouteRouteBuilderExtensions.cs
@@ -0,0 +1,126 @@
+// 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.Routing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Provides extension methods for <see cref="IRouteBuilder"/> to add routes.
+ /// </summary>
+ public static class MapRouteRouteBuilderExtensions
+ {
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> with the specified name and template.
+ /// </summary>
+ /// <param name="routeBuilder">The <see cref="IRouteBuilder"/> to add the route to.</param>
+ /// <param name="name">The name of the route.</param>
+ /// <param name="template">The URL pattern of the route.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public static IRouteBuilder MapRoute(
+ this IRouteBuilder routeBuilder,
+ string name,
+ string template)
+ {
+ MapRoute(routeBuilder, name, template, defaults: null);
+ return routeBuilder;
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> with the specified name, template, and default values.
+ /// </summary>
+ /// <param name="routeBuilder">The <see cref="IRouteBuilder"/> to add the route to.</param>
+ /// <param name="name">The name of the route.</param>
+ /// <param name="template">The URL pattern of the route.</param>
+ /// <param name="defaults">
+ /// An object that contains default values for route parameters. The object's properties represent the names
+ /// and values of the default values.
+ /// </param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public static IRouteBuilder MapRoute(
+ this IRouteBuilder routeBuilder,
+ string name,
+ string template,
+ object defaults)
+ {
+ return MapRoute(routeBuilder, name, template, defaults, constraints: null);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> with the specified name, template, default values, and
+ /// constraints.
+ /// </summary>
+ /// <param name="routeBuilder">The <see cref="IRouteBuilder"/> to add the route to.</param>
+ /// <param name="name">The name of the route.</param>
+ /// <param name="template">The URL pattern of the route.</param>
+ /// <param name="defaults">
+ /// An object that contains default values for route parameters. The object's properties represent the names
+ /// and values of the default values.
+ /// </param>
+ /// <param name="constraints">
+ /// An object that contains constraints for the route. The object's properties represent the names and values
+ /// of the constraints.
+ /// </param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public static IRouteBuilder MapRoute(
+ this IRouteBuilder routeBuilder,
+ string name,
+ string template,
+ object defaults,
+ object constraints)
+ {
+ return MapRoute(routeBuilder, name, template, defaults, constraints, dataTokens: null);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> with the specified name, template, default values, and
+ /// data tokens.
+ /// </summary>
+ /// <param name="routeBuilder">The <see cref="IRouteBuilder"/> to add the route to.</param>
+ /// <param name="name">The name of the route.</param>
+ /// <param name="template">The URL pattern of the route.</param>
+ /// <param name="defaults">
+ /// An object that contains default values for route parameters. The object's properties represent the names
+ /// and values of the default values.
+ /// </param>
+ /// <param name="constraints">
+ /// An object that contains constraints for the route. The object's properties represent the names and values
+ /// of the constraints.
+ /// </param>
+ /// <param name="dataTokens">
+ /// An object that contains data tokens for the route. The object's properties represent the names and values
+ /// of the data tokens.
+ /// </param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public static IRouteBuilder MapRoute(
+ this IRouteBuilder routeBuilder,
+ string name,
+ string template,
+ object defaults,
+ object constraints,
+ object dataTokens)
+ {
+ if (routeBuilder.DefaultHandler == null)
+ {
+ throw new RouteCreationException(Resources.FormatDefaultHandler_MustBeSet(nameof(IRouteBuilder)));
+ }
+
+ var inlineConstraintResolver = routeBuilder
+ .ServiceProvider
+ .GetRequiredService<IInlineConstraintResolver>();
+
+ routeBuilder.Routes.Add(new Route(
+ routeBuilder.DefaultHandler,
+ name,
+ template,
+ new RouteValueDictionary(defaults),
+ new RouteValueDictionary(constraints),
+ new RouteValueDictionary(dataTokens),
+ inlineConstraintResolver));
+
+ return routeBuilder;
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj b/src/Routing/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj
new file mode 100644
index 0000000000..f1e3d36a55
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Microsoft.AspNetCore.Routing.csproj
@@ -0,0 +1,29 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <Description>ASP.NET Core middleware for routing requests to application logic and for generating links.
+Commonly used types:
+Microsoft.AspNetCore.Routing.Route
+Microsoft.AspNetCore.Routing.RouteCollection</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;routing</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\..\shared\Microsoft.AspNetCore.Routing.DecisionTree.Sources\**\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.Routing.Abstractions\Microsoft.AspNetCore.Routing.Abstractions.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(MicrosoftAspNetCoreHttpExtensionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.HashCodeCombiner.Sources" Version="$(MicrosoftExtensionsHashCodeCombinerSourcesPackageVersion)" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="$(MicrosoftExtensionsObjectPoolPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.PropertyHelper.Sources" Version="$(MicrosoftExtensionsPropertyHelperSourcesPackageVersion)" PrivateAssets="All" />
+ </ItemGroup>
+</Project>
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Properties/AssemblyInfo.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..d8b03e1556
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// 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("Microsoft.AspNetCore.Routing.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..6d2bb0a7c9
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Properties/Resources.Designer.cs
@@ -0,0 +1,422 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Routing
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Routing.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// Value must be greater than or equal to {0}.
+ /// </summary>
+ internal static string ArgumentMustBeGreaterThanOrEqualTo
+ {
+ get => GetString("ArgumentMustBeGreaterThanOrEqualTo");
+ }
+
+ /// <summary>
+ /// Value must be greater than or equal to {0}.
+ /// </summary>
+ internal static string FormatArgumentMustBeGreaterThanOrEqualTo(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("ArgumentMustBeGreaterThanOrEqualTo"), p0);
+
+ /// <summary>
+ /// The value for argument '{0}' should be less than or equal to the value for the argument '{1}'.
+ /// </summary>
+ internal static string RangeConstraint_MinShouldBeLessThanOrEqualToMax
+ {
+ get => GetString("RangeConstraint_MinShouldBeLessThanOrEqualToMax");
+ }
+
+ /// <summary>
+ /// The value for argument '{0}' should be less than or equal to the value for the argument '{1}'.
+ /// </summary>
+ internal static string FormatRangeConstraint_MinShouldBeLessThanOrEqualToMax(object p0, object p1)
+ => string.Format(CultureInfo.CurrentCulture, GetString("RangeConstraint_MinShouldBeLessThanOrEqualToMax"), p0, p1);
+
+ /// <summary>
+ /// The '{0}' property of '{1}' must not be null.
+ /// </summary>
+ internal static string PropertyOfTypeCannotBeNull
+ {
+ get => GetString("PropertyOfTypeCannotBeNull");
+ }
+
+ /// <summary>
+ /// The '{0}' property of '{1}' must not be null.
+ /// </summary>
+ internal static string FormatPropertyOfTypeCannotBeNull(object p0, object p1)
+ => string.Format(CultureInfo.CurrentCulture, GetString("PropertyOfTypeCannotBeNull"), p0, p1);
+
+ /// <summary>
+ /// The supplied route name '{0}' is ambiguous and matched more than one route.
+ /// </summary>
+ internal static string NamedRoutes_AmbiguousRoutesFound
+ {
+ get => GetString("NamedRoutes_AmbiguousRoutesFound");
+ }
+
+ /// <summary>
+ /// The supplied route name '{0}' is ambiguous and matched more than one route.
+ /// </summary>
+ internal static string FormatNamedRoutes_AmbiguousRoutesFound(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("NamedRoutes_AmbiguousRoutesFound"), p0);
+
+ /// <summary>
+ /// A default handler must be set on the {0}.
+ /// </summary>
+ internal static string DefaultHandler_MustBeSet
+ {
+ get => GetString("DefaultHandler_MustBeSet");
+ }
+
+ /// <summary>
+ /// A default handler must be set on the {0}.
+ /// </summary>
+ internal static string FormatDefaultHandler_MustBeSet(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("DefaultHandler_MustBeSet"), p0);
+
+ /// <summary>
+ /// The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}.
+ /// </summary>
+ internal static string DefaultInlineConstraintResolver_AmbiguousCtors
+ {
+ get => GetString("DefaultInlineConstraintResolver_AmbiguousCtors");
+ }
+
+ /// <summary>
+ /// The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}.
+ /// </summary>
+ internal static string FormatDefaultInlineConstraintResolver_AmbiguousCtors(object p0, object p1)
+ => string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_AmbiguousCtors"), p0, p1);
+
+ /// <summary>
+ /// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.
+ /// </summary>
+ internal static string DefaultInlineConstraintResolver_CouldNotFindCtor
+ {
+ get => GetString("DefaultInlineConstraintResolver_CouldNotFindCtor");
+ }
+
+ /// <summary>
+ /// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.
+ /// </summary>
+ internal static string FormatDefaultInlineConstraintResolver_CouldNotFindCtor(object p0, object p1)
+ => string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_CouldNotFindCtor"), p0, p1);
+
+ /// <summary>
+ /// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.
+ /// </summary>
+ internal static string DefaultInlineConstraintResolver_TypeNotConstraint
+ {
+ get => GetString("DefaultInlineConstraintResolver_TypeNotConstraint");
+ }
+
+ /// <summary>
+ /// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.
+ /// </summary>
+ internal static string FormatDefaultInlineConstraintResolver_TypeNotConstraint(object p0, object p1, object p2)
+ => string.Format(CultureInfo.CurrentCulture, GetString("DefaultInlineConstraintResolver_TypeNotConstraint"), p0, p1, p2);
+
+ /// <summary>
+ /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.
+ /// </summary>
+ internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment
+ {
+ get => GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment");
+ }
+
+ /// <summary>
+ /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.
+ /// </summary>
+ internal static string FormatTemplateRoute_CannotHaveCatchAllInMultiSegment()
+ => GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment");
+
+ /// <summary>
+ /// The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them.
+ /// </summary>
+ internal static string TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly
+ {
+ get => GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly");
+ }
+
+ /// <summary>
+ /// The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them.
+ /// </summary>
+ internal static string FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly"), p0);
+
+ /// <summary>
+ /// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.
+ /// </summary>
+ internal static string TemplateRoute_CannotHaveConsecutiveParameters
+ {
+ get => GetString("TemplateRoute_CannotHaveConsecutiveParameters");
+ }
+
+ /// <summary>
+ /// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.
+ /// </summary>
+ internal static string FormatTemplateRoute_CannotHaveConsecutiveParameters()
+ => GetString("TemplateRoute_CannotHaveConsecutiveParameters");
+
+ /// <summary>
+ /// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.
+ /// </summary>
+ internal static string TemplateRoute_CannotHaveConsecutiveSeparators
+ {
+ get => GetString("TemplateRoute_CannotHaveConsecutiveSeparators");
+ }
+
+ /// <summary>
+ /// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.
+ /// </summary>
+ internal static string FormatTemplateRoute_CannotHaveConsecutiveSeparators()
+ => GetString("TemplateRoute_CannotHaveConsecutiveSeparators");
+
+ /// <summary>
+ /// A catch-all parameter cannot be marked optional.
+ /// </summary>
+ internal static string TemplateRoute_CatchAllCannotBeOptional
+ {
+ get => GetString("TemplateRoute_CatchAllCannotBeOptional");
+ }
+
+ /// <summary>
+ /// A catch-all parameter cannot be marked optional.
+ /// </summary>
+ internal static string FormatTemplateRoute_CatchAllCannotBeOptional()
+ => GetString("TemplateRoute_CatchAllCannotBeOptional");
+
+ /// <summary>
+ /// An optional parameter cannot have default value.
+ /// </summary>
+ internal static string TemplateRoute_OptionalCannotHaveDefaultValue
+ {
+ get => GetString("TemplateRoute_OptionalCannotHaveDefaultValue");
+ }
+
+ /// <summary>
+ /// An optional parameter cannot have default value.
+ /// </summary>
+ internal static string FormatTemplateRoute_OptionalCannotHaveDefaultValue()
+ => GetString("TemplateRoute_OptionalCannotHaveDefaultValue");
+
+ /// <summary>
+ /// A catch-all parameter can only appear as the last segment of the route template.
+ /// </summary>
+ internal static string TemplateRoute_CatchAllMustBeLast
+ {
+ get => GetString("TemplateRoute_CatchAllMustBeLast");
+ }
+
+ /// <summary>
+ /// A catch-all parameter can only appear as the last segment of the route template.
+ /// </summary>
+ internal static string FormatTemplateRoute_CatchAllMustBeLast()
+ => GetString("TemplateRoute_CatchAllMustBeLast");
+
+ /// <summary>
+ /// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character.
+ /// </summary>
+ internal static string TemplateRoute_InvalidLiteral
+ {
+ get => GetString("TemplateRoute_InvalidLiteral");
+ }
+
+ /// <summary>
+ /// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character.
+ /// </summary>
+ internal static string FormatTemplateRoute_InvalidLiteral(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_InvalidLiteral"), p0);
+
+ /// <summary>
+ /// The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter.
+ /// </summary>
+ internal static string TemplateRoute_InvalidParameterName
+ {
+ get => GetString("TemplateRoute_InvalidParameterName");
+ }
+
+ /// <summary>
+ /// The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter.
+ /// </summary>
+ internal static string FormatTemplateRoute_InvalidParameterName(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_InvalidParameterName"), p0);
+
+ /// <summary>
+ /// The route template cannot start with a '~' character unless followed by a '/'.
+ /// </summary>
+ internal static string TemplateRoute_InvalidRouteTemplate
+ {
+ get => GetString("TemplateRoute_InvalidRouteTemplate");
+ }
+
+ /// <summary>
+ /// The route template cannot start with a '~' character unless followed by a '/'.
+ /// </summary>
+ internal static string FormatTemplateRoute_InvalidRouteTemplate()
+ => GetString("TemplateRoute_InvalidRouteTemplate");
+
+ /// <summary>
+ /// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.
+ /// </summary>
+ internal static string TemplateRoute_MismatchedParameter
+ {
+ get => GetString("TemplateRoute_MismatchedParameter");
+ }
+
+ /// <summary>
+ /// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.
+ /// </summary>
+ internal static string FormatTemplateRoute_MismatchedParameter()
+ => GetString("TemplateRoute_MismatchedParameter");
+
+ /// <summary>
+ /// The route parameter name '{0}' appears more than one time in the route template.
+ /// </summary>
+ internal static string TemplateRoute_RepeatedParameter
+ {
+ get => GetString("TemplateRoute_RepeatedParameter");
+ }
+
+ /// <summary>
+ /// The route parameter name '{0}' appears more than one time in the route template.
+ /// </summary>
+ internal static string FormatTemplateRoute_RepeatedParameter(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_RepeatedParameter"), p0);
+
+ /// <summary>
+ /// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.
+ /// </summary>
+ internal static string RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint
+ {
+ get => GetString("RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint");
+ }
+
+ /// <summary>
+ /// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.
+ /// </summary>
+ internal static string FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint(object p0, object p1, object p2, object p3)
+ => string.Format(CultureInfo.CurrentCulture, GetString("RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint"), p0, p1, p2, p3);
+
+ /// <summary>
+ /// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.
+ /// </summary>
+ internal static string RouteConstraintBuilder_CouldNotResolveConstraint
+ {
+ get => GetString("RouteConstraintBuilder_CouldNotResolveConstraint");
+ }
+
+ /// <summary>
+ /// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.
+ /// </summary>
+ internal static string FormatRouteConstraintBuilder_CouldNotResolveConstraint(object p0, object p1, object p2, object p3)
+ => string.Format(CultureInfo.CurrentCulture, GetString("RouteConstraintBuilder_CouldNotResolveConstraint"), p0, p1, p2, p3);
+
+ /// <summary>
+ /// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.
+ /// </summary>
+ internal static string TemplateRoute_UnescapedBrace
+ {
+ get => GetString("TemplateRoute_UnescapedBrace");
+ }
+
+ /// <summary>
+ /// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.
+ /// </summary>
+ internal static string FormatTemplateRoute_UnescapedBrace()
+ => GetString("TemplateRoute_UnescapedBrace");
+
+ /// <summary>
+ /// In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter.
+ /// </summary>
+ internal static string TemplateRoute_OptionalParameterCanbBePrecededByPeriod
+ {
+ get => GetString("TemplateRoute_OptionalParameterCanbBePrecededByPeriod");
+ }
+
+ /// <summary>
+ /// In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter.
+ /// </summary>
+ internal static string FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod(object p0, object p1, object p2)
+ => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_OptionalParameterCanbBePrecededByPeriod"), p0, p1, p2);
+
+ /// <summary>
+ /// An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'.
+ /// </summary>
+ internal static string TemplateRoute_OptionalParameterHasTobeTheLast
+ {
+ get => GetString("TemplateRoute_OptionalParameterHasTobeTheLast");
+ }
+
+ /// <summary>
+ /// An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'.
+ /// </summary>
+ internal static string FormatTemplateRoute_OptionalParameterHasTobeTheLast(object p0, object p1, object p2)
+ => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_OptionalParameterHasTobeTheLast"), p0, p1, p2);
+
+ /// <summary>
+ /// Two or more routes named '{0}' have different templates.
+ /// </summary>
+ internal static string AttributeRoute_DifferentLinkGenerationEntries_SameName
+ {
+ get => GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName");
+ }
+
+ /// <summary>
+ /// Two or more routes named '{0}' have different templates.
+ /// </summary>
+ internal static string FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("AttributeRoute_DifferentLinkGenerationEntries_SameName"), p0);
+
+ /// <summary>
+ /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code.
+ /// </summary>
+ internal static string UnableToFindServices
+ {
+ get => GetString("UnableToFindServices");
+ }
+
+ /// <summary>
+ /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code.
+ /// </summary>
+ internal static string FormatUnableToFindServices(object p0, object p1, object p2)
+ => string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p2);
+
+ /// <summary>
+ /// An error occurred while creating the route with name '{0}' and template '{1}'.
+ /// </summary>
+ internal static string TemplateRoute_Exception
+ {
+ get => GetString("TemplateRoute_Exception");
+ }
+
+ /// <summary>
+ /// An error occurred while creating the route with name '{0}' and template '{1}'.
+ /// </summary>
+ internal static string FormatTemplateRoute_Exception(object p0, object p1)
+ => string.Format(CultureInfo.CurrentCulture, GetString("TemplateRoute_Exception"), p0, p1);
+
+ 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/Routing/src/Microsoft.AspNetCore.Routing/RequestDelegateRouteBuilderExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RequestDelegateRouteBuilderExtensions.cs
new file mode 100644
index 0000000000..f376683c4d
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RequestDelegateRouteBuilderExtensions.cs
@@ -0,0 +1,295 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Constraints;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public static class RequestDelegateRouteBuilderExtensions
+ {
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> for the given <paramref name="template"/>, and
+ /// <paramref name="handler"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="handler">The <see cref="RequestDelegate"/> route handler.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapRoute(this IRouteBuilder builder, string template, RequestDelegate handler)
+ {
+ var route = new Route(
+ new RouteHandler(handler),
+ template,
+ defaults: null,
+ constraints: null,
+ dataTokens: null,
+ inlineConstraintResolver: GetConstraintResolver(builder));
+
+ builder.Routes.Add(route);
+ return builder;
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> for the given <paramref name="template"/>, and
+ /// <paramref name="action"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="action">The action to apply to the <see cref="IApplicationBuilder"/>.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapMiddlewareRoute(this IRouteBuilder builder, string template, Action<IApplicationBuilder> action)
+ {
+ var nested = builder.ApplicationBuilder.New();
+ action(nested);
+ return builder.MapRoute(template, nested.Build());
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP DELETE requests for the given
+ /// <paramref name="template"/>, and <paramref name="handler"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="handler">The <see cref="RequestDelegate"/> route handler.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapDelete(this IRouteBuilder builder, string template, RequestDelegate handler)
+ {
+ return builder.MapVerb("DELETE", template, handler);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP DELETE requests for the given
+ /// <paramref name="template"/>, and <paramref name="action"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="action">The action to apply to the <see cref="IApplicationBuilder"/>.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapMiddlewareDelete(this IRouteBuilder builder, string template, Action<IApplicationBuilder> action)
+ {
+ return builder.MapMiddlewareVerb("DELETE", template, action);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP DELETE requests for the given
+ /// <paramref name="template"/>, and <paramref name="handler"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="handler">The route handler.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapDelete(
+ this IRouteBuilder builder,
+ string template,
+ Func<HttpRequest, HttpResponse, RouteData, Task> handler)
+ {
+ return builder.MapVerb("DELETE", template, handler);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP GET requests for the given
+ /// <paramref name="template"/>, and <paramref name="handler"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="handler">The <see cref="RequestDelegate"/> route handler.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapGet(this IRouteBuilder builder, string template, RequestDelegate handler)
+ {
+ return builder.MapVerb("GET", template, handler);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP GET requests for the given
+ /// <paramref name="template"/>, and <paramref name="action"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="action">The action to apply to the <see cref="IApplicationBuilder"/>.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapMiddlewareGet(this IRouteBuilder builder, string template, Action<IApplicationBuilder> action)
+ {
+ return builder.MapMiddlewareVerb("GET", template, action);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP GET requests for the given
+ /// <paramref name="template"/>, and <paramref name="handler"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="handler">The route handler.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapGet(
+ this IRouteBuilder builder,
+ string template,
+ Func<HttpRequest, HttpResponse, RouteData, Task> handler)
+ {
+ return builder.MapVerb("GET", template, handler);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP POST requests for the given
+ /// <paramref name="template"/>, and <paramref name="handler"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="handler">The <see cref="RequestDelegate"/> route handler.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapPost(this IRouteBuilder builder, string template, RequestDelegate handler)
+ {
+ return builder.MapVerb("POST", template, handler);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP POST requests for the given
+ /// <paramref name="template"/>, and <paramref name="action"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="action">The action to apply to the <see cref="IApplicationBuilder"/>.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapMiddlewarePost(this IRouteBuilder builder, string template, Action<IApplicationBuilder> action)
+ {
+ return builder.MapMiddlewareVerb("POST", template, action);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP POST requests for the given
+ /// <paramref name="template"/>, and <paramref name="handler"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="handler">The route handler.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapPost(
+ this IRouteBuilder builder,
+ string template,
+ Func<HttpRequest, HttpResponse, RouteData, Task> handler)
+ {
+ return builder.MapVerb("POST", template, handler);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP PUT requests for the given
+ /// <paramref name="template"/>, and <paramref name="handler"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="handler">The <see cref="RequestDelegate"/> route handler.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapPut(this IRouteBuilder builder, string template, RequestDelegate handler)
+ {
+ return builder.MapVerb("PUT", template, handler);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP PUT requests for the given
+ /// <paramref name="template"/>, and <paramref name="action"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="action">The action to apply to the <see cref="IApplicationBuilder"/>.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapMiddlewarePut(this IRouteBuilder builder, string template, Action<IApplicationBuilder> action)
+ {
+ return builder.MapMiddlewareVerb("PUT", template, action);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP PUT requests for the given
+ /// <paramref name="template"/>, and <paramref name="handler"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="handler">The route handler.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapPut(
+ this IRouteBuilder builder,
+ string template,
+ Func<HttpRequest, HttpResponse, RouteData, Task> handler)
+ {
+ return builder.MapVerb("PUT", template, handler);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP requests for the given
+ /// <paramref name="verb"/>, <paramref name="template"/>, and <paramref name="handler"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="verb">The HTTP verb allowed by the route.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="handler">The route handler.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapVerb(
+ this IRouteBuilder builder,
+ string verb,
+ string template,
+ Func<HttpRequest, HttpResponse, RouteData, Task> handler)
+ {
+ RequestDelegate requestDelegate = (httpContext) =>
+ {
+ return handler(httpContext.Request, httpContext.Response, httpContext.GetRouteData());
+ };
+
+ return builder.MapVerb(verb, template, requestDelegate);
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP requests for the given
+ /// <paramref name="verb"/>, <paramref name="template"/>, and <paramref name="handler"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="verb">The HTTP verb allowed by the route.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="handler">The <see cref="RequestDelegate"/> route handler.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapVerb(
+ this IRouteBuilder builder,
+ string verb,
+ string template,
+ RequestDelegate handler)
+ {
+ var route = new Route(
+ new RouteHandler(handler),
+ template,
+ defaults: null,
+ constraints: new RouteValueDictionary(new { httpMethod = new HttpMethodRouteConstraint(verb) }),
+ dataTokens: null,
+ inlineConstraintResolver: GetConstraintResolver(builder));
+
+ builder.Routes.Add(route);
+ return builder;
+ }
+
+ /// <summary>
+ /// Adds a route to the <see cref="IRouteBuilder"/> that only matches HTTP requests for the given
+ /// <paramref name="verb"/>, <paramref name="template"/>, and <paramref name="action"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IRouteBuilder"/>.</param>
+ /// <param name="verb">The HTTP verb allowed by the route.</param>
+ /// <param name="template">The route template.</param>
+ /// <param name="action">The action to apply to the <see cref="IApplicationBuilder"/>.</param>
+ /// <returns>A reference to the <paramref name="builder"/> after this operation has completed.</returns>
+ public static IRouteBuilder MapMiddlewareVerb(
+ this IRouteBuilder builder,
+ string verb,
+ string template,
+ Action<IApplicationBuilder> action)
+ {
+ var nested = builder.ApplicationBuilder.New();
+ action(nested);
+ return builder.MapVerb(verb, template, nested.Build());
+ }
+
+ private static IInlineConstraintResolver GetConstraintResolver(IRouteBuilder builder)
+ {
+ return builder.ServiceProvider.GetRequiredService<IInlineConstraintResolver>();
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Resources.resx b/src/Routing/src/Microsoft.AspNetCore.Routing/Resources.resx
new file mode 100644
index 0000000000..d883906f7c
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Resources.resx
@@ -0,0 +1,204 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ArgumentMustBeGreaterThanOrEqualTo" xml:space="preserve">
+ <value>Value must be greater than or equal to {0}.</value>
+ </data>
+ <data name="RangeConstraint_MinShouldBeLessThanOrEqualToMax" xml:space="preserve">
+ <value>The value for argument '{0}' should be less than or equal to the value for the argument '{1}'.</value>
+ </data>
+ <data name="PropertyOfTypeCannotBeNull" xml:space="preserve">
+ <value>The '{0}' property of '{1}' must not be null.</value>
+ </data>
+ <data name="NamedRoutes_AmbiguousRoutesFound" xml:space="preserve">
+ <value>The supplied route name '{0}' is ambiguous and matched more than one route.</value>
+ </data>
+ <data name="DefaultHandler_MustBeSet" xml:space="preserve">
+ <value>A default handler must be set on the {0}.</value>
+ </data>
+ <data name="DefaultInlineConstraintResolver_AmbiguousCtors" xml:space="preserve">
+ <value>The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}.</value>
+ </data>
+ <data name="DefaultInlineConstraintResolver_CouldNotFindCtor" xml:space="preserve">
+ <value>Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.</value>
+ </data>
+ <data name="DefaultInlineConstraintResolver_TypeNotConstraint" xml:space="preserve">
+ <value>The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.</value>
+ </data>
+ <data name="TemplateRoute_CannotHaveCatchAllInMultiSegment" xml:space="preserve">
+ <value>A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.</value>
+ </data>
+ <data name="TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly" xml:space="preserve">
+ <value>The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them.</value>
+ </data>
+ <data name="TemplateRoute_CannotHaveConsecutiveParameters" xml:space="preserve">
+ <value>A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.</value>
+ </data>
+ <data name="TemplateRoute_CannotHaveConsecutiveSeparators" xml:space="preserve">
+ <value>The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.</value>
+ </data>
+ <data name="TemplateRoute_CatchAllCannotBeOptional" xml:space="preserve">
+ <value>A catch-all parameter cannot be marked optional.</value>
+ </data>
+ <data name="TemplateRoute_OptionalCannotHaveDefaultValue" xml:space="preserve">
+ <value>An optional parameter cannot have default value.</value>
+ </data>
+ <data name="TemplateRoute_CatchAllMustBeLast" xml:space="preserve">
+ <value>A catch-all parameter can only appear as the last segment of the route template.</value>
+ </data>
+ <data name="TemplateRoute_InvalidLiteral" xml:space="preserve">
+ <value>The literal section '{0}' is invalid. Literal sections cannot contain the '?' character.</value>
+ </data>
+ <data name="TemplateRoute_InvalidParameterName" xml:space="preserve">
+ <value>The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter.</value>
+ </data>
+ <data name="TemplateRoute_InvalidRouteTemplate" xml:space="preserve">
+ <value>The route template cannot start with a '~' character unless followed by a '/'.</value>
+ </data>
+ <data name="TemplateRoute_MismatchedParameter" xml:space="preserve">
+ <value>There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.</value>
+ </data>
+ <data name="TemplateRoute_RepeatedParameter" xml:space="preserve">
+ <value>The route parameter name '{0}' appears more than one time in the route template.</value>
+ </data>
+ <data name="RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint" xml:space="preserve">
+ <value>The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.</value>
+ </data>
+ <data name="RouteConstraintBuilder_CouldNotResolveConstraint" xml:space="preserve">
+ <value>The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.</value>
+ </data>
+ <data name="TemplateRoute_UnescapedBrace" xml:space="preserve">
+ <value>In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.</value>
+ </data>
+ <data name="TemplateRoute_OptionalParameterCanbBePrecededByPeriod" xml:space="preserve">
+ <value>In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter.</value>
+ </data>
+ <data name="TemplateRoute_OptionalParameterHasTobeTheLast" xml:space="preserve">
+ <value>An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'.</value>
+ </data>
+ <data name="AttributeRoute_DifferentLinkGenerationEntries_SameName" xml:space="preserve">
+ <value>Two or more routes named '{0}' have different templates.</value>
+ </data>
+ <data name="UnableToFindServices" xml:space="preserve">
+ <value>Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code.</value>
+ </data>
+ <data name="TemplateRoute_Exception" xml:space="preserve">
+ <value>An error occurred while creating the route with name '{0}' and template '{1}'.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Route.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Route.cs
new file mode 100644
index 0000000000..3d38f922b3
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Route.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 System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class Route : RouteBase
+ {
+ private readonly IRouter _target;
+
+ public Route(
+ IRouter target,
+ string routeTemplate,
+ IInlineConstraintResolver inlineConstraintResolver)
+ : this(
+ target,
+ routeTemplate,
+ defaults: null,
+ constraints: null,
+ dataTokens: null,
+ inlineConstraintResolver: inlineConstraintResolver)
+ {
+ }
+
+ public Route(
+ IRouter target,
+ string routeTemplate,
+ RouteValueDictionary defaults,
+ IDictionary<string, object> constraints,
+ RouteValueDictionary dataTokens,
+ IInlineConstraintResolver inlineConstraintResolver)
+ : this(target, null, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver)
+ {
+ }
+
+ public Route(
+ IRouter target,
+ string routeName,
+ string routeTemplate,
+ RouteValueDictionary defaults,
+ IDictionary<string, object> constraints,
+ RouteValueDictionary dataTokens,
+ IInlineConstraintResolver inlineConstraintResolver)
+ : base(
+ routeTemplate,
+ routeName,
+ inlineConstraintResolver,
+ defaults,
+ constraints,
+ dataTokens)
+ {
+ if (target == null)
+ {
+ throw new ArgumentNullException(nameof(target));
+ }
+
+ _target = target;
+ }
+
+ public string RouteTemplate => ParsedTemplate.TemplateText;
+
+ protected override Task OnRouteMatched(RouteContext context)
+ {
+ context.RouteData.Routers.Add(_target);
+ return _target.RouteAsync(context);
+ }
+
+ protected override VirtualPathData OnVirtualPathGenerated(VirtualPathContext context)
+ {
+ return _target.GetVirtualPath(context);
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteBase.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteBase.cs
new file mode 100644
index 0000000000..6d1eb7bd27
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteBase.cs
@@ -0,0 +1,272 @@
+// 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.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.AspNetCore.Routing.Logging;
+using Microsoft.AspNetCore.Routing.Template;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public abstract class RouteBase : IRouter, INamedRouter
+ {
+ private TemplateMatcher _matcher;
+ private TemplateBinder _binder;
+ private ILogger _logger;
+ private ILogger _constraintLogger;
+
+ public RouteBase(
+ string template,
+ string name,
+ IInlineConstraintResolver constraintResolver,
+ RouteValueDictionary defaults,
+ IDictionary<string, object> constraints,
+ RouteValueDictionary dataTokens)
+ {
+ if (constraintResolver == null)
+ {
+ throw new ArgumentNullException(nameof(constraintResolver));
+ }
+
+ template = template ?? string.Empty;
+ Name = name;
+ ConstraintResolver = constraintResolver;
+ DataTokens = dataTokens ?? new RouteValueDictionary();
+
+ try
+ {
+ // Data we parse from the template will be used to fill in the rest of the constraints or
+ // defaults. The parser will throw for invalid routes.
+ ParsedTemplate = TemplateParser.Parse(template);
+
+ Constraints = GetConstraints(constraintResolver, ParsedTemplate, constraints);
+ Defaults = GetDefaults(ParsedTemplate, defaults);
+ }
+ catch (Exception exception)
+ {
+ throw new RouteCreationException(Resources.FormatTemplateRoute_Exception(name, template), exception);
+ }
+ }
+
+ public virtual IDictionary<string, IRouteConstraint> Constraints { get; protected set; }
+
+ protected virtual IInlineConstraintResolver ConstraintResolver { get; set; }
+
+ public virtual RouteValueDictionary DataTokens { get; protected set; }
+
+ public virtual RouteValueDictionary Defaults { get; protected set; }
+
+ public virtual string Name { get; protected set; }
+
+ public virtual RouteTemplate ParsedTemplate { get; protected set; }
+
+ protected abstract Task OnRouteMatched(RouteContext context);
+
+ protected abstract VirtualPathData OnVirtualPathGenerated(VirtualPathContext context);
+
+ /// <inheritdoc />
+ public virtual Task RouteAsync(RouteContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ EnsureMatcher();
+ EnsureLoggers(context.HttpContext);
+
+ var requestPath = context.HttpContext.Request.Path;
+
+ if (!_matcher.TryMatch(requestPath, context.RouteData.Values))
+ {
+ // If we got back a null value set, that means the URI did not match
+ return Task.CompletedTask;
+ }
+
+ // Perf: Avoid accessing dictionaries if you don't need to write to them, these dictionaries are all
+ // created lazily.
+ if (DataTokens.Count > 0)
+ {
+ MergeValues(context.RouteData.DataTokens, DataTokens);
+ }
+
+ if (!RouteConstraintMatcher.Match(
+ Constraints,
+ context.RouteData.Values,
+ context.HttpContext,
+ this,
+ RouteDirection.IncomingRequest,
+ _constraintLogger))
+ {
+ return Task.CompletedTask;
+ }
+ _logger.MatchedRoute(Name, ParsedTemplate.TemplateText);
+
+ return OnRouteMatched(context);
+ }
+
+ /// <inheritdoc />
+ public virtual VirtualPathData GetVirtualPath(VirtualPathContext context)
+ {
+ EnsureBinder(context.HttpContext);
+ EnsureLoggers(context.HttpContext);
+
+ var values = _binder.GetValues(context.AmbientValues, context.Values);
+ if (values == null)
+ {
+ // We're missing one of the required values for this route.
+ return null;
+ }
+
+ if (!RouteConstraintMatcher.Match(
+ Constraints,
+ values.CombinedValues,
+ context.HttpContext,
+ this,
+ RouteDirection.UrlGeneration,
+ _constraintLogger))
+ {
+ return null;
+ }
+
+ context.Values = values.CombinedValues;
+
+ var pathData = OnVirtualPathGenerated(context);
+ if (pathData != null)
+ {
+ // If the target generates a value then that can short circuit.
+ return pathData;
+ }
+
+ // If we can produce a value go ahead and do it, the caller can check context.IsBound
+ // to see if the values were validated.
+
+ // When we still cannot produce a value, this should return null.
+ var virtualPath = _binder.BindValues(values.AcceptedValues);
+ if (virtualPath == null)
+ {
+ return null;
+ }
+
+ pathData = new VirtualPathData(this, virtualPath);
+ if (DataTokens != null)
+ {
+ foreach (var dataToken in DataTokens)
+ {
+ pathData.DataTokens.Add(dataToken.Key, dataToken.Value);
+ }
+ }
+
+ return pathData;
+ }
+
+ protected static IDictionary<string, IRouteConstraint> GetConstraints(
+ IInlineConstraintResolver inlineConstraintResolver,
+ RouteTemplate parsedTemplate,
+ IDictionary<string, object> constraints)
+ {
+ var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, parsedTemplate.TemplateText);
+
+ if (constraints != null)
+ {
+ foreach (var kvp in constraints)
+ {
+ constraintBuilder.AddConstraint(kvp.Key, kvp.Value);
+ }
+ }
+
+ foreach (var parameter in parsedTemplate.Parameters)
+ {
+ if (parameter.IsOptional)
+ {
+ constraintBuilder.SetOptional(parameter.Name);
+ }
+
+ foreach (var inlineConstraint in parameter.InlineConstraints)
+ {
+ constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint);
+ }
+ }
+
+ return constraintBuilder.Build();
+ }
+
+ protected static RouteValueDictionary GetDefaults(
+ RouteTemplate parsedTemplate,
+ RouteValueDictionary defaults)
+ {
+ var result = defaults == null ? new RouteValueDictionary() : new RouteValueDictionary(defaults);
+
+ foreach (var parameter in parsedTemplate.Parameters)
+ {
+ if (parameter.DefaultValue != null)
+ {
+ if (result.ContainsKey(parameter.Name))
+ {
+ throw new InvalidOperationException(
+ Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(
+ parameter.Name));
+ }
+ else
+ {
+ result.Add(parameter.Name, parameter.DefaultValue);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private static void MergeValues(
+ RouteValueDictionary destination,
+ RouteValueDictionary values)
+ {
+ foreach (var kvp in values)
+ {
+ // This will replace the original value for the specified key.
+ // Values from the matched route will take preference over previous
+ // data in the route context.
+ destination[kvp.Key] = kvp.Value;
+ }
+ }
+
+ private void EnsureBinder(HttpContext context)
+ {
+ if (_binder == null)
+ {
+ var pool = context.RequestServices.GetRequiredService<ObjectPool<UriBuildingContext>>();
+ _binder = new TemplateBinder(UrlEncoder.Default, pool, ParsedTemplate, Defaults);
+ }
+ }
+
+ private void EnsureLoggers(HttpContext context)
+ {
+ if (_logger == null)
+ {
+ var factory = context.RequestServices.GetRequiredService<ILoggerFactory>();
+ _logger = factory.CreateLogger(typeof(RouteBase).FullName);
+ _constraintLogger = factory.CreateLogger(typeof(RouteConstraintMatcher).FullName);
+ }
+ }
+
+ private void EnsureMatcher()
+ {
+ if (_matcher == null)
+ {
+ _matcher = new TemplateMatcher(ParsedTemplate, Defaults);
+ }
+ }
+
+ public override string ToString()
+ {
+ return ParsedTemplate.TemplateText;
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteBuilder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteBuilder.cs
new file mode 100644
index 0000000000..b492bfb59e
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteBuilder.cs
@@ -0,0 +1,61 @@
+// 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 Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class RouteBuilder : IRouteBuilder
+ {
+ public RouteBuilder(IApplicationBuilder applicationBuilder)
+ : this(applicationBuilder, defaultHandler: null)
+ {
+ }
+
+ public RouteBuilder(IApplicationBuilder applicationBuilder, IRouter defaultHandler)
+ {
+ if (applicationBuilder == null)
+ {
+ throw new ArgumentNullException(nameof(applicationBuilder));
+ }
+
+ if (applicationBuilder.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null)
+ {
+ throw new InvalidOperationException(Resources.FormatUnableToFindServices(
+ nameof(IServiceCollection),
+ nameof(RoutingServiceCollectionExtensions.AddRouting),
+ "ConfigureServices(...)"));
+ }
+
+ ApplicationBuilder = applicationBuilder;
+ DefaultHandler = defaultHandler;
+ ServiceProvider = applicationBuilder.ApplicationServices;
+
+ Routes = new List<IRouter>();
+ }
+
+ public IApplicationBuilder ApplicationBuilder { get; }
+
+ public IRouter DefaultHandler { get; set; }
+
+ public IServiceProvider ServiceProvider { get; }
+
+ public IList<IRouter> Routes { get; }
+
+ public IRouter Build()
+ {
+ var routeCollection = new RouteCollection();
+
+ foreach (var route in Routes)
+ {
+ routeCollection.Add(route);
+ }
+
+ return routeCollection;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteCollection.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteCollection.cs
new file mode 100644
index 0000000000..a2dae64291
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteCollection.cs
@@ -0,0 +1,181 @@
+// 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.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class RouteCollection : IRouteCollection
+ {
+ private readonly static char[] UrlQueryDelimiters = new char[] { '?', '#' };
+ private readonly List<IRouter> _routes = new List<IRouter>();
+ private readonly List<IRouter> _unnamedRoutes = new List<IRouter>();
+ private readonly Dictionary<string, INamedRouter> _namedRoutes =
+ new Dictionary<string, INamedRouter>(StringComparer.OrdinalIgnoreCase);
+
+ private RouteOptions _options;
+
+ public IRouter this[int index]
+ {
+ get { return _routes[index]; }
+ }
+
+ public int Count
+ {
+ get { return _routes.Count; }
+ }
+
+ public void Add(IRouter router)
+ {
+ if (router == null)
+ {
+ throw new ArgumentNullException(nameof(router));
+ }
+
+ var namedRouter = router as INamedRouter;
+ if (namedRouter != null)
+ {
+ if (!string.IsNullOrEmpty(namedRouter.Name))
+ {
+ _namedRoutes.Add(namedRouter.Name, namedRouter);
+ }
+ }
+ else
+ {
+ _unnamedRoutes.Add(router);
+ }
+
+ _routes.Add(router);
+ }
+
+ public async virtual Task RouteAsync(RouteContext context)
+ {
+ // Perf: We want to avoid allocating a new RouteData for each route we need to process.
+ // We can do this by snapshotting the state at the beginning and then restoring it
+ // for each router we execute.
+ var snapshot = context.RouteData.PushState(null, values: null, dataTokens: null);
+
+ for (var i = 0; i < Count; i++)
+ {
+ var route = this[i];
+ context.RouteData.Routers.Add(route);
+
+ try
+ {
+ await route.RouteAsync(context);
+
+ if (context.Handler != null)
+ {
+ break;
+ }
+ }
+ finally
+ {
+ if (context.Handler == null)
+ {
+ snapshot.Restore();
+ }
+ }
+ }
+ }
+
+ public virtual VirtualPathData GetVirtualPath(VirtualPathContext context)
+ {
+ EnsureOptions(context.HttpContext);
+
+ if (!string.IsNullOrEmpty(context.RouteName))
+ {
+ VirtualPathData namedRoutePathData = null;
+ INamedRouter matchedNamedRoute;
+ if (_namedRoutes.TryGetValue(context.RouteName, out matchedNamedRoute))
+ {
+ namedRoutePathData = matchedNamedRoute.GetVirtualPath(context);
+ }
+
+ var pathData = GetVirtualPath(context, _unnamedRoutes);
+
+ // If the named route and one of the unnamed routes also matches, then we have an ambiguity.
+ if (namedRoutePathData != null && pathData != null)
+ {
+ var message = Resources.FormatNamedRoutes_AmbiguousRoutesFound(context.RouteName);
+ throw new InvalidOperationException(message);
+ }
+
+ return NormalizeVirtualPath(namedRoutePathData ?? pathData);
+ }
+ else
+ {
+ return NormalizeVirtualPath(GetVirtualPath(context, _routes));
+ }
+ }
+
+ private VirtualPathData GetVirtualPath(VirtualPathContext context, List<IRouter> routes)
+ {
+ for (var i = 0; i < routes.Count; i++)
+ {
+ var route = routes[i];
+
+ var pathData = route.GetVirtualPath(context);
+ if (pathData != null)
+ {
+ return pathData;
+ }
+ }
+
+ return null;
+ }
+
+ private VirtualPathData NormalizeVirtualPath(VirtualPathData pathData)
+ {
+ if (pathData == null)
+ {
+ return pathData;
+ }
+
+ var url = pathData.VirtualPath;
+
+ if (!string.IsNullOrEmpty(url) && (_options.LowercaseUrls || _options.AppendTrailingSlash))
+ {
+ var indexOfSeparator = url.IndexOfAny(UrlQueryDelimiters);
+ var urlWithoutQueryString = url;
+ var queryString = string.Empty;
+
+ if (indexOfSeparator != -1)
+ {
+ urlWithoutQueryString = url.Substring(0, indexOfSeparator);
+ queryString = url.Substring(indexOfSeparator);
+ }
+
+ if (_options.LowercaseUrls)
+ {
+ urlWithoutQueryString = urlWithoutQueryString.ToLowerInvariant();
+ }
+
+ if (_options.AppendTrailingSlash && !urlWithoutQueryString.EndsWith("/"))
+ {
+ urlWithoutQueryString += "/";
+ }
+
+ // queryString will contain the delimiter ? or # as the first character, so it's safe to append.
+ url = urlWithoutQueryString + queryString;
+
+ return new VirtualPathData(pathData.Router, url, pathData.DataTokens);
+ }
+
+ return pathData;
+ }
+
+ private void EnsureOptions(HttpContext context)
+ {
+ if (_options == null)
+ {
+ _options = context.RequestServices.GetRequiredService<IOptions<RouteOptions>>().Value;
+ }
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs
new file mode 100644
index 0000000000..c230ed0e96
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs
@@ -0,0 +1,191 @@
+// 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 Microsoft.AspNetCore.Routing.Constraints;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ /// <summary>
+ /// A builder for produding a mapping of keys to see <see cref="IRouteConstraint"/>.
+ /// </summary>
+ /// <remarks>
+ /// <see cref="RouteConstraintBuilder"/> allows iterative building a set of route constraints, and will
+ /// merge multiple entries for the same key.
+ /// </remarks>
+ public class RouteConstraintBuilder
+ {
+ private readonly IInlineConstraintResolver _inlineConstraintResolver;
+ private readonly string _displayName;
+
+ private readonly Dictionary<string, List<IRouteConstraint>> _constraints;
+ private readonly HashSet<string> _optionalParameters;
+ /// <summary>
+ /// Creates a new <see cref="RouteConstraintBuilder"/> instance.
+ /// </summary>
+ /// <param name="inlineConstraintResolver">The <see cref="IInlineConstraintResolver"/>.</param>
+ /// <param name="displayName">The display name (for use in error messages).</param>
+ public RouteConstraintBuilder(
+ IInlineConstraintResolver inlineConstraintResolver,
+ string displayName)
+ {
+ if (inlineConstraintResolver == null)
+ {
+ throw new ArgumentNullException(nameof(inlineConstraintResolver));
+ }
+
+ if (displayName == null)
+ {
+ throw new ArgumentNullException(nameof(displayName));
+ }
+
+ _inlineConstraintResolver = inlineConstraintResolver;
+ _displayName = displayName;
+
+ _constraints = new Dictionary<string, List<IRouteConstraint>>(StringComparer.OrdinalIgnoreCase);
+ _optionalParameters = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Builds a mapping of constraints.
+ /// </summary>
+ /// <returns>An <see cref="IDictionary{String, IRouteConstraint}"/> of the constraints.</returns>
+ public IDictionary<string, IRouteConstraint> Build()
+ {
+ var constraints = new Dictionary<string, IRouteConstraint>(StringComparer.OrdinalIgnoreCase);
+ foreach (var kvp in _constraints)
+ {
+ IRouteConstraint constraint;
+ if (kvp.Value.Count == 1)
+ {
+ constraint = kvp.Value[0];
+ }
+ else
+ {
+ constraint = new CompositeRouteConstraint(kvp.Value.ToArray());
+ }
+
+ if (_optionalParameters.Contains(kvp.Key))
+ {
+ var optionalConstraint = new OptionalRouteConstraint(constraint);
+ constraints.Add(kvp.Key, optionalConstraint);
+ }
+ else
+ {
+ constraints.Add(kvp.Key, constraint);
+ }
+ }
+
+ return constraints;
+ }
+
+ /// <summary>
+ /// Adds a constraint instance for the given key.
+ /// </summary>
+ /// <param name="key">The key.</param>
+ /// <param name="value">
+ /// The constraint instance. Must either be a string or an instance of <see cref="IRouteConstraint"/>.
+ /// </param>
+ /// <remarks>
+ /// If the <paramref name="value"/> is a string, it will be converted to a <see cref="RegexRouteConstraint"/>.
+ ///
+ /// For example, the string <code>Product[0-9]+</code> will be converted to the regular expression
+ /// <code>^(Product[0-9]+)</code>. See <see cref="System.Text.RegularExpressions.Regex"/> for more details.
+ /// </remarks>
+ public void AddConstraint(string key, object value)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ var constraint = value as IRouteConstraint;
+ if (constraint == null)
+ {
+ var regexPattern = value as string;
+ if (regexPattern == null)
+ {
+ throw new RouteCreationException(
+ Resources.FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint(
+ key,
+ value,
+ _displayName,
+ typeof(IRouteConstraint)));
+ }
+
+ var constraintsRegEx = "^(" + regexPattern + ")$";
+ constraint = new RegexRouteConstraint(constraintsRegEx);
+ }
+
+ Add(key, constraint);
+ }
+
+ /// <summary>
+ /// Adds a constraint for the given key, resolved by the <see cref="IInlineConstraintResolver"/>.
+ /// </summary>
+ /// <param name="key">The key.</param>
+ /// <param name="constraintText">The text to be resolved by <see cref="IInlineConstraintResolver"/>.</param>
+ /// <remarks>
+ /// The <see cref="IInlineConstraintResolver"/> can create <see cref="IRouteConstraint"/> instances
+ /// based on <paramref name="constraintText"/>. See <see cref="RouteOptions.ConstraintMap"/> to register
+ /// custom constraint types.
+ /// </remarks>
+ public void AddResolvedConstraint(string key, string constraintText)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (constraintText == null)
+ {
+ throw new ArgumentNullException(nameof(constraintText));
+ }
+
+ var constraint = _inlineConstraintResolver.ResolveConstraint(constraintText);
+ if (constraint == null)
+ {
+ throw new InvalidOperationException(
+ Resources.FormatRouteConstraintBuilder_CouldNotResolveConstraint(
+ key,
+ constraintText,
+ _displayName,
+ _inlineConstraintResolver.GetType().Name));
+ }
+
+ Add(key, constraint);
+ }
+
+ /// <summary>
+ /// Sets the given key as optional.
+ /// </summary>
+ /// <param name="key">The key.</param>
+ public void SetOptional(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ _optionalParameters.Add(key);
+ }
+
+ private void Add(string key, IRouteConstraint constraint)
+ {
+ List<IRouteConstraint> list;
+ if (!_constraints.TryGetValue(key, out list))
+ {
+ list = new List<IRouteConstraint>();
+ _constraints.Add(key, list);
+ }
+
+ list.Add(constraint);
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintMatcher.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintMatcher.cs
new file mode 100644
index 0000000000..8601d85581
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteConstraintMatcher.cs
@@ -0,0 +1,67 @@
+// 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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Logging;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public static class RouteConstraintMatcher
+ {
+ public static bool Match(
+ IDictionary<string, IRouteConstraint> constraints,
+ RouteValueDictionary routeValues,
+ HttpContext httpContext,
+ IRouter route,
+ RouteDirection routeDirection,
+ ILogger logger)
+ {
+ if (routeValues == null)
+ {
+ throw new ArgumentNullException(nameof(routeValues));
+ }
+
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException(nameof(httpContext));
+ }
+
+ if (route == null)
+ {
+ throw new ArgumentNullException(nameof(route));
+ }
+
+ if (logger == null)
+ {
+ throw new ArgumentNullException(nameof(logger));
+ }
+
+ if (constraints == null || constraints.Count == 0)
+ {
+ return true;
+ }
+
+ foreach (var kvp in constraints)
+ {
+ var constraint = kvp.Value;
+ if (!constraint.Match(httpContext, route, kvp.Key, routeValues, routeDirection))
+ {
+ if (routeDirection.Equals(RouteDirection.IncomingRequest))
+ {
+ object routeValue;
+ routeValues.TryGetValue(kvp.Key, out routeValue);
+
+ logger.RouteValueDoesNotMatchConstraint(routeValue, kvp.Key, kvp.Value);
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteCreationException.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteCreationException.cs
new file mode 100644
index 0000000000..0c47e7e412
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteCreationException.cs
@@ -0,0 +1,33 @@
+// 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.Routing
+{
+ /// <summary>
+ /// The exception that is thrown for invalid routes or constraints.
+ /// </summary>
+ public class RouteCreationException : Exception
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RouteCreationException"/> class with a specified error message.
+ /// </summary>
+ /// <param name="message">The message that describes the error.</param>
+ public RouteCreationException(string message)
+ : base(message)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RouteCreationException"/> class with a specified error message
+ /// and a reference to the inner exception that is the cause of this exception.
+ /// </summary>
+ /// <param name="message">The error message that explains the reason for the exception.</param>
+ /// <param name="innerException">The exception that is the cause of the current exception.</param>
+ public RouteCreationException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteHandler.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteHandler.cs
new file mode 100644
index 0000000000..a2fcce601f
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteHandler.cs
@@ -0,0 +1,35 @@
+// 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.Routing
+{
+ public class RouteHandler : IRouteHandler, IRouter
+ {
+ private readonly RequestDelegate _requestDelegate;
+
+ public RouteHandler(RequestDelegate requestDelegate)
+ {
+ _requestDelegate = requestDelegate;
+ }
+
+ public RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData)
+ {
+ return _requestDelegate;
+ }
+
+ public VirtualPathData GetVirtualPath(VirtualPathContext context)
+ {
+ // Nothing to do.
+ return null;
+ }
+
+ public Task RouteAsync(RouteContext context)
+ {
+ context.Handler = _requestDelegate;
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteOptions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteOptions.cs
new file mode 100644
index 0000000000..1ae47ed13f
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteOptions.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 System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Routing.Constraints;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class RouteOptions
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether all generated URLs are lower-case.
+ /// </summary>
+ public bool LowercaseUrls { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether a trailing slash should be appended to the generated URLs.
+ /// </summary>
+ public bool AppendTrailingSlash { get; set; }
+
+ private IDictionary<string, Type> _constraintTypeMap = GetDefaultConstraintMap();
+
+ public IDictionary<string, Type> ConstraintMap
+ {
+ get
+ {
+ return _constraintTypeMap;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(ConstraintMap));
+ }
+
+ _constraintTypeMap = value;
+ }
+ }
+
+ private static IDictionary<string, Type> GetDefaultConstraintMap()
+ {
+ return new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
+ {
+ // Type-specific constraints
+ { "int", typeof(IntRouteConstraint) },
+ { "bool", typeof(BoolRouteConstraint) },
+ { "datetime", typeof(DateTimeRouteConstraint) },
+ { "decimal", typeof(DecimalRouteConstraint) },
+ { "double", typeof(DoubleRouteConstraint) },
+ { "float", typeof(FloatRouteConstraint) },
+ { "guid", typeof(GuidRouteConstraint) },
+ { "long", typeof(LongRouteConstraint) },
+
+ // Length constraints
+ { "minlength", typeof(MinLengthRouteConstraint) },
+ { "maxlength", typeof(MaxLengthRouteConstraint) },
+ { "length", typeof(LengthRouteConstraint) },
+
+ // Min/Max value constraints
+ { "min", typeof(MinRouteConstraint) },
+ { "max", typeof(MaxRouteConstraint) },
+ { "range", typeof(RangeRouteConstraint) },
+
+ // Regex-based constraints
+ { "alpha", typeof(AlphaRouteConstraint) },
+ { "regex", typeof(RegexInlineRouteConstraint) },
+
+ {"required", typeof(RequiredRouteConstraint) },
+ };
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.cs
new file mode 100644
index 0000000000..6f2a1eab45
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouteValueEqualityComparer.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.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ /// <summary>
+ /// An <see cref="IEqualityComparer{Object}"/> implementation that compares objects as-if
+ /// they were route value strings.
+ /// </summary>
+ /// <remarks>
+ /// Values that are are not strings are converted to strings using
+ /// <c>Convert.ToString(x, CultureInfo.InvariantCulture)</c>. <c>null</c> values are converted
+ /// to the empty string.
+ ///
+ /// strings are compared using <see cref="StringComparison.OrdinalIgnoreCase"/>.
+ /// </remarks>
+ public class RouteValueEqualityComparer : IEqualityComparer<object>
+ {
+ /// <inheritdoc />
+ public new bool Equals(object x, object y)
+ {
+ var stringX = x as string ?? Convert.ToString(x, CultureInfo.InvariantCulture);
+ var stringY = y as string ?? Convert.ToString(y, CultureInfo.InvariantCulture);
+
+ if (string.IsNullOrEmpty(stringX) && string.IsNullOrEmpty(stringY))
+ {
+ return true;
+ }
+ else
+ {
+ return string.Equals(stringX, stringY, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
+ /// <inheritdoc />
+ public int GetHashCode(object obj)
+ {
+ var stringObj = obj as string ?? Convert.ToString(obj, CultureInfo.InvariantCulture);
+ if (string.IsNullOrEmpty(stringObj))
+ {
+ return StringComparer.OrdinalIgnoreCase.GetHashCode(string.Empty);
+ }
+ else
+ {
+ return StringComparer.OrdinalIgnoreCase.GetHashCode(stringObj);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RouterMiddleware.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RouterMiddleware.cs
new file mode 100644
index 0000000000..a8256dc5fa
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RouterMiddleware.cs
@@ -0,0 +1,52 @@
+// 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;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Logging;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ public class RouterMiddleware
+ {
+ private readonly ILogger _logger;
+ private readonly RequestDelegate _next;
+ private readonly IRouter _router;
+
+ public RouterMiddleware(
+ RequestDelegate next,
+ ILoggerFactory loggerFactory,
+ IRouter router)
+ {
+ _next = next;
+ _router = router;
+
+ _logger = loggerFactory.CreateLogger<RouterMiddleware>();
+ }
+
+ public async Task Invoke(HttpContext httpContext)
+ {
+ var context = new RouteContext(httpContext);
+ context.RouteData.Routers.Add(_router);
+
+ await _router.RouteAsync(context);
+
+ if (context.Handler == null)
+ {
+ _logger.RequestDidNotMatchRoutes();
+ await _next.Invoke(httpContext);
+ }
+ else
+ {
+ httpContext.Features[typeof(IRoutingFeature)] = new RoutingFeature()
+ {
+ RouteData = context.RouteData,
+ };
+
+ await context.Handler(context.HttpContext);
+ }
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RoutingBuilderExtensions.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RoutingBuilderExtensions.cs
new file mode 100644
index 0000000000..cc2e2457e8
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RoutingBuilderExtensions.cs
@@ -0,0 +1,78 @@
+// 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.Routing;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Extension methods for adding the <see cref="RouterMiddleware"/> middleware to an <see cref="IApplicationBuilder"/>.
+ /// </summary>
+ public static class RoutingBuilderExtensions
+ {
+ /// <summary>
+ /// Adds a <see cref="RouterMiddleware"/> middleware to the specified <see cref="IApplicationBuilder"/> with the specified <see cref="IRouter"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
+ /// <param name="router">The <see cref="IRouter"/> to use for routing requests.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public static IApplicationBuilder UseRouter(this IApplicationBuilder builder, IRouter router)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (router == null)
+ {
+ throw new ArgumentNullException(nameof(router));
+ }
+
+ if (builder.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null)
+ {
+ throw new InvalidOperationException(Resources.FormatUnableToFindServices(
+ nameof(IServiceCollection),
+ nameof(RoutingServiceCollectionExtensions.AddRouting),
+ "ConfigureServices(...)"));
+ }
+
+ return builder.UseMiddleware<RouterMiddleware>(router);
+ }
+
+ /// <summary>
+ /// Adds a <see cref="RouterMiddleware"/> middleware to the specified <see cref="IApplicationBuilder"/>
+ /// with the <see cref="IRouter"/> built from configured <see cref="IRouteBuilder"/>.
+ /// </summary>
+ /// <param name="builder">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
+ /// <param name="action">An <see cref="Action{IRouteBuilder}"/> to configure the provided <see cref="IRouteBuilder"/>.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public static IApplicationBuilder UseRouter(this IApplicationBuilder builder, Action<IRouteBuilder> action)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (action == null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ if (builder.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null)
+ {
+ throw new InvalidOperationException(Resources.FormatUnableToFindServices(
+ nameof(IServiceCollection),
+ nameof(RoutingServiceCollectionExtensions.AddRouting),
+ "ConfigureServices(...)"));
+ }
+
+ var routeBuilder = new RouteBuilder(builder);
+ action(routeBuilder);
+
+ return builder.UseRouter(routeBuilder.Build());
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/RoutingFeature.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/RoutingFeature.cs
new file mode 100644
index 0000000000..30b88ac1b9
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/RoutingFeature.cs
@@ -0,0 +1,10 @@
+// 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.Routing
+{
+ public class RoutingFeature : IRoutingFeature
+ {
+ public RouteData RouteData { get; set; }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs
new file mode 100644
index 0000000000..5c50eefb0e
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs
@@ -0,0 +1,32 @@
+// 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.Routing.Template
+{
+ /// <summary>
+ /// The parsed representation of an inline constraint in a route parameter.
+ /// </summary>
+ public class InlineConstraint
+ {
+ /// <summary>
+ /// Creates a new <see cref="InlineConstraint"/>.
+ /// </summary>
+ /// <param name="constraint">The constraint text.</param>
+ public InlineConstraint(string constraint)
+ {
+ if (constraint == null)
+ {
+ throw new ArgumentNullException(nameof(constraint));
+ }
+
+ Constraint = constraint;
+ }
+
+ /// <summary>
+ /// Gets the constraint text.
+ /// </summary>
+ public string Constraint { get; }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs
new file mode 100644
index 0000000000..663f455923
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RoutePrecedence.cs
@@ -0,0 +1,131 @@
+// 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.Linq;
+
+namespace Microsoft.AspNetCore.Routing.Template
+{
+ /// <summary>
+ /// Computes precedence for a route template.
+ /// </summary>
+ public static class RoutePrecedence
+ {
+ // Compute the precedence for matching a provided url
+ // e.g.: /api/template == 1.1
+ // /api/template/{id} == 1.13
+ // /api/{id:int} == 1.2
+ // /api/template/{id:int} == 1.12
+ public static decimal ComputeInbound(RouteTemplate template)
+ {
+ // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
+ // and 4 results in a combined precedence of 2.14 (decimal).
+ var precedence = 0m;
+
+ for (var i = 0; i < template.Segments.Count; i++)
+ {
+ var segment = template.Segments[i];
+
+ var digit = ComputeInboundPrecedenceDigit(segment);
+ Debug.Assert(digit >= 0 && digit < 10);
+
+ precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
+ }
+
+ return precedence;
+ }
+
+ // Compute the precedence for generating a url
+ // e.g.: /api/template == 5.5
+ // /api/template/{id} == 5.53
+ // /api/{id:int} == 5.4
+ // /api/template/{id:int} == 5.54
+ public static decimal ComputeOutbound(RouteTemplate template)
+ {
+ // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
+ // and 4 results in a combined precedence of 2.14 (decimal).
+ var precedence = 0m;
+
+ for (var i = 0; i < template.Segments.Count; i++)
+ {
+ var segment = template.Segments[i];
+
+ var digit = ComputeOutboundPrecedenceDigit(segment);
+ Debug.Assert(digit >= 0 && digit < 10);
+
+ precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
+ }
+
+ return precedence;
+ }
+
+ // Segments have the following order:
+ // 5 - Literal segments
+ // 4 - Multi-part segments && Constrained parameter segments
+ // 3 - Unconstrained parameter segements
+ // 2 - Constrained wildcard parameter segments
+ // 1 - Unconstrained wildcard parameter segments
+ private static int ComputeOutboundPrecedenceDigit(TemplateSegment segment)
+ {
+ if(segment.Parts.Count > 1)
+ {
+ return 4;
+ }
+
+ var part = segment.Parts[0];
+ if(part.IsLiteral)
+ {
+ return 5;
+ }
+ else
+ {
+ Debug.Assert(part.IsParameter);
+ var digit = part.IsCatchAll ? 1 : 3;
+
+ if (part.InlineConstraints != null && part.InlineConstraints.Any())
+ {
+ digit++;
+ }
+
+ return digit;
+ }
+ }
+
+ // Segments have the following order:
+ // 1 - Literal segments
+ // 2 - Constrained parameter segments / Multi-part segments
+ // 3 - Unconstrained parameter segments
+ // 4 - Constrained wildcard parameter segments
+ // 5 - Unconstrained wildcard parameter segments
+ private static int ComputeInboundPrecedenceDigit(TemplateSegment segment)
+ {
+ if (segment.Parts.Count > 1)
+ {
+ // Multi-part segments should appear after literal segments and along with parameter segments
+ return 2;
+ }
+
+ var part = segment.Parts[0];
+ // Literal segments always go first
+ if (part.IsLiteral)
+ {
+ return 1;
+ }
+ else
+ {
+ Debug.Assert(part.IsParameter);
+ var digit = part.IsCatchAll ? 5 : 3;
+
+ // If there is a route constraint for the parameter, reduce order by 1
+ // Constrained parameters end up with order 2, Constrained catch alls end up with order 4
+ if (part.InlineConstraints != null && part.InlineConstraints.Any())
+ {
+ digit--;
+ }
+
+ return digit;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs
new file mode 100644
index 0000000000..d482f3a1b4
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/RouteTemplate.cs
@@ -0,0 +1,82 @@
+// 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.Linq;
+
+namespace Microsoft.AspNetCore.Routing.Template
+{
+ [DebuggerDisplay("{DebuggerToString()}")]
+ public class RouteTemplate
+ {
+ private const string SeparatorString = "/";
+
+ public RouteTemplate(string template, List<TemplateSegment> segments)
+ {
+ if (segments == null)
+ {
+ throw new ArgumentNullException(nameof(segments));
+ }
+
+ TemplateText = template;
+
+ Segments = segments;
+
+ Parameters = new List<TemplatePart>();
+ for (var i = 0; i < segments.Count; i++)
+ {
+ var segment = Segments[i];
+ for (var j = 0; j < segment.Parts.Count; j++)
+ {
+ var part = segment.Parts[j];
+ if (part.IsParameter)
+ {
+ Parameters.Add(part);
+ }
+ }
+ }
+ }
+
+ public string TemplateText { get; }
+
+ public IList<TemplatePart> Parameters { get; }
+
+ public IList<TemplateSegment> Segments { get; }
+
+ public TemplateSegment GetSegment(int index)
+ {
+ if (index < 0)
+ {
+ throw new IndexOutOfRangeException();
+ }
+
+ return index >= Segments.Count ? null : Segments[index];
+ }
+
+ private string DebuggerToString()
+ {
+ return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString()));
+ }
+
+ /// <summary>
+ /// Gets the parameter matching the given name.
+ /// </summary>
+ /// <param name="name">The name of the parameter to match.</param>
+ /// <returns>The matching parameter or <c>null</c> if no parameter matches the given name.</returns>
+ public TemplatePart GetParameter(string name)
+ {
+ for (var i = 0; i < Parameters.Count; i++)
+ {
+ var parameter = Parameters[i];
+ if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase))
+ {
+ return parameter;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs
new file mode 100644
index 0000000000..802352e935
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs
@@ -0,0 +1,459 @@
+// 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;
+using System.Diagnostics;
+using System.Globalization;
+using System.Text.Encodings.Web;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Microsoft.AspNetCore.Routing.Template
+{
+ public class TemplateBinder
+ {
+ private readonly UrlEncoder _urlEncoder;
+ private readonly ObjectPool<UriBuildingContext> _pool;
+
+ private readonly RouteValueDictionary _defaults;
+ private readonly RouteValueDictionary _filters;
+ private readonly RouteTemplate _template;
+
+ /// <summary>
+ /// Creates a new instance of <see cref="TemplateBinder"/>.
+ /// </summary>
+ /// <param name="urlEncoder">The <see cref="UrlEncoder"/>.</param>
+ /// <param name="pool">The <see cref="ObjectPool{T}"/>.</param>
+ /// <param name="template">The <see cref="RouteTemplate"/> to bind values to.</param>
+ /// <param name="defaults">The default values for <paramref name="template"/>.</param>
+ public TemplateBinder(
+ UrlEncoder urlEncoder,
+ ObjectPool<UriBuildingContext> pool,
+ RouteTemplate template,
+ RouteValueDictionary defaults)
+ {
+ if (urlEncoder == null)
+ {
+ throw new ArgumentNullException(nameof(urlEncoder));
+ }
+
+ if (pool == null)
+ {
+ throw new ArgumentNullException(nameof(pool));
+ }
+
+ if (template == null)
+ {
+ throw new ArgumentNullException(nameof(template));
+ }
+
+ _urlEncoder = urlEncoder;
+ _pool = pool;
+ _template = template;
+ _defaults = defaults;
+
+ // Any default that doesn't have a corresponding parameter is a 'filter' and if a value
+ // is provided for that 'filter' it must match the value in defaults.
+ _filters = new RouteValueDictionary(_defaults);
+ foreach (var parameter in _template.Parameters)
+ {
+ _filters.Remove(parameter.Name);
+ }
+ }
+
+ // Step 1: Get the list of values we're going to try to use to match and generate this URI
+ public TemplateValuesResult GetValues(RouteValueDictionary ambientValues, RouteValueDictionary values)
+ {
+ var context = new TemplateBindingContext(_defaults);
+
+ // Find out which entries in the URI are valid for the URI we want to generate.
+ // If the URI had ordered parameters a="1", b="2", c="3" and the new values
+ // specified that b="9", then we need to invalidate everything after it. The new
+ // values should then be a="1", b="9", c=<no value>.
+ //
+ // We also handle the case where a parameter is optional but has no value - we shouldn't
+ // accept additional parameters that appear *after* that parameter.
+ for (var i = 0; i < _template.Parameters.Count; i++)
+ {
+ var parameter = _template.Parameters[i];
+
+ // If it's a parameter subsegment, examine the current value to see if it matches the new value
+ var parameterName = parameter.Name;
+
+ object newParameterValue;
+ var hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue);
+
+ object currentParameterValue = null;
+ var hasCurrentParameterValue = ambientValues != null &&
+ ambientValues.TryGetValue(parameterName, out currentParameterValue);
+
+ if (hasNewParameterValue && hasCurrentParameterValue)
+ {
+ if (!RoutePartsEqual(currentParameterValue, newParameterValue))
+ {
+ // Stop copying current values when we find one that doesn't match
+ break;
+ }
+ }
+
+ if (!hasNewParameterValue &&
+ !hasCurrentParameterValue &&
+ _defaults?.ContainsKey(parameter.Name) != true)
+ {
+ // This is an unsatisfied parameter value and there are no defaults. We might still
+ // be able to generate a URL but we should stop 'accepting' ambient values.
+ //
+ // This might be a case like:
+ // template: a/{b?}/{c?}
+ // ambient: { c = 17 }
+ // values: { }
+ //
+ // We can still generate a URL from this ("/a") but we shouldn't accept 'c' because
+ // we can't use it.
+ //
+ // In the example above we should fall into this block for 'b'.
+ break;
+ }
+
+ // If the parameter is a match, add it to the list of values we will use for URI generation
+ if (hasNewParameterValue)
+ {
+ if (IsRoutePartNonEmpty(newParameterValue))
+ {
+ context.Accept(parameterName, newParameterValue);
+ }
+ }
+ else
+ {
+ if (hasCurrentParameterValue)
+ {
+ context.Accept(parameterName, currentParameterValue);
+ }
+ }
+ }
+
+ // Add all remaining new values to the list of values we will use for URI generation
+ foreach (var kvp in values)
+ {
+ if (IsRoutePartNonEmpty(kvp.Value))
+ {
+ context.Accept(kvp.Key, kvp.Value);
+ }
+ }
+
+ // Accept all remaining default values if they match a required parameter
+ for (var i = 0; i < _template.Parameters.Count; i++)
+ {
+ var parameter = _template.Parameters[i];
+ if (parameter.IsOptional || parameter.IsCatchAll)
+ {
+ continue;
+ }
+
+ if (context.NeedsValue(parameter.Name))
+ {
+ // Add the default value only if there isn't already a new value for it and
+ // only if it actually has a default value, which we determine based on whether
+ // the parameter value is required.
+ context.AcceptDefault(parameter.Name);
+ }
+ }
+
+ // Validate that all required parameters have a value.
+ for (var i = 0; i < _template.Parameters.Count; i++)
+ {
+ var parameter = _template.Parameters[i];
+ if (parameter.IsOptional || parameter.IsCatchAll)
+ {
+ continue;
+ }
+
+ if (!context.AcceptedValues.ContainsKey(parameter.Name))
+ {
+ // We don't have a value for this parameter, so we can't generate a url.
+ return null;
+ }
+ }
+
+ // Any default values that don't appear as parameters are treated like filters. Any new values
+ // provided must match these defaults.
+ foreach (var filter in _filters)
+ {
+ var parameter = GetParameter(filter.Key);
+ if (parameter != null)
+ {
+ continue;
+ }
+
+ object value;
+ if (values.TryGetValue(filter.Key, out value))
+ {
+ if (!RoutePartsEqual(value, filter.Value))
+ {
+ // If there is a non-parameterized value in the route and there is a
+ // new value for it and it doesn't match, this route won't match.
+ return null;
+ }
+ }
+ }
+
+ // Add any ambient values that don't match parameters - they need to be visible to constraints
+ // but they will ignored by link generation.
+ var combinedValues = new RouteValueDictionary(context.AcceptedValues);
+ if (ambientValues != null)
+ {
+ foreach (var kvp in ambientValues)
+ {
+ if (IsRoutePartNonEmpty(kvp.Value))
+ {
+ var parameter = GetParameter(kvp.Key);
+ if (parameter == null && !context.AcceptedValues.ContainsKey(kvp.Key))
+ {
+ combinedValues.Add(kvp.Key, kvp.Value);
+ }
+ }
+ }
+ }
+
+ return new TemplateValuesResult()
+ {
+ AcceptedValues = context.AcceptedValues,
+ CombinedValues = combinedValues,
+ };
+ }
+
+ // Step 2: If the route is a match generate the appropriate URI
+ public string BindValues(RouteValueDictionary acceptedValues)
+ {
+ var context = _pool.Get();
+ var result = BindValues(context, acceptedValues);
+ _pool.Return(context);
+ return result;
+ }
+
+ private string BindValues(UriBuildingContext context, RouteValueDictionary acceptedValues)
+ {
+ for (var i = 0; i < _template.Segments.Count; i++)
+ {
+ Debug.Assert(context.BufferState == SegmentState.Beginning);
+ Debug.Assert(context.UriState == SegmentState.Beginning);
+
+ var segment = _template.Segments[i];
+
+ for (var j = 0; j < segment.Parts.Count; j++)
+ {
+ var part = segment.Parts[j];
+
+ if (part.IsLiteral)
+ {
+ if (!context.Accept(part.Text))
+ {
+ return null;
+ }
+ }
+ else if (part.IsParameter)
+ {
+ // If it's a parameter, get its value
+ object value;
+ var hasValue = acceptedValues.TryGetValue(part.Name, out value);
+ if (hasValue)
+ {
+ acceptedValues.Remove(part.Name);
+ }
+
+ var isSameAsDefault = false;
+ object defaultValue;
+ if (_defaults != null && _defaults.TryGetValue(part.Name, out defaultValue))
+ {
+ if (RoutePartsEqual(value, defaultValue))
+ {
+ isSameAsDefault = true;
+ }
+ }
+
+ var converted = Convert.ToString(value, CultureInfo.InvariantCulture);
+ if (isSameAsDefault)
+ {
+ // If the accepted value is the same as the default value buffer it since
+ // we won't necessarily add it to the URI we generate.
+ if (!context.Buffer(converted))
+ {
+ return null;
+ }
+ }
+ else
+ {
+ // If the value is not accepted, it is null or empty value in the
+ // middle of the segment. We accept this if the parameter is an
+ // optional parameter and it is preceded by an optional seperator.
+ // I this case, we need to remove the optional seperator that we
+ // have added to the URI
+ // Example: template = {id}.{format?}. parameters: id=5
+ // In this case after we have generated "5.", we wont find any value
+ // for format, so we remove '.' and generate 5.
+ if (!context.Accept(converted))
+ {
+ if (j != 0 && part.IsOptional && segment.Parts[j - 1].IsOptionalSeperator)
+ {
+ context.Remove(segment.Parts[j - 1].Text);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+ }
+ }
+
+ context.EndSegment();
+ }
+
+ // Generate the query string from the remaining values
+ var wroteFirst = false;
+ foreach (var kvp in acceptedValues)
+ {
+ if (_defaults != null && _defaults.ContainsKey(kvp.Key))
+ {
+ // This value is a 'filter' we don't need to put it in the query string.
+ continue;
+ }
+
+ var values = kvp.Value as IEnumerable;
+ if (values != null && !(values is string))
+ {
+ foreach (var value in values)
+ {
+ wroteFirst |= AddParameterToContext(context, kvp.Key, value, wroteFirst);
+ }
+ }
+ else
+ {
+ wroteFirst |= AddParameterToContext(context, kvp.Key, kvp.Value, wroteFirst);
+ }
+ }
+ return context.ToString();
+ }
+
+ private bool AddParameterToContext(UriBuildingContext context, string key, object value, bool wroteFirst)
+ {
+ var converted = Convert.ToString(value, CultureInfo.InvariantCulture);
+ if (!string.IsNullOrEmpty(converted))
+ {
+ context.Writer.Write(wroteFirst ? '&' : '?');
+ _urlEncoder.Encode(context.Writer, key);
+ context.Writer.Write('=');
+ _urlEncoder.Encode(context.Writer, converted);
+ return true;
+ }
+ return false;
+ }
+
+ private TemplatePart GetParameter(string name)
+ {
+ for (var i = 0; i < _template.Parameters.Count; i++)
+ {
+ var parameter = _template.Parameters[i];
+ if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase))
+ {
+ return parameter;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Compares two objects for equality as parts of a case-insensitive path.
+ /// </summary>
+ /// <param name="a">An object to compare.</param>
+ /// <param name="b">An object to compare.</param>
+ /// <returns>True if the object are equal, otherwise false.</returns>
+ public static bool RoutePartsEqual(object a, object b)
+ {
+ var sa = a as string;
+ var sb = b as string;
+
+ if (sa != null && sb != null)
+ {
+ // For strings do a case-insensitive comparison
+ return string.Equals(sa, sb, StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ if (a != null && b != null)
+ {
+ // Explicitly call .Equals() in case it is overridden in the type
+ return a.Equals(b);
+ }
+ else
+ {
+ // At least one of them is null. Return true if they both are
+ return a == b;
+ }
+ }
+ }
+
+ private static bool IsRoutePartNonEmpty(object routePart)
+ {
+ var routePartString = routePart as string;
+ if (routePartString == null)
+ {
+ return routePart != null;
+ }
+ else
+ {
+ return routePartString.Length > 0;
+ }
+ }
+
+ [DebuggerDisplay("{DebuggerToString(),nq}")]
+ private struct TemplateBindingContext
+ {
+ private readonly RouteValueDictionary _defaults;
+ private readonly RouteValueDictionary _acceptedValues;
+
+ public TemplateBindingContext(RouteValueDictionary defaults)
+ {
+ _defaults = defaults;
+
+ _acceptedValues = new RouteValueDictionary();
+ }
+
+ public RouteValueDictionary AcceptedValues
+ {
+ get { return _acceptedValues; }
+ }
+
+ public void Accept(string key, object value)
+ {
+ if (!_acceptedValues.ContainsKey(key))
+ {
+ _acceptedValues.Add(key, value);
+ }
+ }
+
+ public void AcceptDefault(string key)
+ {
+ Debug.Assert(!_acceptedValues.ContainsKey(key));
+
+ object value;
+ if (_defaults != null && _defaults.TryGetValue(key, out value))
+ {
+ _acceptedValues.Add(key, value);
+ }
+ }
+
+ public bool NeedsValue(string key)
+ {
+ return !_acceptedValues.ContainsKey(key);
+ }
+
+ private string DebuggerToString()
+ {
+ return string.Format("{{Accepted: '{0}'}}", string.Join(", ", _acceptedValues.Keys));
+ }
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs
new file mode 100644
index 0000000000..e7cc2ab9f5
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateMatcher.cs
@@ -0,0 +1,456 @@
+// 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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Internal;
+
+namespace Microsoft.AspNetCore.Routing.Template
+{
+ public class TemplateMatcher
+ {
+ private const string SeparatorString = "/";
+ private const char SeparatorChar = '/';
+
+ // Perf: This is a cache to avoid looking things up in 'Defaults' each request.
+ private readonly bool[] _hasDefaultValue;
+ private readonly object[] _defaultValues;
+
+ private static readonly char[] Delimiters = new char[] { SeparatorChar };
+
+ public TemplateMatcher(
+ RouteTemplate template,
+ RouteValueDictionary defaults)
+ {
+ if (template == null)
+ {
+ throw new ArgumentNullException(nameof(template));
+ }
+
+ Template = template;
+ Defaults = defaults ?? new RouteValueDictionary();
+
+ // Perf: cache the default value for each parameter (other than complex segments).
+ _hasDefaultValue = new bool[Template.Segments.Count];
+ _defaultValues = new object[Template.Segments.Count];
+
+ for (var i = 0; i < Template.Segments.Count; i++)
+ {
+ var segment = Template.Segments[i];
+ if (!segment.IsSimple)
+ {
+ continue;
+ }
+
+ var part = segment.Parts[0];
+ if (!part.IsParameter)
+ {
+ continue;
+ }
+
+ object value;
+ if (Defaults.TryGetValue(part.Name, out value))
+ {
+ _hasDefaultValue[i] = true;
+ _defaultValues[i] = value;
+ }
+ }
+ }
+
+ public RouteValueDictionary Defaults { get; }
+
+ public RouteTemplate Template { get; }
+
+ public bool TryMatch(PathString path, RouteValueDictionary values)
+ {
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ var i = 0;
+ var pathTokenizer = new PathTokenizer(path);
+
+ // Perf: We do a traversal of the request-segments + route-segments twice.
+ //
+ // For most segment-types, we only really need to any work on one of the two passes.
+ //
+ // On the first pass, we're just looking to see if there's anything that would disqualify us from matching.
+ // The most common case would be a literal segment that doesn't match.
+ //
+ // On the second pass, we're almost certainly going to match the URL, so go ahead and allocate the 'values'
+ // and start capturing strings.
+ foreach (var pathSegment in pathTokenizer)
+ {
+ if (pathSegment.Length == 0)
+ {
+ return false;
+ }
+
+ var routeSegment = Template.GetSegment(i++);
+ if (routeSegment == null && pathSegment.Length > 0)
+ {
+ // If routeSegment is null, then we're out of route segments. All we can match is the empty
+ // string.
+ return false;
+ }
+ else if (routeSegment.IsSimple && routeSegment.Parts[0].IsLiteral)
+ {
+ // This is a literal segment, so we need to match the text, or the route isn't a match.
+ var part = routeSegment.Parts[0];
+ if (!pathSegment.Equals(part.Text, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+ else if (routeSegment.IsSimple && routeSegment.Parts[0].IsCatchAll)
+ {
+ // Nothing to validate for a catch-all - it can match any string, including the empty string.
+ //
+ // Also, a catch-all has to be the last part, so we're done.
+ break;
+ }
+ else if (routeSegment.IsSimple && routeSegment.Parts[0].IsParameter)
+ {
+ // For a parameter, validate that it's a has some length, or we have a default, or it's optional.
+ var part = routeSegment.Parts[0];
+ if (pathSegment.Length == 0 &&
+ !_hasDefaultValue[i] &&
+ !part.IsOptional)
+ {
+ // There's no value for this parameter, the route can't match.
+ return false;
+ }
+ }
+ else
+ {
+ Debug.Assert(!routeSegment.IsSimple);
+
+ // Don't attempt to validate a complex segment at this point other than being non-emtpy,
+ // do it in the second pass.
+ }
+ }
+
+ for (; i < Template.Segments.Count; i++)
+ {
+ // We've matched the request path so far, but still have remaining route segments. These need
+ // to be all single-part parameter segments with default values or else they won't match.
+ var routeSegment = Template.GetSegment(i);
+ Debug.Assert(routeSegment != null);
+
+ if (!routeSegment.IsSimple)
+ {
+ // If the segment is a complex segment, it MUST contain literals, and we've parsed the full
+ // path so far, so it can't match.
+ return false;
+ }
+
+ var part = routeSegment.Parts[0];
+ if (part.IsLiteral)
+ {
+ // If the segment is a simple literal - which need the URL to provide a value, so we don't match.
+ return false;
+ }
+
+ if (part.IsCatchAll)
+ {
+ // Nothing to validate for a catch-all - it can match any string, including the empty string.
+ //
+ // Also, a catch-all has to be the last part, so we're done.
+ break;
+ }
+
+ // If we get here, this is a simple segment with a parameter. We need it to be optional, or for the
+ // defaults to have a value.
+ Debug.Assert(routeSegment.IsSimple && part.IsParameter);
+ if (!_hasDefaultValue[i] && !part.IsOptional)
+ {
+ // There's no default for this (non-optional) parameter so it can't match.
+ return false;
+ }
+ }
+
+ // At this point we've very likely got a match, so start capturing values for real.
+
+ i = 0;
+ foreach (var requestSegment in pathTokenizer)
+ {
+ var routeSegment = Template.GetSegment(i++);
+
+ if (routeSegment.IsSimple && routeSegment.Parts[0].IsCatchAll)
+ {
+ // A catch-all captures til the end of the string.
+ var part = routeSegment.Parts[0];
+ var captured = requestSegment.Buffer.Substring(requestSegment.Offset);
+ if (captured.Length > 0)
+ {
+ values[part.Name] = captured;
+ }
+ else
+ {
+ // It's ok for a catch-all to produce a null value, so we don't check _hasDefaultValue.
+ values[part.Name] = _defaultValues[i];
+ }
+
+ // A catch-all has to be the last part, so we're done.
+ break;
+ }
+ else if (routeSegment.IsSimple && routeSegment.Parts[0].IsParameter)
+ {
+ // A simple parameter captures the whole segment, or a default value if nothing was
+ // provided.
+ var part = routeSegment.Parts[0];
+ if (requestSegment.Length > 0)
+ {
+ values[part.Name] = requestSegment.ToString();
+ }
+ else
+ {
+ if (_hasDefaultValue[i])
+ {
+ values[part.Name] = _defaultValues[i];
+ }
+ }
+ }
+ else if (!routeSegment.IsSimple)
+ {
+ if (!MatchComplexSegment(routeSegment, requestSegment.ToString(), Defaults, values))
+ {
+ return false;
+ }
+ }
+ }
+
+ for (; i < Template.Segments.Count; i++)
+ {
+ // We've matched the request path so far, but still have remaining route segments. We already know these
+ // are simple parameters that either have a default, or don't need to produce a value.
+ var routeSegment = Template.GetSegment(i);
+ Debug.Assert(routeSegment != null);
+ Debug.Assert(routeSegment.IsSimple);
+
+ var part = routeSegment.Parts[0];
+ Debug.Assert(part.IsParameter);
+
+ // It's ok for a catch-all to produce a null value
+ if (_hasDefaultValue[i] || part.IsCatchAll)
+ {
+ // Don't replace an existing value with a null.
+ var defaultValue = _defaultValues[i];
+ if (defaultValue != null || !values.ContainsKey(part.Name))
+ {
+ values[part.Name] = defaultValue;
+ }
+ }
+ }
+
+ // Copy all remaining default values to the route data
+ foreach (var kvp in Defaults)
+ {
+ if (!values.ContainsKey(kvp.Key))
+ {
+ values.Add(kvp.Key, kvp.Value);
+ }
+ }
+
+ return true;
+ }
+
+ private bool MatchComplexSegment(
+ TemplateSegment routeSegment,
+ string requestSegment,
+ IReadOnlyDictionary<string, object> defaults,
+ RouteValueDictionary values)
+ {
+ var indexOfLastSegment = routeSegment.Parts.Count - 1;
+
+ // We match the request to the template starting at the rightmost parameter
+ // If the last segment of template is optional, then request can match the
+ // template with or without the last parameter. So we start with regular matching,
+ // but if it doesn't match, we start with next to last parameter. Example:
+ // Template: {p1}/{p2}.{p3?}. If the request is one/two.three it will match right away
+ // giving p3 value of three. But if the request is one/two, we start matching from the
+ // rightmost giving p3 the value of two, then we end up not matching the segment.
+ // In this case we start again from p2 to match the request and we succeed giving
+ // the value two to p2
+ if (routeSegment.Parts[indexOfLastSegment].IsOptional &&
+ routeSegment.Parts[indexOfLastSegment - 1].IsOptionalSeperator)
+ {
+ if (MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment))
+ {
+ return true;
+ }
+ else
+ {
+ if (requestSegment.EndsWith(
+ routeSegment.Parts[indexOfLastSegment - 1].Text,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ return MatchComplexSegmentCore(
+ routeSegment,
+ requestSegment,
+ Defaults,
+ values,
+ indexOfLastSegment - 2);
+ }
+ }
+ else
+ {
+ return MatchComplexSegmentCore(routeSegment, requestSegment, Defaults, values, indexOfLastSegment);
+ }
+ }
+
+ private bool MatchComplexSegmentCore(
+ TemplateSegment routeSegment,
+ string requestSegment,
+ IReadOnlyDictionary<string, object> defaults,
+ RouteValueDictionary values,
+ int indexOfLastSegmentUsed)
+ {
+ Debug.Assert(routeSegment != null);
+ Debug.Assert(routeSegment.Parts.Count > 1);
+
+ // Find last literal segment and get its last index in the string
+ var lastIndex = requestSegment.Length;
+
+ TemplatePart parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value
+ TemplatePart lastLiteral = null; // Keeps track of the left-most literal we've encountered
+
+ var outValues = new RouteValueDictionary();
+
+ while (indexOfLastSegmentUsed >= 0)
+ {
+ var newLastIndex = lastIndex;
+
+ var part = routeSegment.Parts[indexOfLastSegmentUsed];
+ if (part.IsParameter)
+ {
+ // Hold on to the parameter so that we can fill it in when we locate the next literal
+ parameterNeedsValue = part;
+ }
+ else
+ {
+ Debug.Assert(part.IsLiteral);
+ lastLiteral = part;
+
+ var startIndex = lastIndex - 1;
+ // If we have a pending parameter subsegment, we must leave at least one character for that
+ if (parameterNeedsValue != null)
+ {
+ startIndex--;
+ }
+
+ if (startIndex < 0)
+ {
+ return false;
+ }
+
+ var indexOfLiteral = requestSegment.LastIndexOf(
+ part.Text,
+ startIndex,
+ StringComparison.OrdinalIgnoreCase);
+ if (indexOfLiteral == -1)
+ {
+ // If we couldn't find this literal index, this segment cannot match
+ return false;
+ }
+
+ // If the first subsegment is a literal, it must match at the right-most extent of the request URI.
+ // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/".
+ // This check is related to the check we do at the very end of this function.
+ if (indexOfLastSegmentUsed == (routeSegment.Parts.Count - 1))
+ {
+ if ((indexOfLiteral + part.Text.Length) != requestSegment.Length)
+ {
+ return false;
+ }
+ }
+
+ newLastIndex = indexOfLiteral;
+ }
+
+ if ((parameterNeedsValue != null) &&
+ (((lastLiteral != null) && (part.IsLiteral)) || (indexOfLastSegmentUsed == 0)))
+ {
+ // If we have a pending parameter that needs a value, grab that value
+
+ int parameterStartIndex;
+ int parameterTextLength;
+
+ if (lastLiteral == null)
+ {
+ if (indexOfLastSegmentUsed == 0)
+ {
+ parameterStartIndex = 0;
+ }
+ else
+ {
+ parameterStartIndex = newLastIndex;
+ Debug.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above");
+ }
+ parameterTextLength = lastIndex;
+ }
+ else
+ {
+ // If we're getting a value for a parameter that is somewhere in the middle of the segment
+ if ((indexOfLastSegmentUsed == 0) && (part.IsParameter))
+ {
+ parameterStartIndex = 0;
+ parameterTextLength = lastIndex;
+ }
+ else
+ {
+ parameterStartIndex = newLastIndex + lastLiteral.Text.Length;
+ parameterTextLength = lastIndex - parameterStartIndex;
+ }
+ }
+
+ var parameterValueString = requestSegment.Substring(parameterStartIndex, parameterTextLength);
+
+ if (string.IsNullOrEmpty(parameterValueString))
+ {
+ // If we're here that means we have a segment that contains multiple sub-segments.
+ // For these segments all parameters must have non-empty values. If the parameter
+ // has an empty value it's not a match.
+ return false;
+
+ }
+ else
+ {
+ // If there's a value in the segment for this parameter, use the subsegment value
+ outValues.Add(parameterNeedsValue.Name, parameterValueString);
+ }
+
+ parameterNeedsValue = null;
+ lastLiteral = null;
+ }
+
+ lastIndex = newLastIndex;
+ indexOfLastSegmentUsed--;
+ }
+
+ // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of
+ // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment
+ // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching
+ // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire*
+ // request URI in order for it to be a match.
+ // This check is related to the check we do earlier in this function for LiteralSubsegments.
+ if (lastIndex == 0 || routeSegment.Parts[0].IsParameter)
+ {
+ foreach (var item in outValues)
+ {
+ values.Add(item.Key, item.Value);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs
new file mode 100644
index 0000000000..0168b22a4b
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateParser.cs
@@ -0,0 +1,540 @@
+// 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.Globalization;
+
+namespace Microsoft.AspNetCore.Routing.Template
+{
+ public static class TemplateParser
+ {
+ private const char Separator = '/';
+ private const char OpenBrace = '{';
+ private const char CloseBrace = '}';
+ private const char EqualsSign = '=';
+ private const char QuestionMark = '?';
+ private const char Asterisk = '*';
+ private const string PeriodString = ".";
+
+ public static RouteTemplate Parse(string routeTemplate)
+ {
+ if (routeTemplate == null)
+ {
+ routeTemplate = String.Empty;
+ }
+
+ var trimmedRouteTemplate = TrimPrefix(routeTemplate);
+
+ var context = new TemplateParserContext(trimmedRouteTemplate);
+ var segments = new List<TemplateSegment>();
+
+ while (context.Next())
+ {
+ if (context.Current == Separator)
+ {
+ // If we get here is means that there's a consecutive '/' character.
+ // Templates don't start with a '/' and parsing a segment consumes the separator.
+ throw new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators,
+ nameof(routeTemplate));
+ }
+ else
+ {
+ if (!ParseSegment(context, segments))
+ {
+ throw new ArgumentException(context.Error, nameof(routeTemplate));
+ }
+ }
+ }
+
+ if (IsAllValid(context, segments))
+ {
+ return new RouteTemplate(routeTemplate, segments);
+ }
+ else
+ {
+ throw new ArgumentException(context.Error, nameof(routeTemplate));
+ }
+ }
+
+ private static string TrimPrefix(string routeTemplate)
+ {
+ if (routeTemplate.StartsWith("~/", StringComparison.Ordinal))
+ {
+ return routeTemplate.Substring(2);
+ }
+ else if (routeTemplate.StartsWith("/", StringComparison.Ordinal))
+ {
+ return routeTemplate.Substring(1);
+ }
+ else if (routeTemplate.StartsWith("~", StringComparison.Ordinal))
+ {
+ throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, nameof(routeTemplate));
+ }
+ return routeTemplate;
+ }
+
+ private static bool ParseSegment(TemplateParserContext context, List<TemplateSegment> segments)
+ {
+ Debug.Assert(context != null);
+ Debug.Assert(segments != null);
+
+ var segment = new TemplateSegment();
+
+ while (true)
+ {
+ if (context.Current == OpenBrace)
+ {
+ if (!context.Next())
+ {
+ // This is a dangling open-brace, which is not allowed
+ context.Error = Resources.TemplateRoute_MismatchedParameter;
+ return false;
+ }
+
+ if (context.Current == OpenBrace)
+ {
+ // This is an 'escaped' brace in a literal, like "{{foo"
+ context.Back();
+ if (!ParseLiteral(context, segment))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ // This is the inside of a parameter
+ if (!ParseParameter(context, segment))
+ {
+ return false;
+ }
+ }
+ }
+ else if (context.Current == Separator)
+ {
+ // We've reached the end of the segment
+ break;
+ }
+ else
+ {
+ if (!ParseLiteral(context, segment))
+ {
+ return false;
+ }
+ }
+
+ if (!context.Next())
+ {
+ // We've reached the end of the string
+ break;
+ }
+ }
+
+ if (IsSegmentValid(context, segment))
+ {
+ segments.Add(segment);
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ private static bool ParseParameter(TemplateParserContext context, TemplateSegment segment)
+ {
+ context.Mark();
+
+ while (true)
+ {
+ if (context.Current == OpenBrace)
+ {
+ // This is an open brace inside of a parameter, it has to be escaped
+ if (context.Next())
+ {
+ if (context.Current != OpenBrace)
+ {
+ // If we see something like "{p1:regex(^\d{3", we will come here.
+ context.Error = Resources.TemplateRoute_UnescapedBrace;
+ return false;
+ }
+ }
+ else
+ {
+ // This is a dangling open-brace, which is not allowed
+ // Example: "{p1:regex(^\d{"
+ context.Error = Resources.TemplateRoute_MismatchedParameter;
+ return false;
+ }
+ }
+ else if (context.Current == CloseBrace)
+ {
+ // When we encounter Closed brace here, it either means end of the parameter or it is a closed
+ // brace in the parameter, in that case it needs to be escaped.
+ // Example: {p1:regex(([}}])\w+}. First pair is escaped one and last marks end of the parameter
+ if (!context.Next())
+ {
+ // This is the end of the string -and we have a valid parameter
+ context.Back();
+ break;
+ }
+
+ if (context.Current == CloseBrace)
+ {
+ // This is an 'escaped' brace in a parameter name
+ }
+ else
+ {
+ // This is the end of the parameter
+ context.Back();
+ break;
+ }
+ }
+
+ if (!context.Next())
+ {
+ // This is a dangling open-brace, which is not allowed
+ context.Error = Resources.TemplateRoute_MismatchedParameter;
+ return false;
+ }
+ }
+
+ var rawParameter = context.Capture();
+ var decoded = rawParameter.Replace("}}", "}").Replace("{{", "{");
+
+ // At this point, we need to parse the raw name for inline constraint,
+ // default values and optional parameters.
+ var templatePart = InlineRouteParameterParser.ParseRouteParameter(decoded);
+
+ if (templatePart.IsCatchAll && templatePart.IsOptional)
+ {
+ context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional;
+ return false;
+ }
+
+ if (templatePart.IsOptional && templatePart.DefaultValue != null)
+ {
+ // Cannot be optional and have a default value.
+ // The only way to declare an optional parameter is to have a ? at the end,
+ // hence we cannot have both default value and optional parameter within the template.
+ // A workaround is to add it as a separate entry in the defaults argument.
+ context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue;
+ return false;
+ }
+
+ var parameterName = templatePart.Name;
+ if (IsValidParameterName(context, parameterName))
+ {
+ segment.Parts.Add(templatePart);
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ private static bool ParseLiteral(TemplateParserContext context, TemplateSegment segment)
+ {
+ context.Mark();
+
+ string encoded;
+ while (true)
+ {
+ if (context.Current == Separator)
+ {
+ encoded = context.Capture();
+ context.Back();
+ break;
+ }
+ else if (context.Current == OpenBrace)
+ {
+ if (!context.Next())
+ {
+ // This is a dangling open-brace, which is not allowed
+ context.Error = Resources.TemplateRoute_MismatchedParameter;
+ return false;
+ }
+
+ if (context.Current == OpenBrace)
+ {
+ // This is an 'escaped' brace in a literal, like "{{foo" - keep going.
+ }
+ else
+ {
+ // We've just seen the start of a parameter, so back up and return
+ context.Back();
+ encoded = context.Capture();
+ context.Back();
+ break;
+ }
+ }
+ else if (context.Current == CloseBrace)
+ {
+ if (!context.Next())
+ {
+ // This is a dangling close-brace, which is not allowed
+ context.Error = Resources.TemplateRoute_MismatchedParameter;
+ return false;
+ }
+
+ if (context.Current == CloseBrace)
+ {
+ // This is an 'escaped' brace in a literal, like "{{foo" - keep going.
+ }
+ else
+ {
+ // This is an unbalanced close-brace, which is not allowed
+ context.Error = Resources.TemplateRoute_MismatchedParameter;
+ return false;
+ }
+ }
+
+ if (!context.Next())
+ {
+ encoded = context.Capture();
+ break;
+ }
+ }
+
+ var decoded = encoded.Replace("}}", "}").Replace("{{", "{");
+ if (IsValidLiteral(context, decoded))
+ {
+ segment.Parts.Add(TemplatePart.CreateLiteral(decoded));
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ private static bool IsAllValid(TemplateParserContext context, List<TemplateSegment> segments)
+ {
+ // A catch-all parameter must be the last part of the last segment
+ for (var i = 0; i < segments.Count; i++)
+ {
+ var segment = segments[i];
+ for (var j = 0; j < segment.Parts.Count; j++)
+ {
+ var part = segment.Parts[j];
+ if (part.IsParameter &&
+ part.IsCatchAll &&
+ (i != segments.Count - 1 || j != segment.Parts.Count - 1))
+ {
+ context.Error = Resources.TemplateRoute_CatchAllMustBeLast;
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private static bool IsSegmentValid(TemplateParserContext context, TemplateSegment segment)
+ {
+ // If a segment has multiple parts, then it can't contain a catch all.
+ for (var i = 0; i < segment.Parts.Count; i++)
+ {
+ var part = segment.Parts[i];
+ if (part.IsParameter && part.IsCatchAll && segment.Parts.Count > 1)
+ {
+ context.Error = Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment;
+ return false;
+ }
+ }
+
+ // if a segment has multiple parts, then only the last one parameter can be optional
+ // if it is following a optional seperator.
+ for (var i = 0; i < segment.Parts.Count; i++)
+ {
+ var part = segment.Parts[i];
+
+ if (part.IsParameter && part.IsOptional && segment.Parts.Count > 1)
+ {
+ // This optional parameter is the last part in the segment
+ if (i == segment.Parts.Count - 1)
+ {
+ if (!segment.Parts[i - 1].IsLiteral)
+ {
+ // The optional parameter is preceded by something that is not a literal.
+ // Example of error message:
+ // "In the segment '{RouteValue}{param?}', the optional parameter 'param' is preceded
+ // by an invalid segment '{RouteValue}'. Only a period (.) can precede an optional parameter.
+ context.Error = string.Format(
+ Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod,
+ segment.DebuggerToString(),
+ part.Name,
+ segment.Parts[i - 1].DebuggerToString());
+
+ return false;
+ }
+ else if (segment.Parts[i - 1].Text != PeriodString)
+ {
+ // The optional parameter is preceded by a literal other than period.
+ // Example of error message:
+ // "In the segment '{RouteValue}-{param?}', the optional parameter 'param' is preceded
+ // by an invalid segment '-'. Only a period (.) can precede an optional parameter.
+ context.Error = string.Format(
+ Resources.TemplateRoute_OptionalParameterCanbBePrecededByPeriod,
+ segment.DebuggerToString(),
+ part.Name,
+ segment.Parts[i - 1].Text);
+
+ return false;
+ }
+
+ segment.Parts[i - 1].IsOptionalSeperator = true;
+ }
+ else
+ {
+ // This optional parameter is not the last one in the segment
+ // Example:
+ // An optional parameter must be at the end of the segment.In the segment '{RouteValue?})',
+ // optional parameter 'RouteValue' is followed by ')'
+ var nextPart = segment.Parts[i + 1];
+ var invalidPartText = nextPart.IsParameter ? nextPart.Name : nextPart.Text;
+
+ context.Error = string.Format(
+ Resources.TemplateRoute_OptionalParameterHasTobeTheLast,
+ segment.DebuggerToString(),
+ segment.Parts[i].Name,
+ invalidPartText);
+
+ return false;
+ }
+ }
+ }
+
+ // A segment cannot contain two consecutive parameters
+ var isLastSegmentParameter = false;
+ for (var i = 0; i < segment.Parts.Count; i++)
+ {
+ var part = segment.Parts[i];
+ if (part.IsParameter && isLastSegmentParameter)
+ {
+ context.Error = Resources.TemplateRoute_CannotHaveConsecutiveParameters;
+ return false;
+ }
+
+ isLastSegmentParameter = part.IsParameter;
+ }
+
+ return true;
+ }
+
+ private static bool IsValidParameterName(TemplateParserContext context, string parameterName)
+ {
+ if (parameterName.Length == 0)
+ {
+ context.Error = String.Format(CultureInfo.CurrentCulture,
+ Resources.TemplateRoute_InvalidParameterName, parameterName);
+ return false;
+ }
+
+ for (var i = 0; i < parameterName.Length; i++)
+ {
+ var c = parameterName[i];
+ if (c == Separator || c == OpenBrace || c == CloseBrace || c == QuestionMark || c == Asterisk)
+ {
+ context.Error = String.Format(CultureInfo.CurrentCulture,
+ Resources.TemplateRoute_InvalidParameterName, parameterName);
+ return false;
+ }
+ }
+
+ if (!context.ParameterNames.Add(parameterName))
+ {
+ context.Error = String.Format(CultureInfo.CurrentCulture,
+ Resources.TemplateRoute_RepeatedParameter, parameterName);
+ return false;
+ }
+
+ return true;
+ }
+
+ private static bool IsValidLiteral(TemplateParserContext context, string literal)
+ {
+ Debug.Assert(context != null);
+ Debug.Assert(literal != null);
+
+ if (literal.IndexOf(QuestionMark) != -1)
+ {
+ context.Error = String.Format(CultureInfo.CurrentCulture,
+ Resources.TemplateRoute_InvalidLiteral, literal);
+ return false;
+ }
+
+ return true;
+ }
+
+ private static bool IsInvalidRouteTemplate(string routeTemplate)
+ {
+ return routeTemplate.StartsWith("~", StringComparison.Ordinal) ||
+ routeTemplate.StartsWith("/", StringComparison.Ordinal);
+ }
+
+ private class TemplateParserContext
+ {
+ private readonly string _template;
+ private int _index;
+ private int? _mark;
+
+ private HashSet<string> _parameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ public TemplateParserContext(string template)
+ {
+ Debug.Assert(template != null);
+ _template = template;
+
+ _index = -1;
+ }
+
+ public char Current
+ {
+ get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; }
+ }
+
+ public string Error
+ {
+ get;
+ set;
+ }
+
+ public HashSet<string> ParameterNames
+ {
+ get { return _parameterNames; }
+ }
+
+ public bool Back()
+ {
+ return --_index >= 0;
+ }
+
+ public bool Next()
+ {
+ return ++_index < _template.Length;
+ }
+
+ public void Mark()
+ {
+ _mark = _index;
+ }
+
+ public string Capture()
+ {
+ if (_mark.HasValue)
+ {
+ var value = _template.Substring(_mark.Value, _index - _mark.Value);
+ _mark = null;
+ return value;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs
new file mode 100644
index 0000000000..70f588a41a
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs
@@ -0,0 +1,67 @@
+// 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.Linq;
+
+namespace Microsoft.AspNetCore.Routing.Template
+{
+ [DebuggerDisplay("{DebuggerToString()}")]
+ public class TemplatePart
+ {
+ public static TemplatePart CreateLiteral(string text)
+ {
+ return new TemplatePart()
+ {
+ IsLiteral = true,
+ Text = text,
+ };
+ }
+
+ public static TemplatePart CreateParameter(string name,
+ bool isCatchAll,
+ bool isOptional,
+ object defaultValue,
+ IEnumerable<InlineConstraint> inlineConstraints)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ return new TemplatePart()
+ {
+ IsParameter = true,
+ Name = name,
+ IsCatchAll = isCatchAll,
+ IsOptional = isOptional,
+ DefaultValue = defaultValue,
+ InlineConstraints = inlineConstraints ?? Enumerable.Empty<InlineConstraint>(),
+ };
+ }
+
+ public bool IsCatchAll { get; private set; }
+ public bool IsLiteral { get; private set; }
+ public bool IsParameter { get; private set; }
+ public bool IsOptional { get; private set; }
+ public bool IsOptionalSeperator { get; set; }
+ public string Name { get; private set; }
+ public string Text { get; private set; }
+ public object DefaultValue { get; private set; }
+ public IEnumerable<InlineConstraint> InlineConstraints { get; private set; }
+
+ internal string DebuggerToString()
+ {
+ if (IsParameter)
+ {
+ return "{" + (IsCatchAll ? "*" : string.Empty) + Name + (IsOptional ? "?" : string.Empty) + "}";
+ }
+ else
+ {
+ return Text;
+ }
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.cs
new file mode 100644
index 0000000000..4a86526509
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateSegment.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.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace Microsoft.AspNetCore.Routing.Template
+{
+ [DebuggerDisplay("{DebuggerToString()}")]
+ public class TemplateSegment
+ {
+ public bool IsSimple => Parts.Count == 1;
+
+ public List<TemplatePart> Parts { get; } = new List<TemplatePart>();
+
+ internal string DebuggerToString()
+ {
+ return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString()));
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateValuesResult.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateValuesResult.cs
new file mode 100644
index 0000000000..2a7c46398f
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Template/TemplateValuesResult.cs
@@ -0,0 +1,29 @@
+// 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.Routing.Template
+{
+ /// <summary>
+ /// The values used as inputs for constraints and link generation.
+ /// </summary>
+ public class TemplateValuesResult
+ {
+ /// <summary>
+ /// The set of values that will appear in the URL.
+ /// </summary>
+ public RouteValueDictionary AcceptedValues { get; set; }
+
+ /// <summary>
+ /// The set of values that that were supplied for URL generation.
+ /// </summary>
+ /// <remarks>
+ /// This combines implicit (ambient) values from the <see cref="RouteData"/> of the current request
+ /// (if applicable), explictly provided values, and default values for parameters that appear in
+ /// the route template.
+ ///
+ /// Implicit (ambient) values which are invalidated due to changes in values lexically earlier in the
+ /// route template are excluded from this set.
+ /// </remarks>
+ public RouteValueDictionary CombinedValues { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs
new file mode 100644
index 0000000000..57f1b6db7b
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundMatch.cs
@@ -0,0 +1,30 @@
+// 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.Diagnostics;
+using Microsoft.AspNetCore.Routing.Template;
+
+namespace Microsoft.AspNetCore.Routing.Tree
+{
+ /// <summary>
+ /// A candidate route to match incoming URLs in a <see cref="TreeRouter"/>.
+ /// </summary>
+ [DebuggerDisplay("{DebuggerToString(),nq}")]
+ public class InboundMatch
+ {
+ /// <summary>
+ /// Gets or sets the <see cref="InboundRouteEntry"/>.
+ /// </summary>
+ public InboundRouteEntry Entry { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="TemplateMatcher"/>.
+ /// </summary>
+ public TemplateMatcher TemplateMatcher { get; set; }
+
+ private string DebuggerToString()
+ {
+ return TemplateMatcher?.Template?.TemplateText;
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs
new file mode 100644
index 0000000000..7c4a5f0abc
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/InboundRouteEntry.cs
@@ -0,0 +1,56 @@
+// 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 Microsoft.AspNetCore.Routing.Template;
+
+namespace Microsoft.AspNetCore.Routing.Tree
+{
+ /// <summary>
+ /// Used to build an <see cref="TreeRouter"/>. Represents a URL template tha will be used to match incoming
+ /// request URLs.
+ /// </summary>
+ public class InboundRouteEntry
+ {
+ /// <summary>
+ /// Gets or sets the route constraints.
+ /// </summary>
+ public IDictionary<string, IRouteConstraint> Constraints { get; set; }
+
+ /// <summary>
+ /// Gets or sets the route defaults.
+ /// </summary>
+ public RouteValueDictionary Defaults { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="IRouter"/> to invoke when this entry matches.
+ /// </summary>
+ public IRouter Handler { get; set; }
+
+ /// <summary>
+ /// Gets or sets the order of the entry.
+ /// </summary>
+ /// <remarks>
+ /// Entries are ordered first by <see cref="Order"/> (ascending) then by <see cref="Precedence"/> (descending).
+ /// </remarks>
+ public int Order { get; set; }
+
+ /// <summary>
+ /// Gets or sets the precedence of the entry.
+ /// </summary>
+ /// <remarks>
+ /// Entries are ordered first by <see cref="Order"/> (ascending) then by <see cref="Precedence"/> (descending).
+ /// </remarks>
+ public decimal Precedence { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the route.
+ /// </summary>
+ public string RouteName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="RouteTemplate"/>.
+ /// </summary>
+ public RouteTemplate RouteTemplate { get; set; }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundMatch.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundMatch.cs
new file mode 100644
index 0000000000..49980b9912
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundMatch.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.AspNetCore.Routing.Template;
+
+namespace Microsoft.AspNetCore.Routing.Tree
+{
+ /// <summary>
+ /// A candidate match for link generation in a <see cref="TreeRouter"/>.
+ /// </summary>
+ public class OutboundMatch
+ {
+ /// <summary>
+ /// Gets or sets the <see cref="OutboundRouteEntry"/>.
+ /// </summary>
+ public OutboundRouteEntry Entry { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="TemplateBinder"/>.
+ /// </summary>
+ public TemplateBinder TemplateBinder { get; set; }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundRouteEntry.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundRouteEntry.cs
new file mode 100644
index 0000000000..2364c3f350
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/OutboundRouteEntry.cs
@@ -0,0 +1,62 @@
+// 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 Microsoft.AspNetCore.Routing.Template;
+
+namespace Microsoft.AspNetCore.Routing.Tree
+{
+ /// <summary>
+ /// Used to build a <see cref="TreeRouter"/>. Represents a URL template that will be used to generate
+ /// outgoing URLs.
+ /// </summary>
+ public class OutboundRouteEntry
+ {
+ /// <summary>
+ /// Gets or sets the route constraints.
+ /// </summary>
+ public IDictionary<string, IRouteConstraint> Constraints { get; set; }
+
+ /// <summary>
+ /// Gets or sets the route defaults.
+ /// </summary>
+ public RouteValueDictionary Defaults { get; set; }
+
+ /// <summary>
+ /// The <see cref="IRouter"/> to invoke when this entry matches.
+ /// </summary>
+ public IRouter Handler { get; set; }
+
+ /// <summary>
+ /// Gets or sets the order of the entry.
+ /// </summary>
+ /// <remarks>
+ /// Entries are ordered first by <see cref="Order"/> (ascending) then by <see cref="Precedence"/> (descending).
+ /// </remarks>
+ public int Order { get; set; }
+
+ /// <summary>
+ /// Gets or sets the precedence of the template for link generation. A greater value of
+ /// <see cref="Precedence"/> means that an entry is considered first.
+ /// </summary>
+ /// <remarks>
+ /// Entries are ordered first by <see cref="Order"/> (ascending) then by <see cref="Precedence"/> (descending).
+ /// </remarks>
+ public decimal Precedence { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the route.
+ /// </summary>
+ public string RouteName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the set of values that must be present for link genration.
+ /// </summary>
+ public RouteValueDictionary RequiredLinkValues { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="RouteTemplate"/>.
+ /// </summary>
+ public RouteTemplate RouteTemplate { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs
new file mode 100644
index 0000000000..a746e7d170
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs
@@ -0,0 +1,454 @@
+// 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.Linq;
+using System.Text.Encodings.Web;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.AspNetCore.Routing.Template;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Microsoft.AspNetCore.Routing.Tree
+{
+ /// <summary>
+ /// Builder for <see cref="TreeRouter"/> instances.
+ /// </summary>
+ public class TreeRouteBuilder
+ {
+ private readonly ILogger _logger;
+ private readonly ILogger _constraintLogger;
+ private readonly UrlEncoder _urlEncoder;
+ private readonly ObjectPool<UriBuildingContext> _objectPool;
+ private readonly IInlineConstraintResolver _constraintResolver;
+
+ /// <summary>
+ /// <para>
+ /// This constructor is obsolete and will be removed in a future version. The recommended
+ /// alternative is the overload that does not take a UrlEncoder.
+ /// </para>
+ /// <para>Initializes a new instance of <see cref="TreeRouteBuilder"/>.</para>
+ /// </summary>
+ /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
+ /// <param name="urlEncoder">The <see cref="UrlEncoder"/>.</param>
+ /// <param name="objectPool">The <see cref="ObjectPool{UrlBuildingContext}"/>.</param>
+ /// <param name="constraintResolver">The <see cref="IInlineConstraintResolver"/>.</param>
+ [Obsolete("This constructor is obsolete and will be removed in a future version. The recommended " +
+ "alternative is the overload that does not take a UrlEncoder.")]
+ public TreeRouteBuilder(
+ ILoggerFactory loggerFactory,
+ UrlEncoder urlEncoder,
+ ObjectPool<UriBuildingContext> objectPool,
+ IInlineConstraintResolver constraintResolver)
+ : this(loggerFactory, objectPool, constraintResolver)
+ {
+ if (urlEncoder == null)
+ {
+ throw new ArgumentNullException(nameof(urlEncoder));
+ }
+
+ _urlEncoder = urlEncoder;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of <see cref="TreeRouteBuilder"/>.
+ /// </summary>
+ /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
+ /// <param name="objectPool">The <see cref="ObjectPool{UrlBuildingContext}"/>.</param>
+ /// <param name="constraintResolver">The <see cref="IInlineConstraintResolver"/>.</param>
+ public TreeRouteBuilder(
+ ILoggerFactory loggerFactory,
+ ObjectPool<UriBuildingContext> objectPool,
+ IInlineConstraintResolver constraintResolver)
+ {
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ if (objectPool == null)
+ {
+ throw new ArgumentNullException(nameof(objectPool));
+ }
+
+ if (constraintResolver == null)
+ {
+ throw new ArgumentNullException(nameof(constraintResolver));
+ }
+
+ _urlEncoder = UrlEncoder.Default;
+ _objectPool = objectPool;
+ _constraintResolver = constraintResolver;
+
+ _logger = loggerFactory.CreateLogger<TreeRouter>();
+ _constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName);
+ }
+
+ /// <summary>
+ /// Adds a new inbound route to the <see cref="TreeRouter"/>.
+ /// </summary>
+ /// <param name="handler">The <see cref="IRouter"/> for handling the route.</param>
+ /// <param name="routeTemplate">The <see cref="RouteTemplate"/> of the route.</param>
+ /// <param name="routeName">The route name.</param>
+ /// <param name="order">The route order.</param>
+ /// <returns>The <see cref="InboundRouteEntry"/>.</returns>
+ public InboundRouteEntry MapInbound(
+ IRouter handler,
+ RouteTemplate routeTemplate,
+ string routeName,
+ int order)
+ {
+ if (handler == null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ if (routeTemplate == null)
+ {
+ throw new ArgumentNullException(nameof(routeTemplate));
+ }
+
+ var entry = new InboundRouteEntry()
+ {
+ Handler = handler,
+ Order = order,
+ Precedence = RoutePrecedence.ComputeInbound(routeTemplate),
+ RouteName = routeName,
+ RouteTemplate = routeTemplate,
+ };
+
+ var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText);
+ foreach (var parameter in routeTemplate.Parameters)
+ {
+ if (parameter.InlineConstraints != null)
+ {
+ if (parameter.IsOptional)
+ {
+ constraintBuilder.SetOptional(parameter.Name);
+ }
+
+ foreach (var constraint in parameter.InlineConstraints)
+ {
+ constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint);
+ }
+ }
+ }
+
+ entry.Constraints = constraintBuilder.Build();
+
+ entry.Defaults = new RouteValueDictionary();
+ foreach (var parameter in entry.RouteTemplate.Parameters)
+ {
+ if (parameter.DefaultValue != null)
+ {
+ entry.Defaults.Add(parameter.Name, parameter.DefaultValue);
+ }
+ }
+
+ InboundEntries.Add(entry);
+ return entry;
+ }
+
+ /// <summary>
+ /// Adds a new outbound route to the <see cref="TreeRouter"/>.
+ /// </summary>
+ /// <param name="handler">The <see cref="IRouter"/> for handling the link generation.</param>
+ /// <param name="routeTemplate">The <see cref="RouteTemplate"/> of the route.</param>
+ /// <param name="requiredLinkValues">The <see cref="RouteValueDictionary"/> containing the route values.</param>
+ /// <param name="routeName">The route name.</param>
+ /// <param name="order">The route order.</param>
+ /// <returns>The <see cref="OutboundRouteEntry"/>.</returns>
+ public OutboundRouteEntry MapOutbound(
+ IRouter handler,
+ RouteTemplate routeTemplate,
+ RouteValueDictionary requiredLinkValues,
+ string routeName,
+ int order)
+ {
+ if (handler == null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ if (routeTemplate == null)
+ {
+ throw new ArgumentNullException(nameof(routeTemplate));
+ }
+
+ if (requiredLinkValues == null)
+ {
+ throw new ArgumentNullException(nameof(requiredLinkValues));
+ }
+
+ var entry = new OutboundRouteEntry()
+ {
+ Handler = handler,
+ Order = order,
+ Precedence = RoutePrecedence.ComputeOutbound(routeTemplate),
+ RequiredLinkValues = requiredLinkValues,
+ RouteName = routeName,
+ RouteTemplate = routeTemplate,
+ };
+
+ var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText);
+ foreach (var parameter in routeTemplate.Parameters)
+ {
+ if (parameter.InlineConstraints != null)
+ {
+ if (parameter.IsOptional)
+ {
+ constraintBuilder.SetOptional(parameter.Name);
+ }
+
+ foreach (var constraint in parameter.InlineConstraints)
+ {
+ constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint);
+ }
+ }
+ }
+
+ entry.Constraints = constraintBuilder.Build();
+
+ entry.Defaults = new RouteValueDictionary();
+ foreach (var parameter in entry.RouteTemplate.Parameters)
+ {
+ if (parameter.DefaultValue != null)
+ {
+ entry.Defaults.Add(parameter.Name, parameter.DefaultValue);
+ }
+ }
+
+ OutboundEntries.Add(entry);
+ return entry;
+ }
+
+ /// <summary>
+ /// Gets the list of <see cref="InboundRouteEntry"/>.
+ /// </summary>
+ public IList<InboundRouteEntry> InboundEntries { get; } = new List<InboundRouteEntry>();
+
+ /// <summary>
+ /// Gets the list of <see cref="OutboundRouteEntry"/>.
+ /// </summary>
+ public IList<OutboundRouteEntry> OutboundEntries { get; } = new List<OutboundRouteEntry>();
+
+ /// <summary>
+ /// Builds a <see cref="TreeRouter"/> with the <see cref="InboundEntries"/>
+ /// and <see cref="OutboundEntries"/> defined in this <see cref="TreeRouteBuilder"/>.
+ /// </summary>
+ /// <returns>The <see cref="TreeRouter"/>.</returns>
+ public TreeRouter Build()
+ {
+ return Build(version: 0);
+ }
+
+ /// <summary>
+ /// Builds a <see cref="TreeRouter"/> with the <see cref="InboundEntries"/>
+ /// and <see cref="OutboundEntries"/> defined in this <see cref="TreeRouteBuilder"/>.
+ /// </summary>
+ /// <param name="version">The version of the <see cref="TreeRouter"/>.</param>
+ /// <returns>The <see cref="TreeRouter"/>.</returns>
+ public TreeRouter Build(int version)
+ {
+ // Tree route builder builds a tree for each of the different route orders defined by
+ // the user. When a route needs to be matched, the matching algorithm in tree router
+ // just iterates over the trees in ascending order when it tries to match the route.
+ var trees = new Dictionary<int, UrlMatchingTree>();
+
+ foreach (var entry in InboundEntries)
+ {
+ UrlMatchingTree tree;
+ if (!trees.TryGetValue(entry.Order, out tree))
+ {
+ tree = new UrlMatchingTree(entry.Order);
+ trees.Add(entry.Order, tree);
+ }
+
+ AddEntryToTree(tree, entry);
+ }
+
+ return new TreeRouter(
+ trees.Values.OrderBy(tree => tree.Order).ToArray(),
+ OutboundEntries,
+ _urlEncoder,
+ _objectPool,
+ _logger,
+ _constraintLogger,
+ version);
+ }
+
+ /// <summary>
+ /// Removes all <see cref="InboundEntries"/> and <see cref="OutboundEntries"/> from this
+ /// <see cref="TreeRouteBuilder"/>.
+ /// </summary>
+ public void Clear()
+ {
+ InboundEntries.Clear();
+ OutboundEntries.Clear();
+ }
+
+ private void AddEntryToTree(UrlMatchingTree tree, InboundRouteEntry entry)
+ {
+ // The url matching tree represents all the routes asociated with a given
+ // order. Each node in the tree represents all the different categories
+ // a segment can have for which there is a defined inbound route entry.
+ // Each node contains a set of Matches that indicate all the routes for which
+ // a URL is a potential match. This list contains the routes with the same
+ // number of segments and the routes with the same number of segments plus an
+ // additional catch all parameter (as it can be empty).
+ // For example, for a set of routes like:
+ // 'Customer/Index/{id}'
+ // '{Controller}/{Action}/{*parameters}'
+ //
+ // The route tree will look like:
+ // Root ->
+ // Literals: Customer ->
+ // Literals: Index ->
+ // Parameters: {id}
+ // Matches: 'Customer/Index/{id}'
+ // Parameters: {Controller} ->
+ // Parameters: {Action} ->
+ // Matches: '{Controller}/{Action}/{*parameters}'
+ // CatchAlls: {*parameters}
+ // Matches: '{Controller}/{Action}/{*parameters}'
+ //
+ // When the tree router tries to match a route, it iterates the list of url matching trees
+ // in ascending order. For each tree it traverses each node starting from the root in the
+ // following order: Literals, constrained parameters, parameters, constrained catch all routes, catch alls.
+ // When it gets to a node of the same length as the route its trying to match, it simply looks at the list of
+ // candidates (which is in precence order) and tries to match the url against it.
+ //
+
+ var current = tree.Root;
+ var matcher = new TemplateMatcher(entry.RouteTemplate, entry.Defaults);
+
+ for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++)
+ {
+ var segment = entry.RouteTemplate.Segments[i];
+ if (!segment.IsSimple)
+ {
+ // Treat complex segments as a constrained parameter
+ if (current.ConstrainedParameters == null)
+ {
+ current.ConstrainedParameters = new UrlMatchingNode(length: i + 1);
+ }
+
+ current = current.ConstrainedParameters;
+ continue;
+ }
+
+ Debug.Assert(segment.Parts.Count == 1);
+ var part = segment.Parts[0];
+ if (part.IsLiteral)
+ {
+ UrlMatchingNode next;
+ if (!current.Literals.TryGetValue(part.Text, out next))
+ {
+ next = new UrlMatchingNode(length: i + 1);
+ current.Literals.Add(part.Text, next);
+ }
+
+ current = next;
+ continue;
+ }
+
+ // We accept templates that have intermediate optional values, but we ignore
+ // those values for route matching. For that reason, we need to add the entry
+ // to the list of matches, only if the remaining segments are optional. For example:
+ // /{controller}/{action=Index}/{id} will be equivalent to /{controller}/{action}/{id}
+ // for the purposes of route matching.
+ if (part.IsParameter &&
+ RemainingSegmentsAreOptional(entry.RouteTemplate.Segments, i))
+ {
+ current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher });
+ }
+
+ if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll)
+ {
+ if (current.ConstrainedParameters == null)
+ {
+ current.ConstrainedParameters = new UrlMatchingNode(length: i + 1);
+ }
+
+ current = current.ConstrainedParameters;
+ continue;
+ }
+
+ if (part.IsParameter && !part.IsCatchAll)
+ {
+ if (current.Parameters == null)
+ {
+ current.Parameters = new UrlMatchingNode(length: i + 1);
+ }
+
+ current = current.Parameters;
+ continue;
+ }
+
+ if (part.IsParameter && part.InlineConstraints.Any() && part.IsCatchAll)
+ {
+ if (current.ConstrainedCatchAlls == null)
+ {
+ current.ConstrainedCatchAlls = new UrlMatchingNode(length: i + 1) { IsCatchAll = true };
+ }
+
+ current = current.ConstrainedCatchAlls;
+ continue;
+ }
+
+ if (part.IsParameter && part.IsCatchAll)
+ {
+ if (current.CatchAlls == null)
+ {
+ current.CatchAlls = new UrlMatchingNode(length: i + 1) { IsCatchAll = true };
+ }
+
+ current = current.CatchAlls;
+ continue;
+ }
+
+ Debug.Fail("We shouldn't get here.");
+ }
+
+ current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher });
+ current.Matches.Sort((x, y) =>
+ {
+ var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence);
+ return result == 0 ? x.Entry.RouteTemplate.TemplateText.CompareTo(y.Entry.RouteTemplate.TemplateText) : result;
+ });
+ }
+
+ private static bool RemainingSegmentsAreOptional(IList<TemplateSegment> segments, int currentParameterIndex)
+ {
+ for (var i = currentParameterIndex; i < segments.Count; i++)
+ {
+ if (!segments[i].IsSimple)
+ {
+ // /{complex}-{segment}
+ return false;
+ }
+
+ var part = segments[i].Parts[0];
+ if (!part.IsParameter)
+ {
+ // /literal
+ return false;
+ }
+
+ var isOptionlCatchAllOrHasDefaultValue = part.IsOptional ||
+ part.IsCatchAll ||
+ part.DefaultValue != null;
+
+ if (!isOptionlCatchAllOrHasDefaultValue)
+ {
+ // /{parameter}
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs
new file mode 100644
index 0000000000..31a1091093
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs
@@ -0,0 +1,423 @@
+// 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;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.AspNetCore.Routing.Logging;
+using Microsoft.AspNetCore.Routing.Template;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Microsoft.AspNetCore.Routing.Tree
+{
+ /// <summary>
+ /// An <see cref="IRouter"/> implementation for attribute routing.
+ /// </summary>
+ public class TreeRouter : IRouter
+ {
+ // Key used by routing and action selection to match an attribute route entry to a
+ // group of action descriptors.
+ public static readonly string RouteGroupKey = "!__route_group";
+
+ private readonly LinkGenerationDecisionTree _linkGenerationTree;
+ private readonly UrlMatchingTree[] _trees;
+ private readonly IDictionary<string, OutboundMatch> _namedEntries;
+
+ private readonly ILogger _logger;
+ private readonly ILogger _constraintLogger;
+
+ /// <summary>
+ /// Creates a new <see cref="TreeRouter"/>.
+ /// </summary>
+ /// <param name="trees">The list of <see cref="UrlMatchingTree"/> that contains the route entries.</param>
+ /// <param name="linkGenerationEntries">The set of <see cref="OutboundRouteEntry"/>.</param>
+ /// <param name="urlEncoder">The <see cref="UrlEncoder"/>.</param>
+ /// <param name="objectPool">The <see cref="ObjectPool{T}"/>.</param>
+ /// <param name="routeLogger">The <see cref="ILogger"/> instance.</param>
+ /// <param name="constraintLogger">The <see cref="ILogger"/> instance used
+ /// in <see cref="RouteConstraintMatcher"/>.</param>
+ /// <param name="version">The version of this route.</param>
+ public TreeRouter(
+ UrlMatchingTree[] trees,
+ IEnumerable<OutboundRouteEntry> linkGenerationEntries,
+ UrlEncoder urlEncoder,
+ ObjectPool<UriBuildingContext> objectPool,
+ ILogger routeLogger,
+ ILogger constraintLogger,
+ int version)
+ {
+ if (trees == null)
+ {
+ throw new ArgumentNullException(nameof(trees));
+ }
+
+ if (linkGenerationEntries == null)
+ {
+ throw new ArgumentNullException(nameof(linkGenerationEntries));
+ }
+
+ if (urlEncoder == null)
+ {
+ throw new ArgumentNullException(nameof(urlEncoder));
+ }
+
+ if (objectPool == null)
+ {
+ throw new ArgumentNullException(nameof(objectPool));
+ }
+
+ if (routeLogger == null)
+ {
+ throw new ArgumentNullException(nameof(routeLogger));
+ }
+
+ if (constraintLogger == null)
+ {
+ throw new ArgumentNullException(nameof(constraintLogger));
+ }
+
+ _trees = trees;
+ _logger = routeLogger;
+ _constraintLogger = constraintLogger;
+
+ _namedEntries = new Dictionary<string, OutboundMatch>(StringComparer.OrdinalIgnoreCase);
+
+ var outboundMatches = new List<OutboundMatch>();
+
+ foreach (var entry in linkGenerationEntries)
+ {
+
+ var binder = new TemplateBinder(urlEncoder, objectPool, entry.RouteTemplate, entry.Defaults);
+ var outboundMatch = new OutboundMatch() { Entry = entry, TemplateBinder = binder };
+ outboundMatches.Add(outboundMatch);
+
+ // Skip unnamed entries
+ if (entry.RouteName == null)
+ {
+ continue;
+ }
+
+ // We only need to keep one OutboundMatch per route template
+ // so in case two entries have the same name and the same template we only keep
+ // the first entry.
+ OutboundMatch namedMatch;
+ if (_namedEntries.TryGetValue(entry.RouteName, out namedMatch) &&
+ !string.Equals(
+ namedMatch.Entry.RouteTemplate.TemplateText,
+ entry.RouteTemplate.TemplateText,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException(
+ Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.RouteName),
+ nameof(linkGenerationEntries));
+ }
+ else if (namedMatch == null)
+ {
+ _namedEntries.Add(entry.RouteName, outboundMatch);
+ }
+ }
+
+ // The decision tree will take care of ordering for these entries.
+ _linkGenerationTree = new LinkGenerationDecisionTree(outboundMatches.ToArray());
+
+ Version = version;
+ }
+
+ /// <summary>
+ /// Gets the version of this route.
+ /// </summary>
+ public int Version { get; }
+
+ internal IEnumerable<UrlMatchingTree> MatchingTrees => _trees;
+
+ /// <inheritdoc />
+ public VirtualPathData GetVirtualPath(VirtualPathContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // If it's a named route we will try to generate a link directly and
+ // if we can't, we will not try to generate it using an unnamed route.
+ if (context.RouteName != null)
+ {
+ return GetVirtualPathForNamedRoute(context);
+ }
+
+ // The decision tree will give us back all entries that match the provided route data in the correct
+ // order. We just need to iterate them and use the first one that can generate a link.
+ var matches = _linkGenerationTree.GetMatches(context);
+
+ if (matches == null)
+ {
+ return null;
+ }
+
+ for (var i = 0; i < matches.Count; i++)
+ {
+ var path = GenerateVirtualPath(context, matches[i].Match.Entry, matches[i].Match.TemplateBinder);
+ if (path != null)
+ {
+ return path;
+ }
+ }
+
+ return null;
+ }
+
+ /// <inheritdoc />
+ public async Task RouteAsync(RouteContext context)
+ {
+ foreach (var tree in _trees)
+ {
+ var tokenizer = new PathTokenizer(context.HttpContext.Request.Path);
+ var root = tree.Root;
+
+ var treeEnumerator = new TreeEnumerator(root, tokenizer);
+
+ // Create a snapshot before processing the route. We'll restore this snapshot before running each
+ // to restore the state. This is likely an "empty" snapshot, which doesn't allocate.
+ var snapshot = context.RouteData.PushState(router: null, values: null, dataTokens: null);
+
+ while (treeEnumerator.MoveNext())
+ {
+ var node = treeEnumerator.Current;
+ foreach (var item in node.Matches)
+ {
+ var entry = item.Entry;
+ var matcher = item.TemplateMatcher;
+
+ try
+ {
+ if (!matcher.TryMatch(context.HttpContext.Request.Path, context.RouteData.Values))
+ {
+ continue;
+ }
+
+ if (!RouteConstraintMatcher.Match(
+ entry.Constraints,
+ context.RouteData.Values,
+ context.HttpContext,
+ this,
+ RouteDirection.IncomingRequest,
+ _constraintLogger))
+ {
+ continue;
+ }
+
+ _logger.MatchedRoute(entry.RouteName, entry.RouteTemplate.TemplateText);
+ context.RouteData.Routers.Add(entry.Handler);
+
+ await entry.Handler.RouteAsync(context);
+ if (context.Handler != null)
+ {
+ return;
+ }
+ }
+ finally
+ {
+ if (context.Handler == null)
+ {
+ // Restore the original values to prevent polluting the route data.
+ snapshot.Restore();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private struct TreeEnumerator : IEnumerator<UrlMatchingNode>
+ {
+ private readonly Stack<UrlMatchingNode> _stack;
+ private readonly PathTokenizer _tokenizer;
+
+ public TreeEnumerator(UrlMatchingNode root, PathTokenizer tokenizer)
+ {
+ _stack = new Stack<UrlMatchingNode>();
+ _tokenizer = tokenizer;
+ Current = null;
+
+ _stack.Push(root);
+ }
+
+ public UrlMatchingNode Current { get; private set; }
+
+ object IEnumerator.Current => Current;
+
+ public void Dispose()
+ {
+ }
+
+ public bool MoveNext()
+ {
+ if (_stack == null)
+ {
+ return false;
+ }
+
+ while (_stack.Count > 0)
+ {
+ var next = _stack.Pop();
+
+ // In case of wild card segment, the request path segment length can be greater
+ // Example:
+ // Template: a/{*path}
+ // Request Url: a/b/c/d
+ if (next.IsCatchAll && next.Matches.Count > 0)
+ {
+ Current = next;
+ return true;
+ }
+ // Next template has the same length as the url we are trying to match
+ // The only possible matching segments are either our current matches or
+ // any catch-all segment after this segment in which the catch all is empty.
+ else if (next.Depth == _tokenizer.Count)
+ {
+ if (next.Matches.Count > 0)
+ {
+ Current = next;
+ return true;
+ }
+ else
+ {
+ // We can stop looking as any other child node from this node will be
+ // either a literal, a constrained parameter or a parameter.
+ // (Catch alls and constrained catch alls will show up as candidate matches).
+ continue;
+ }
+ }
+
+ if (next.CatchAlls != null)
+ {
+ _stack.Push(next.CatchAlls);
+ }
+
+ if (next.ConstrainedCatchAlls != null)
+ {
+ _stack.Push(next.ConstrainedCatchAlls);
+ }
+
+ if (next.Parameters != null)
+ {
+ _stack.Push(next.Parameters);
+ }
+
+ if (next.ConstrainedParameters != null)
+ {
+ _stack.Push(next.ConstrainedParameters);
+ }
+
+ if (next.Literals.Count > 0)
+ {
+ UrlMatchingNode node;
+ Debug.Assert(next.Depth < _tokenizer.Count);
+ if (next.Literals.TryGetValue(_tokenizer[next.Depth].Value, out node))
+ {
+ _stack.Push(node);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public void Reset()
+ {
+ _stack.Clear();
+ Current = null;
+ }
+ }
+
+ private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context)
+ {
+ OutboundMatch match;
+ if (_namedEntries.TryGetValue(context.RouteName, out match))
+ {
+ var path = GenerateVirtualPath(context, match.Entry, match.TemplateBinder);
+ if (path != null)
+ {
+ return path;
+ }
+ }
+ return null;
+ }
+
+ private VirtualPathData GenerateVirtualPath(
+ VirtualPathContext context,
+ OutboundRouteEntry entry,
+ TemplateBinder binder)
+ {
+ // In attribute the context includes the values that are used to select this entry - typically
+ // these will be the standard 'action', 'controller' and maybe 'area' tokens. However, we don't
+ // want to pass these to the link generation code, or else they will end up as query parameters.
+ //
+ // So, we need to exclude from here any values that are 'required link values', but aren't
+ // parameters in the template.
+ //
+ // Ex:
+ // template: api/Products/{action}
+ // required values: { id = "5", action = "Buy", Controller = "CoolProducts" }
+ //
+ // result: { id = "5", action = "Buy" }
+ var inputValues = new RouteValueDictionary();
+ foreach (var kvp in context.Values)
+ {
+ if (entry.RequiredLinkValues.ContainsKey(kvp.Key))
+ {
+ var parameter = entry.RouteTemplate.GetParameter(kvp.Key);
+
+ if (parameter == null)
+ {
+ continue;
+ }
+ }
+
+ inputValues.Add(kvp.Key, kvp.Value);
+ }
+
+ var bindingResult = binder.GetValues(context.AmbientValues, inputValues);
+ if (bindingResult == null)
+ {
+ // A required parameter in the template didn't get a value.
+ return null;
+ }
+
+ var matched = RouteConstraintMatcher.Match(
+ entry.Constraints,
+ bindingResult.CombinedValues,
+ context.HttpContext,
+ this,
+ RouteDirection.UrlGeneration,
+ _constraintLogger);
+
+ if (!matched)
+ {
+ // A constraint rejected this link.
+ return null;
+ }
+
+ var pathData = entry.Handler.GetVirtualPath(context);
+ if (pathData != null)
+ {
+ // If path is non-null then the target router short-circuited, we don't expect this
+ // in typical MVC scenarios.
+ return pathData;
+ }
+
+ var path = binder.BindValues(bindingResult.AcceptedValues);
+ if (path == null)
+ {
+ return null;
+ }
+
+ return new VirtualPathData(this, path);
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs
new file mode 100644
index 0000000000..ffc387efe9
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingNode.cs
@@ -0,0 +1,81 @@
+// 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.Linq;
+
+namespace Microsoft.AspNetCore.Routing.Tree
+{
+ /// <summary>
+ /// A node in a <see cref="UrlMatchingTree"/>.
+ /// </summary>
+ [DebuggerDisplay("{DebuggerToString(),nq}")]
+ public class UrlMatchingNode
+ {
+ /// <summary>
+ /// Initializes a new instance of <see cref="UrlMatchingNode"/>.
+ /// </summary>
+ /// <param name="length">The length of the path to this node in the <see cref="UrlMatchingTree"/>.</param>
+ public UrlMatchingNode(int length)
+ {
+ Depth = length;
+
+ Matches = new List<InboundMatch>();
+ Literals = new Dictionary<string, UrlMatchingNode>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Gets the length of the path to this node in the <see cref="UrlMatchingTree"/>.
+ /// </summary>
+ public int Depth { get; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this node represents a catch all segment.
+ /// </summary>
+ public bool IsCatchAll { get; set; }
+
+ /// <summary>
+ /// Gets the list of matching route entries associated with this node.
+ /// </summary>
+ /// <remarks>
+ /// These entries are sorted by precedence then template.
+ /// </remarks>
+ public List<InboundMatch> Matches { get; }
+
+ /// <summary>
+ /// Gets the literal segments following this segment.
+ /// </summary>
+ public Dictionary<string, UrlMatchingNode> Literals { get; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="UrlMatchingNode"/> representing
+ /// parameter segments with constraints following this segment in the <see cref="TreeRouter"/>.
+ /// </summary>
+ public UrlMatchingNode ConstrainedParameters { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="UrlMatchingNode"/> representing
+ /// parameter segments following this segment in the <see cref="TreeRouter"/>.
+ /// </summary>
+ public UrlMatchingNode Parameters { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="UrlMatchingNode"/> representing
+ /// catch all parameter segments with constraints following this segment in the <see cref="TreeRouter"/>.
+ /// </summary>
+ public UrlMatchingNode ConstrainedCatchAlls { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="UrlMatchingNode"/> representing
+ /// catch all parameter segments following this segment in the <see cref="TreeRouter"/>.
+ /// </summary>
+ public UrlMatchingNode CatchAlls { get; set; }
+
+ private string DebuggerToString()
+ {
+ return $"Length: {Depth}, Matches: {string.Join(" | ", Matches?.Select(m => $"({m.TemplateMatcher.Template.TemplateText})"))}";
+ }
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingTree.cs b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingTree.cs
new file mode 100644
index 0000000000..90528d75b9
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/Tree/UrlMatchingTree.cs
@@ -0,0 +1,30 @@
+// 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.Routing.Tree
+{
+ /// <summary>
+ /// A tree part of a <see cref="TreeRouter"/>.
+ /// </summary>
+ public class UrlMatchingTree
+ {
+ /// <summary>
+ /// Initializes a new instance of <see cref="UrlMatchingTree"/>.
+ /// </summary>
+ /// <param name="order">The order associated with routes in this <see cref="UrlMatchingTree"/>.</param>
+ public UrlMatchingTree(int order)
+ {
+ Order = order;
+ }
+
+ /// <summary>
+ /// Gets the order of the routes associated with this <see cref="UrlMatchingTree"/>.
+ /// </summary>
+ public int Order { get; }
+
+ /// <summary>
+ /// Gets the root of the <see cref="UrlMatchingTree"/>.
+ /// </summary>
+ public UrlMatchingNode Root { get; } = new UrlMatchingNode(length: 0);
+ }
+}
diff --git a/src/Routing/src/Microsoft.AspNetCore.Routing/baseline.netcore.json b/src/Routing/src/Microsoft.AspNetCore.Routing/baseline.netcore.json
new file mode 100644
index 0000000000..866f3e89cb
--- /dev/null
+++ b/src/Routing/src/Microsoft.AspNetCore.Routing/baseline.netcore.json
@@ -0,0 +1,4579 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Routing, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.Builder.MapRouteRouteBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "MapRoute",
+ "Parameters": [
+ {
+ "Name": "routeBuilder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapRoute",
+ "Parameters": [
+ {
+ "Name": "routeBuilder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "defaults",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapRoute",
+ "Parameters": [
+ {
+ "Name": "routeBuilder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "defaults",
+ "Type": "System.Object"
+ },
+ {
+ "Name": "constraints",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapRoute",
+ "Parameters": [
+ {
+ "Name": "routeBuilder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "defaults",
+ "Type": "System.Object"
+ },
+ {
+ "Name": "constraints",
+ "Type": "System.Object"
+ },
+ {
+ "Name": "dataTokens",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.RouterMiddleware",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Invoke",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "next",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "router",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.RoutingBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseRouter",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "router",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseRouter",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "action",
+ "Type": "System.Action<Microsoft.AspNetCore.Routing.IRouteBuilder>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.DefaultInlineConstraintResolver",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IInlineConstraintResolver"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ResolveConstraint",
+ "Parameters": [
+ {
+ "Name": "inlineConstraint",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "routeOptions",
+ "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Routing.RouteOptions>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ResolveConstraint",
+ "Parameters": [
+ {
+ "Name": "inlineConstraint",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.INamedRouter",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouter"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Name",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.InlineRouteParameterParser",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ParseRouteParameter",
+ "Parameters": [
+ {
+ "Name": "routeParameter",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplatePart",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ApplicationBuilder",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_DefaultHandler",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouter",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_DefaultHandler",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ServiceProvider",
+ "Parameters": [],
+ "ReturnType": "System.IServiceProvider",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Routes",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<Microsoft.AspNetCore.Routing.IRouter>",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Build",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouter",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.IRouteCollection",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouter"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Add",
+ "Parameters": [
+ {
+ "Name": "router",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RequestDelegateRouteBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "MapRoute",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "handler",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapMiddlewareRoute",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "action",
+ "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapDelete",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "handler",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapMiddlewareDelete",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "action",
+ "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapDelete",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "handler",
+ "Type": "System.Func<Microsoft.AspNetCore.Http.HttpRequest, Microsoft.AspNetCore.Http.HttpResponse, Microsoft.AspNetCore.Routing.RouteData, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapGet",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "handler",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapMiddlewareGet",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "action",
+ "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapGet",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "handler",
+ "Type": "System.Func<Microsoft.AspNetCore.Http.HttpRequest, Microsoft.AspNetCore.Http.HttpResponse, Microsoft.AspNetCore.Routing.RouteData, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapPost",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "handler",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapMiddlewarePost",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "action",
+ "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapPost",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "handler",
+ "Type": "System.Func<Microsoft.AspNetCore.Http.HttpRequest, Microsoft.AspNetCore.Http.HttpResponse, Microsoft.AspNetCore.Routing.RouteData, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapPut",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "handler",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapMiddlewarePut",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "action",
+ "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapPut",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "handler",
+ "Type": "System.Func<Microsoft.AspNetCore.Http.HttpRequest, Microsoft.AspNetCore.Http.HttpResponse, Microsoft.AspNetCore.Routing.RouteData, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapVerb",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "verb",
+ "Type": "System.String"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "handler",
+ "Type": "System.Func<Microsoft.AspNetCore.Http.HttpRequest, Microsoft.AspNetCore.Http.HttpResponse, Microsoft.AspNetCore.Routing.RouteData, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapVerb",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "verb",
+ "Type": "System.String"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "handler",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapMiddlewareVerb",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ },
+ {
+ "Name": "verb",
+ "Type": "System.String"
+ },
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "action",
+ "Type": "System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Route",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Routing.RouteBase",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_RouteTemplate",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "OnRouteMatched",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.RouteContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "OnVirtualPathGenerated",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "target",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeTemplate",
+ "Type": "System.String"
+ },
+ {
+ "Name": "inlineConstraintResolver",
+ "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "target",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeTemplate",
+ "Type": "System.String"
+ },
+ {
+ "Name": "defaults",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "constraints",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>"
+ },
+ {
+ "Name": "dataTokens",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "inlineConstraintResolver",
+ "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "target",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "routeTemplate",
+ "Type": "System.String"
+ },
+ {
+ "Name": "defaults",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "constraints",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>"
+ },
+ {
+ "Name": "dataTokens",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "inlineConstraintResolver",
+ "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteBase",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.INamedRouter"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "OnRouteMatched",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.RouteContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Abstract": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "OnVirtualPathGenerated",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData",
+ "Virtual": true,
+ "Abstract": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RouteAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.RouteContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetVirtualPath",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Constraints",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IDictionary<System.String, Microsoft.AspNetCore.Routing.IRouteConstraint>",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Constraints",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Collections.Generic.IDictionary<System.String, Microsoft.AspNetCore.Routing.IRouteConstraint>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ConstraintResolver",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ConstraintResolver",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_DataTokens",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_DataTokens",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Defaults",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Defaults",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Name",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.INamedRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Name",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ParsedTemplate",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Template.RouteTemplate",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ParsedTemplate",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetConstraints",
+ "Parameters": [
+ {
+ "Name": "inlineConstraintResolver",
+ "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver"
+ },
+ {
+ "Name": "parsedTemplate",
+ "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate"
+ },
+ {
+ "Name": "constraints",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>"
+ }
+ ],
+ "ReturnType": "System.Collections.Generic.IDictionary<System.String, Microsoft.AspNetCore.Routing.IRouteConstraint>",
+ "Static": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetDefaults",
+ "Parameters": [
+ {
+ "Name": "parsedTemplate",
+ "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate"
+ },
+ {
+ "Name": "defaults",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Static": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ToString",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "constraintResolver",
+ "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver"
+ },
+ {
+ "Name": "defaults",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "constraints",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>"
+ },
+ {
+ "Name": "dataTokens",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteBuilder",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteBuilder"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ApplicationBuilder",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_DefaultHandler",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouter",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_DefaultHandler",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ServiceProvider",
+ "Parameters": [],
+ "ReturnType": "System.IServiceProvider",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Routes",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<Microsoft.AspNetCore.Routing.IRouter>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Build",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouter",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "applicationBuilder",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "applicationBuilder",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "defaultHandler",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteCollection",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteCollection"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "RouteAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.RouteContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetVirtualPath",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Item",
+ "Parameters": [
+ {
+ "Name": "index",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Count",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Add",
+ "Parameters": [
+ {
+ "Name": "router",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteCollection",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteConstraintBuilder",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Build",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IDictionary<System.String, Microsoft.AspNetCore.Routing.IRouteConstraint>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddConstraint",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddResolvedConstraint",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "constraintText",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SetOptional",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "inlineConstraintResolver",
+ "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteConstraintMatcher",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "constraints",
+ "Type": "System.Collections.Generic.IDictionary<System.String, Microsoft.AspNetCore.Routing.IRouteConstraint>"
+ },
+ {
+ "Name": "routeValues",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILogger"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteCreationException",
+ "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.Routing.RouteHandler",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteHandler",
+ "Microsoft.AspNetCore.Routing.IRouter"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetRequestHandler",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "routeData",
+ "Type": "Microsoft.AspNetCore.Routing.RouteData"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Http.RequestDelegate",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetVirtualPath",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RouteAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.RouteContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "requestDelegate",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_LowercaseUrls",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_LowercaseUrls",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AppendTrailingSlash",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AppendTrailingSlash",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ConstraintMap",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IDictionary<System.String, System.Type>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ConstraintMap",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.Type>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RouteValueEqualityComparer",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "System.Collections.Generic.IEqualityComparer<System.Object>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Equals",
+ "Parameters": [
+ {
+ "Name": "x",
+ "Type": "System.Object"
+ },
+ {
+ "Name": "y",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.IEqualityComparer<System.Object>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetHashCode",
+ "Parameters": [
+ {
+ "Name": "obj",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "System.Int32",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.IEqualityComparer<System.Object>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.RoutingFeature",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRoutingFeature"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_RouteData",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteData",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRoutingFeature",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RouteData",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.RouteData"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRoutingFeature",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Tree.InboundMatch",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Entry",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Tree.InboundRouteEntry",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Entry",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.Tree.InboundRouteEntry"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TemplateMatcher",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplateMatcher",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_TemplateMatcher",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.Template.TemplateMatcher"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Tree.InboundRouteEntry",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Constraints",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IDictionary<System.String, Microsoft.AspNetCore.Routing.IRouteConstraint>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Constraints",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Collections.Generic.IDictionary<System.String, Microsoft.AspNetCore.Routing.IRouteConstraint>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Defaults",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Defaults",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Handler",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Handler",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Order",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Order",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Precedence",
+ "Parameters": [],
+ "ReturnType": "System.Decimal",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Precedence",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Decimal"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RouteName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RouteName",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RouteTemplate",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Template.RouteTemplate",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RouteTemplate",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Tree.OutboundMatch",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Entry",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Tree.OutboundRouteEntry",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Entry",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.Tree.OutboundRouteEntry"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TemplateBinder",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplateBinder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_TemplateBinder",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.Template.TemplateBinder"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Tree.OutboundRouteEntry",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Constraints",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IDictionary<System.String, Microsoft.AspNetCore.Routing.IRouteConstraint>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Constraints",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Collections.Generic.IDictionary<System.String, Microsoft.AspNetCore.Routing.IRouteConstraint>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Defaults",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Defaults",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Handler",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Handler",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Order",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Order",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Precedence",
+ "Parameters": [],
+ "ReturnType": "System.Decimal",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Precedence",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Decimal"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RouteName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RouteName",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RequiredLinkValues",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RequiredLinkValues",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RouteTemplate",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Template.RouteTemplate",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RouteTemplate",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Tree.TreeRouteBuilder",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "MapInbound",
+ "Parameters": [
+ {
+ "Name": "handler",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeTemplate",
+ "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate"
+ },
+ {
+ "Name": "routeName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "order",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Tree.InboundRouteEntry",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapOutbound",
+ "Parameters": [
+ {
+ "Name": "handler",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeTemplate",
+ "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate"
+ },
+ {
+ "Name": "requiredLinkValues",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "order",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Tree.OutboundRouteEntry",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_InboundEntries",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<Microsoft.AspNetCore.Routing.Tree.InboundRouteEntry>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OutboundEntries",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<Microsoft.AspNetCore.Routing.Tree.OutboundRouteEntry>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Build",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Tree.TreeRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Build",
+ "Parameters": [
+ {
+ "Name": "version",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Tree.TreeRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Clear",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "urlEncoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "objectPool",
+ "Type": "Microsoft.Extensions.ObjectPool.ObjectPool<Microsoft.AspNetCore.Routing.Internal.UriBuildingContext>"
+ },
+ {
+ "Name": "constraintResolver",
+ "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "objectPool",
+ "Type": "Microsoft.Extensions.ObjectPool.ObjectPool<Microsoft.AspNetCore.Routing.Internal.UriBuildingContext>"
+ },
+ {
+ "Name": "constraintResolver",
+ "Type": "Microsoft.AspNetCore.Routing.IInlineConstraintResolver"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Tree.TreeRouter",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouter"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Version",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetVirtualPath",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.VirtualPathContext"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.VirtualPathData",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RouteAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Routing.RouteContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouter",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "trees",
+ "Type": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingTree[]"
+ },
+ {
+ "Name": "linkGenerationEntries",
+ "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Routing.Tree.OutboundRouteEntry>"
+ },
+ {
+ "Name": "urlEncoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "objectPool",
+ "Type": "Microsoft.Extensions.ObjectPool.ObjectPool<Microsoft.AspNetCore.Routing.Internal.UriBuildingContext>"
+ },
+ {
+ "Name": "routeLogger",
+ "Type": "Microsoft.Extensions.Logging.ILogger"
+ },
+ {
+ "Name": "constraintLogger",
+ "Type": "Microsoft.Extensions.Logging.ILogger"
+ },
+ {
+ "Name": "version",
+ "Type": "System.Int32"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "RouteGroupKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Depth",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IsCatchAll",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_IsCatchAll",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Matches",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.List<Microsoft.AspNetCore.Routing.Tree.InboundMatch>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Literals",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.Dictionary<System.String, Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ConstrainedParameters",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ConstrainedParameters",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Parameters",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Parameters",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ConstrainedCatchAlls",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ConstrainedCatchAlls",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CatchAlls",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CatchAlls",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "length",
+ "Type": "System.Int32"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingTree",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Order",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Root",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Tree.UrlMatchingNode",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "order",
+ "Type": "System.Int32"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Template.InlineConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Constraint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "constraint",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Template.RoutePrecedence",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "ComputeInbound",
+ "Parameters": [
+ {
+ "Name": "template",
+ "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate"
+ }
+ ],
+ "ReturnType": "System.Decimal",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ComputeOutbound",
+ "Parameters": [
+ {
+ "Name": "template",
+ "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate"
+ }
+ ],
+ "ReturnType": "System.Decimal",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Template.RouteTemplate",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_TemplateText",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Parameters",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<Microsoft.AspNetCore.Routing.Template.TemplatePart>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Segments",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<Microsoft.AspNetCore.Routing.Template.TemplateSegment>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetSegment",
+ "Parameters": [
+ {
+ "Name": "index",
+ "Type": "System.Int32"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplateSegment",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetParameter",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplatePart",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "template",
+ "Type": "System.String"
+ },
+ {
+ "Name": "segments",
+ "Type": "System.Collections.Generic.List<Microsoft.AspNetCore.Routing.Template.TemplateSegment>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Template.TemplateBinder",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetValues",
+ "Parameters": [
+ {
+ "Name": "ambientValues",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplateValuesResult",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "BindValues",
+ "Parameters": [
+ {
+ "Name": "acceptedValues",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RoutePartsEqual",
+ "Parameters": [
+ {
+ "Name": "a",
+ "Type": "System.Object"
+ },
+ {
+ "Name": "b",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "urlEncoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "pool",
+ "Type": "Microsoft.Extensions.ObjectPool.ObjectPool<Microsoft.AspNetCore.Routing.Internal.UriBuildingContext>"
+ },
+ {
+ "Name": "template",
+ "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate"
+ },
+ {
+ "Name": "defaults",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Template.TemplateMatcher",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Defaults",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Template",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Template.RouteTemplate",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "TryMatch",
+ "Parameters": [
+ {
+ "Name": "path",
+ "Type": "Microsoft.AspNetCore.Http.PathString"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "template",
+ "Type": "Microsoft.AspNetCore.Routing.Template.RouteTemplate"
+ },
+ {
+ "Name": "defaults",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Template.TemplateParser",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Parse",
+ "Parameters": [
+ {
+ "Name": "routeTemplate",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Template.RouteTemplate",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Template.TemplatePart",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateLiteral",
+ "Parameters": [
+ {
+ "Name": "text",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplatePart",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateParameter",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "isCatchAll",
+ "Type": "System.Boolean"
+ },
+ {
+ "Name": "isOptional",
+ "Type": "System.Boolean"
+ },
+ {
+ "Name": "defaultValue",
+ "Type": "System.Object"
+ },
+ {
+ "Name": "inlineConstraints",
+ "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Routing.Template.InlineConstraint>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Routing.Template.TemplatePart",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IsCatchAll",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IsLiteral",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IsParameter",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IsOptional",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IsOptionalSeperator",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_IsOptionalSeperator",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Name",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Text",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_DefaultValue",
+ "Parameters": [],
+ "ReturnType": "System.Object",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_InlineConstraints",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Routing.Template.InlineConstraint>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Template.TemplateSegment",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_IsSimple",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Parts",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.List<Microsoft.AspNetCore.Routing.Template.TemplatePart>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Template.TemplateValuesResult",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_AcceptedValues",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AcceptedValues",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CombinedValues",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.RouteValueDictionary",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CombinedValues",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.AlphaRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Routing.Constraints.RegexRouteConstraint",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.BoolRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.CompositeRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Constraints",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Routing.IRouteConstraint>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "constraints",
+ "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Routing.IRouteConstraint>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.DateTimeRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.DecimalRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.DoubleRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.FloatRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.GuidRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.HttpMethodRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_AllowedMethods",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "allowedMethods",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.IntRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.LengthRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_MinLength",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_MaxLength",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "length",
+ "Type": "System.Int32"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "minLength",
+ "Type": "System.Int32"
+ },
+ {
+ "Name": "maxLength",
+ "Type": "System.Int32"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.LongRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.MaxLengthRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_MaxLength",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "maxLength",
+ "Type": "System.Int32"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.MaxRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Max",
+ "Parameters": [],
+ "ReturnType": "System.Int64",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "max",
+ "Type": "System.Int64"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.MinLengthRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_MinLength",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "minLength",
+ "Type": "System.Int32"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.MinRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Min",
+ "Parameters": [],
+ "ReturnType": "System.Int64",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "min",
+ "Type": "System.Int64"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.OptionalRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_InnerConstraint",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "innerConstraint",
+ "Type": "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.RangeRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Min",
+ "Parameters": [],
+ "ReturnType": "System.Int64",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Max",
+ "Parameters": [],
+ "ReturnType": "System.Int64",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "min",
+ "Type": "System.Int64"
+ },
+ {
+ "Name": "max",
+ "Type": "System.Int64"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.RegexInlineRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Routing.Constraints.RegexRouteConstraint",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "regexPattern",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.RegexRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Constraint",
+ "Parameters": [],
+ "ReturnType": "System.Text.RegularExpressions.Regex",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "regex",
+ "Type": "System.Text.RegularExpressions.Regex"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "regexPattern",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.RequiredRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Routing.Constraints.StringRouteConstraint",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Routing.IRouteConstraint"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Match",
+ "Parameters": [
+ {
+ "Name": "httpContext",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "route",
+ "Type": "Microsoft.AspNetCore.Routing.IRouter"
+ },
+ {
+ "Name": "routeKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "values",
+ "Type": "Microsoft.AspNetCore.Routing.RouteValueDictionary"
+ },
+ {
+ "Name": "routeDirection",
+ "Type": "Microsoft.AspNetCore.Routing.RouteDirection"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Routing.IRouteConstraint",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.RoutingServiceCollectionExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddRouting",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddRouting",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Routing.RouteOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Routing/test/Directory.Build.props b/src/Routing/test/Directory.Build.props
new file mode 100644
index 0000000000..02caa062ac
--- /dev/null
+++ b/src/Routing/test/Directory.Build.props
@@ -0,0 +1,20 @@
+<Project>
+ <Import Project="..\Directory.Build.props" />
+
+ <PropertyGroup>
+ <DeveloperBuildTestTfms>netcoreapp2.1</DeveloperBuildTestTfms>
+ <StandardTestTfms>$(DeveloperBuildTestTfms)</StandardTestTfms>
+ <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' ">$(StandardTestTfms);netcoreapp2.0</StandardTestTfms>
+ <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' AND '$(OS)' == 'Windows_NT' ">$(StandardTestTfms);net461</StandardTestTfms>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
+ <PackageReference Include="Moq" Version="$(MoqPackageVersion)" />
+ <PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
+ <PackageReference Include="xunit.analyzers" Version="$(XunitAnalyzersPackageVersion)" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj
new file mode 100644
index 0000000000..fce9e366fc
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing.Abstractions\Microsoft.AspNetCore.Routing.Abstractions.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteDataTest.cs b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteDataTest.cs
new file mode 100644
index 0000000000..b8eac36c7e
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteDataTest.cs
@@ -0,0 +1,157 @@
+// 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 Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class RouteDataTest
+ {
+ [Fact]
+ public void RouteData_DefaultPropertyValues()
+ {
+ // Arrange & Act
+ var routeData = new RouteData();
+
+ // Assert
+ Assert.Empty(routeData.DataTokens);
+ Assert.Empty(routeData.Routers);
+ Assert.Empty(routeData.Values);
+ }
+
+ [Fact]
+ public void RouteData_CopyConstructor()
+ {
+ // Arrange & Act
+ var original = new RouteData();
+
+ original.DataTokens.Add("data", "token");
+ original.Routers.Add(Mock.Of<IRouter>());
+ original.Values.Add("route", "value");
+
+ var routeData = new RouteData(original);
+
+ // Assert
+ Assert.NotSame(routeData.DataTokens, original.DataTokens);
+ Assert.Equal(routeData.DataTokens, original.DataTokens);
+ Assert.NotSame(routeData.Routers, original.Routers);
+ Assert.Equal(routeData.Routers, original.Routers);
+ Assert.NotSame(routeData.Values, original.Values);
+ Assert.Equal(routeData.Values, original.Values);
+ }
+
+ [Fact]
+ public void RouteData_PushStateAndRestore_NullValues()
+ {
+ // Arrange
+ var routeData = new RouteData();
+
+ // Act
+ var snapshot = routeData.PushState(null, null, null);
+ var copy = new RouteData(routeData);
+ snapshot.Restore();
+
+ // Assert
+ Assert.Equal(routeData.DataTokens, copy.DataTokens);
+ Assert.Equal(routeData.Routers, copy.Routers);
+ Assert.Equal(routeData.Values, copy.Values);
+ }
+
+ [Fact]
+ public void RouteData_PushStateAndRestore_EmptyValues()
+ {
+ // Arrange
+ var routeData = new RouteData();
+
+ // Act
+ var snapshot = routeData.PushState(null, new RouteValueDictionary(), new RouteValueDictionary());
+ var copy = new RouteData(routeData);
+ snapshot.Restore();
+
+ // Assert
+ Assert.Equal(routeData.DataTokens, copy.DataTokens);
+ Assert.Equal(routeData.Routers, copy.Routers);
+ Assert.Equal(routeData.Values, copy.Values);
+ }
+
+ // This is an important semantic for catchall parameters. A null route value shouldn't be
+ // merged.
+ [Fact]
+ public void RouteData_PushStateAndRestore_NullRouteValueNotSet()
+ {
+ // Arrange
+ var original = new RouteData();
+ original.Values.Add("bleh", "16");
+
+ var routeData = new RouteData(original);
+
+ // Act
+ var snapshot = routeData.PushState(
+ null,
+ new RouteValueDictionary(new { bleh = (string)null }),
+ new RouteValueDictionary());
+ snapshot.Restore();
+
+ // Assert
+ Assert.Equal(routeData.Values, original.Values);
+ }
+
+ [Fact]
+ public void RouteData_PushStateAndThenModify()
+ {
+ // Arrange
+ var routeData = new RouteData();
+
+ // Act
+ var snapshot = routeData.PushState(null, null, null);
+ routeData.DataTokens.Add("data", "token");
+ routeData.Routers.Add(Mock.Of<IRouter>());
+ routeData.Values.Add("route", "value");
+
+ var copy = new RouteData(routeData);
+ snapshot.Restore();
+
+ // Assert
+ Assert.Empty(routeData.DataTokens);
+ Assert.NotEqual(routeData.DataTokens, copy.DataTokens);
+ Assert.Empty(routeData.Routers);
+ Assert.NotEqual(routeData.Routers, copy.Routers);
+ Assert.Empty(routeData.Values);
+ Assert.NotEqual(routeData.Values, copy.Values);
+ }
+
+ [Fact]
+ public void RouteData_PushStateAndThenModify_WithInitialData()
+ {
+ // Arrange
+ var original = new RouteData();
+ original.DataTokens.Add("data", "token1");
+ original.Routers.Add(Mock.Of<IRouter>());
+ original.Values.Add("route", "value1");
+
+ var routeData = new RouteData(original);
+
+ // Act
+ var snapshot = routeData.PushState(
+ Mock.Of<IRouter>(),
+ new RouteValueDictionary(new { route = "value2" }),
+ new RouteValueDictionary(new { data = "token2" }));
+
+ routeData.DataTokens.Add("data2", "token");
+ routeData.Routers.Add(Mock.Of<IRouter>());
+ routeData.Values.Add("route2", "value");
+
+ var copy = new RouteData(routeData);
+ snapshot.Restore();
+
+ // Assert
+ Assert.Equal(original.DataTokens, routeData.DataTokens);
+ Assert.NotEqual(routeData.DataTokens, copy.DataTokens);
+ Assert.Equal(original.Routers, routeData.Routers);
+ Assert.NotEqual(routeData.Routers, copy.Routers);
+ Assert.Equal(original.Values, routeData.Values);
+ Assert.NotEqual(routeData.Values, copy.Values);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs
new file mode 100644
index 0000000000..764f773e16
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs
@@ -0,0 +1,1572 @@
+// 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.Testing;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class RouteValueDictionaryTests
+ {
+ [Fact]
+ public void DefaultCtor_UsesEmptyStorage()
+ {
+ // Arrange
+ // Act
+ var dict = new RouteValueDictionary();
+
+ // Assert
+ Assert.Empty(dict);
+ Assert.IsType<RouteValueDictionary.EmptyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void CreateFromNull_UsesEmptyStorage()
+ {
+ // Arrange
+ // Act
+ var dict = new RouteValueDictionary(null);
+
+ // Assert
+ Assert.Empty(dict);
+ Assert.IsType<RouteValueDictionary.EmptyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void CreateFromRouteValueDictionary_WithListStorage_CopiesStorage()
+ {
+ // Arrange
+ var other = new RouteValueDictionary()
+ {
+ { "1", 1 }
+ };
+
+ // Act
+ var dict = new RouteValueDictionary(other);
+
+ // Assert
+ Assert.Equal(other, dict);
+
+ var storage = Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ var otherStorage = Assert.IsType<RouteValueDictionary.ListStorage>(other._storage);
+ Assert.NotSame(otherStorage, storage);
+ }
+
+ [Fact]
+ public void CreateFromRouteValueDictionary_WithPropertyStorage_CopiesStorage()
+ {
+ // Arrange
+ var other = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ var dict = new RouteValueDictionary(other);
+
+ // Assert
+ Assert.Equal(other, dict);
+
+ var storage = Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ var otherStorage = Assert.IsType<RouteValueDictionary.PropertyStorage>(other._storage);
+ Assert.Same(otherStorage, storage);
+ }
+
+ [Fact]
+ public void CreateFromRouteValueDictionary_WithEmptyStorage_SharedInstance()
+ {
+ // Arrange
+ var other = new RouteValueDictionary();
+
+ // Act
+ var dict = new RouteValueDictionary(other);
+
+ // Assert
+ Assert.Equal(other, dict);
+
+ var storage = Assert.IsType<RouteValueDictionary.EmptyStorage>(dict._storage);
+ var otherStorage = Assert.IsType<RouteValueDictionary.EmptyStorage>(other._storage);
+ Assert.Same(otherStorage, storage);
+ }
+
+ public static IEnumerable<object[]> IEnumerableKeyValuePairData
+ {
+ get
+ {
+ var routeValues = new[]
+ {
+ new KeyValuePair<string, object>("Name", "James"),
+ new KeyValuePair<string, object>("Age", 30),
+ new KeyValuePair<string, object>("Address", new Address() { City = "Redmond", State = "WA" })
+ };
+
+ yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) };
+
+ yield return new object[] { routeValues.ToList() };
+
+ yield return new object[] { routeValues };
+ }
+ }
+
+ public static IEnumerable<object[]> IEnumerableStringValuePairData
+ {
+ get
+ {
+ var routeValues = new[]
+ {
+ new KeyValuePair<string, string>("First Name", "James"),
+ new KeyValuePair<string, string>("Last Name", "Henrik"),
+ new KeyValuePair<string, string>("Middle Name", "Bob")
+ };
+
+ yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) };
+
+ yield return new object[] { routeValues.ToList() };
+
+ yield return new object[] { routeValues };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(IEnumerableKeyValuePairData))]
+ public void CreateFromIEnumerableKeyValuePair_CopiesValues(object values)
+ {
+ // Arrange & Act
+ var dict = new RouteValueDictionary(values);
+
+ // Assert
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ Assert.Equal("Address", kvp.Key);
+ var address = Assert.IsType<Address>(kvp.Value);
+ Assert.Equal("Redmond", address.City);
+ Assert.Equal("WA", address.State);
+ },
+ kvp => { Assert.Equal("Age", kvp.Key); Assert.Equal(30, kvp.Value); },
+ kvp => { Assert.Equal("Name", kvp.Key); Assert.Equal("James", kvp.Value); });
+ }
+
+ [Theory]
+ [MemberData(nameof(IEnumerableStringValuePairData))]
+ public void CreateFromIEnumerableStringValuePair_CopiesValues(object values)
+ {
+ // Arrange & Act
+ var dict = new RouteValueDictionary(values);
+
+ // Assert
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp => { Assert.Equal("First Name", kvp.Key); Assert.Equal("James", kvp.Value); },
+ kvp => { Assert.Equal("Last Name", kvp.Key); Assert.Equal("Henrik", kvp.Value); },
+ kvp => { Assert.Equal("Middle Name", kvp.Key); Assert.Equal("Bob", kvp.Value); });
+ }
+
+ [Fact]
+ public void CreateFromIEnumerableKeyValuePair_ThrowsExceptionForDuplicateKey()
+ {
+ // Arrange
+ var values = new List<KeyValuePair<string, object>>()
+ {
+ new KeyValuePair<string, object>("name", "Billy"),
+ new KeyValuePair<string, object>("Name", "Joey"),
+ };
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgument(
+ () => new RouteValueDictionary(values),
+ "values",
+ $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}.");
+ }
+
+ [Fact]
+ public void CreateFromIEnumerableStringValuePair_ThrowsExceptionForDuplicateKey()
+ {
+ // Arrange
+ var values = new List<KeyValuePair<string, string>>()
+ {
+ new KeyValuePair<string, string>("name", "Billy"),
+ new KeyValuePair<string, string>("Name", "Joey"),
+ };
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgument(
+ () => new RouteValueDictionary(values),
+ "values",
+ $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}.");
+ }
+
+ [Fact]
+ public void CreateFromObject_CopiesPropertiesFromAnonymousType()
+ {
+ // Arrange
+ var obj = new { cool = "beans", awesome = 123 };
+
+ // Act
+ var dict = new RouteValueDictionary(obj);
+
+ // Assert
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp => { Assert.Equal("awesome", kvp.Key); Assert.Equal(123, kvp.Value); },
+ kvp => { Assert.Equal("cool", kvp.Key); Assert.Equal("beans", kvp.Value); });
+ }
+
+ [Fact]
+ public void CreateFromObject_CopiesPropertiesFromRegularType()
+ {
+ // Arrange
+ var obj = new RegularType() { CoolnessFactor = 73 };
+
+ // Act
+ var dict = new RouteValueDictionary(obj);
+
+ // Assert
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ Assert.Equal("CoolnessFactor", kvp.Key);
+ Assert.Equal(73, kvp.Value);
+ },
+ kvp =>
+ {
+ Assert.Equal("IsAwesome", kvp.Key);
+ var value = Assert.IsType<bool>(kvp.Value);
+ Assert.False(value);
+ });
+ }
+
+ [Fact]
+ public void CreateFromObject_CopiesPropertiesFromRegularType_PublicOnly()
+ {
+ // Arrange
+ var obj = new Visibility() { IsPublic = true, ItsInternalDealWithIt = 5 };
+
+ // Act
+ var dict = new RouteValueDictionary(obj);
+
+ // Assert
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ Assert.Equal("IsPublic", kvp.Key);
+ var value = Assert.IsType<bool>(kvp.Value);
+ Assert.True(value);
+ });
+ }
+
+ [Fact]
+ public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresStatic()
+ {
+ // Arrange
+ var obj = new StaticProperty();
+
+ // Act
+ var dict = new RouteValueDictionary(obj);
+
+ // Assert
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ Assert.Empty(dict);
+ }
+
+ [Fact]
+ public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresSetOnly()
+ {
+ // Arrange
+ var obj = new SetterOnly() { CoolSetOnly = false };
+
+ // Act
+ var dict = new RouteValueDictionary(obj);
+
+ // Assert
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ Assert.Empty(dict);
+ }
+
+ [Fact]
+ public void CreateFromObject_CopiesPropertiesFromRegularType_IncludesInherited()
+ {
+ // Arrange
+ var obj = new Derived() { TotallySweetProperty = true, DerivedProperty = false };
+
+ // Act
+ var dict = new RouteValueDictionary(obj);
+
+ // Assert
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp =>
+ {
+ Assert.Equal("DerivedProperty", kvp.Key);
+ var value = Assert.IsType<bool>(kvp.Value);
+ Assert.False(value);
+ },
+ kvp =>
+ {
+ Assert.Equal("TotallySweetProperty", kvp.Key);
+ var value = Assert.IsType<bool>(kvp.Value);
+ Assert.True(value);
+ });
+ }
+
+ [Fact]
+ public void CreateFromObject_CopiesPropertiesFromRegularType_WithHiddenProperty()
+ {
+ // Arrange
+ var obj = new DerivedHiddenProperty() { DerivedProperty = 5 };
+
+ // Act
+ var dict = new RouteValueDictionary(obj);
+
+ // Assert
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp => { Assert.Equal("DerivedProperty", kvp.Key); Assert.Equal(5, kvp.Value); });
+ }
+
+ [Fact]
+ public void CreateFromObject_CopiesPropertiesFromRegularType_WithIndexerProperty()
+ {
+ // Arrange
+ var obj = new IndexerProperty();
+
+ // Act
+ var dict = new RouteValueDictionary(obj);
+
+ // Assert
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ Assert.Empty(dict);
+ }
+
+ [Fact]
+ public void CreateFromObject_MixedCaseThrows()
+ {
+ // Arrange
+ var obj = new { controller = "Home", Controller = "Home" };
+
+ var message =
+ $"The type '{obj.GetType().FullName}' defines properties 'controller' and 'Controller' which differ " +
+ $"only by casing. This is not supported by {nameof(RouteValueDictionary)} which uses " +
+ $"case-insensitive comparisons.";
+
+ // Act & Assert
+ var exception = Assert.Throws<InvalidOperationException>(() =>
+ {
+ var dictionary = new RouteValueDictionary(obj);
+ });
+
+ // Ignoring case to make sure we're not testing reflection's ordering.
+ Assert.Equal(message, exception.Message, ignoreCase: true);
+ }
+
+ // Our comparer is hardcoded to be OrdinalIgnoreCase no matter what.
+ [Fact]
+ public void Comparer_IsOrdinalIgnoreCase()
+ {
+ // Arrange
+ // Act
+ var dict = new RouteValueDictionary();
+
+ // Assert
+ Assert.Same(StringComparer.OrdinalIgnoreCase, dict.Comparer);
+ }
+
+ // Our comparer is hardcoded to be IsReadOnly==false no matter what.
+ [Fact]
+ public void IsReadOnly_False()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary();
+
+ // Act
+ var result = ((ICollection<KeyValuePair<string, object>>)dict).IsReadOnly;
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void IndexGet_EmptyStorage_ReturnsNull()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary();
+
+ // Act
+ var value = dict["key"];
+
+ // Assert
+ Assert.Null(value);
+ Assert.IsType<RouteValueDictionary.EmptyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void IndexGet_PropertyStorage_NoMatch_ReturnsNull()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { age = 30 });
+
+ // Act
+ var value = dict["key"];
+
+ // Assert
+ Assert.Null(value);
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void IndexGet_PropertyStorage_Match_ReturnsValue()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ var value = dict["key"];
+
+ // Assert
+ Assert.Equal("value", value);
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void IndexGet_PropertyStorage_MatchIgnoreCase_ReturnsValue()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ var value = dict["kEy"];
+
+ // Assert
+ Assert.Equal("value", value);
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void IndexGet_ListStorage_NoMatch_ReturnsNull()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "age", 30 },
+ };
+
+ // Act
+ var value = dict["key"];
+
+ // Assert
+ Assert.Null(value);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void IndexGet_ListStorage_Match_ReturnsValue()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ var value = dict["key"];
+
+ // Assert
+ Assert.Equal("value", value);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void IndexGet_ListStorage_MatchIgnoreCase_ReturnsValue()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ var value = dict["kEy"];
+
+ // Assert
+ Assert.Equal("value", value);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void IndexSet_EmptyStorage_UpgradesToList()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary();
+
+ // Act
+ dict["key"] = "value";
+
+ // Assert
+ Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void IndexSet_PropertyStorage_NoMatch_AddsValue()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { age = 30 });
+
+ // Act
+ dict["key"] = "value";
+
+ // Assert
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); },
+ kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void IndexSet_PropertyStorage_Match_SetsValue()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ dict["key"] = "value";
+
+ // Assert
+ Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void IndexSet_PropertyStorage_MatchIgnoreCase_SetsValue()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ dict["kEy"] = "value";
+
+ // Assert
+ Assert.Collection(dict, kvp => { Assert.Equal("kEy", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void IndexSet_ListStorage_NoMatch_AddsValue()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "age", 30 },
+ };
+
+ // Act
+ dict["key"] = "value";
+
+ // Assert
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); },
+ kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void IndexSet_ListStorage_Match_SetsValue()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ dict["key"] = "value";
+
+ // Assert
+ Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void IndexSet_ListStorage_MatchIgnoreCase_SetsValue()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ dict["key"] = "value";
+
+ // Assert
+ Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Count_EmptyStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary();
+
+ // Act
+ var count = dict.Count;
+
+ // Assert
+ Assert.Equal(0, count);
+ Assert.IsType<RouteValueDictionary.EmptyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Count_PropertyStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value", });
+
+ // Act
+ var count = dict.Count;
+
+ // Assert
+ Assert.Equal(1, count);
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Count_ListStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ var count = dict.Count;
+
+ // Assert
+ Assert.Equal(1, count);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Keys_EmptyStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary();
+
+ // Act
+ var keys = dict.Keys;
+
+ // Assert
+ Assert.Empty(keys);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Keys_PropertyStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value", });
+
+ // Act
+ var keys = dict.Keys;
+
+ // Assert
+ Assert.Equal(new[] { "key" }, keys);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Keys_ListStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ var keys = dict.Keys;
+
+ // Assert
+ Assert.Equal(new[] { "key" }, keys);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Values_EmptyStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary();
+
+ // Act
+ var values = dict.Values;
+
+ // Assert
+ Assert.Empty(values);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Values_PropertyStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value", });
+
+ // Act
+ var values = dict.Values;
+
+ // Assert
+ Assert.Equal(new object[] { "value" }, values);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Values_ListStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ var values = dict.Values;
+
+ // Assert
+ Assert.Equal(new object[] { "value" }, values);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Add_EmptyStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary();
+
+ // Act
+ dict.Add("key", "value");
+
+ // Assert
+ Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Add_PropertyStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { age = 30 });
+
+ // Act
+ dict.Add("key", "value");
+
+ // Assert
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); },
+ kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Add_ListStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "age", 30 },
+ };
+
+ // Act
+ dict.Add("key", "value");
+
+ // Assert
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); },
+ kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Add_DuplicateKey()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ var message = $"An element with the key 'key' already exists in the {nameof(RouteValueDictionary)}";
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgument(() => dict.Add("key", "value2"), "key", message);
+
+ // Assert
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Add_DuplicateKey_CaseInsensitive()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ var message = $"An element with the key 'kEy' already exists in the {nameof(RouteValueDictionary)}";
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgument(() => dict.Add("kEy", "value2"), "key", message);
+
+ // Assert
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Add_KeyValuePair()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "age", 30 },
+ };
+
+ // Act
+ ((ICollection<KeyValuePair<string, object>>)dict).Add(new KeyValuePair<string, object>("key", "value"));
+
+ // Assert
+ Assert.Collection(
+ dict.OrderBy(kvp => kvp.Key),
+ kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); },
+ kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Clear_EmptyStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary();
+
+ // Act
+ dict.Clear();
+
+ // Assert
+ Assert.Empty(dict);
+ Assert.IsType<RouteValueDictionary.EmptyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Clear_PropertyStorage_AlreadyEmpty()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { });
+
+ // Act
+ dict.Clear();
+
+ // Assert
+ Assert.Empty(dict);
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Clear_PropertyStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ dict.Clear();
+
+ // Assert
+ Assert.Empty(dict);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Clear_ListStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ dict.Clear();
+
+ // Assert
+ Assert.Empty(dict);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Contains_KeyValuePair_True()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ var input = new KeyValuePair<string, object>("key", "value");
+
+ // Act
+ var result = ((ICollection<KeyValuePair<string, object>>)dict).Contains(input);
+
+ // Assert
+ Assert.True(result);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Contains_KeyValuePair_True_CaseInsensitive()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ var input = new KeyValuePair<string, object>("KEY", "value");
+
+ // Act
+ var result = ((ICollection<KeyValuePair<string, object>>)dict).Contains(input);
+
+ // Assert
+ Assert.True(result);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Contains_KeyValuePair_False()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ var input = new KeyValuePair<string, object>("other", "value");
+
+ // Act
+ var result = ((ICollection<KeyValuePair<string, object>>)dict).Contains(input);
+
+ // Assert
+ Assert.False(result);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ // Value comparisons use the default equality comparer.
+ [Fact]
+ public void Contains_KeyValuePair_False_ValueComparisonIsDefault()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ var input = new KeyValuePair<string, object>("key", "valUE");
+
+ // Act
+ var result = ((ICollection<KeyValuePair<string, object>>)dict).Contains(input);
+
+ // Assert
+ Assert.False(result);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void ContainsKey_EmptyStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary();
+
+ // Act
+ var result = dict.ContainsKey("key");
+
+ // Assert
+ Assert.False(result);
+ Assert.IsType<RouteValueDictionary.EmptyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void ContainsKey_PropertyStorage_False()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ var result = dict.ContainsKey("other");
+
+ // Assert
+ Assert.False(result);
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void ContainsKey_PropertyStorage_True()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ var result = dict.ContainsKey("key");
+
+ // Assert
+ Assert.True(result);
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void ContainsKey_PropertyStorage_True_CaseInsensitive()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ var result = dict.ContainsKey("kEy");
+
+ // Assert
+ Assert.True(result);
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void ContainsKey_ListStorage_False()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ var result = dict.ContainsKey("other");
+
+ // Assert
+ Assert.False(result);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void ContainsKey_ListStorage_True()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ var result = dict.ContainsKey("key");
+
+ // Assert
+ Assert.True(result);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void ContainsKey_ListStorage_True_CaseInsensitive()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ var result = dict.ContainsKey("kEy");
+
+ // Assert
+ Assert.True(result);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void CopyTo()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ var array = new KeyValuePair<string, object>[2];
+
+ // Act
+ ((ICollection<KeyValuePair<string, object>>)dict).CopyTo(array, 1);
+
+ // Assert
+ Assert.Equal(
+ new KeyValuePair<string, object>[]
+ {
+ default(KeyValuePair<string, object>),
+ new KeyValuePair<string, object>("key", "value")
+ },
+ array);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Remove_KeyValuePair_True()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ var input = new KeyValuePair<string, object>("key", "value");
+
+ // Act
+ var result = ((ICollection<KeyValuePair<string, object>>)dict).Remove(input);
+
+ // Assert
+ Assert.True(result);
+ Assert.Empty(dict);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Remove_KeyValuePair_True_CaseInsensitive()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ var input = new KeyValuePair<string, object>("KEY", "value");
+
+ // Act
+ var result = ((ICollection<KeyValuePair<string, object>>)dict).Remove(input);
+
+ // Assert
+ Assert.True(result);
+ Assert.Empty(dict);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Remove_KeyValuePair_False()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ var input = new KeyValuePair<string, object>("other", "value");
+
+ // Act
+ var result = ((ICollection<KeyValuePair<string, object>>)dict).Remove(input);
+
+ // Assert
+ Assert.False(result);
+ Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ // Value comparisons use the default equality comparer.
+ [Fact]
+ public void Remove_KeyValuePair_False_ValueComparisonIsDefault()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ var input = new KeyValuePair<string, object>("key", "valUE");
+
+ // Act
+ var result = ((ICollection<KeyValuePair<string, object>>)dict).Remove(input);
+
+ // Assert
+ Assert.False(result);
+ Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Remove_EmptyStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary();
+
+ // Act
+ var result = dict.Remove("key");
+
+ // Assert
+ Assert.False(result);
+ Assert.IsType<RouteValueDictionary.EmptyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Remove_PropertyStorage_Empty()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { });
+
+ // Act
+ var result = dict.Remove("other");
+
+ // Assert
+ Assert.False(result);
+ Assert.Empty(dict);
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Remove_PropertyStorage_False()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ var result = dict.Remove("other");
+
+ // Assert
+ Assert.False(result);
+ Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Remove_PropertyStorage_True()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ var result = dict.Remove("key");
+
+ // Assert
+ Assert.True(result);
+ Assert.Empty(dict);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Remove_PropertyStorage_True_CaseInsensitive()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ var result = dict.Remove("kEy");
+
+ // Assert
+ Assert.True(result);
+ Assert.Empty(dict);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Remove_ListStorage_False()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ var result = dict.Remove("other");
+
+ // Assert
+ Assert.False(result);
+ Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); });
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Remove_ListStorage_True()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ var result = dict.Remove("key");
+
+ // Assert
+ Assert.True(result);
+ Assert.Empty(dict);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void Remove_ListStorage_True_CaseInsensitive()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ var result = dict.Remove("kEy");
+
+ // Assert
+ Assert.True(result);
+ Assert.Empty(dict);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void TryGetValue_EmptyStorage()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary();
+
+ // Act
+ object value;
+ var result = dict.TryGetValue("key", out value);
+
+ // Assert
+ Assert.False(result);
+ Assert.Null(value);
+ Assert.IsType<RouteValueDictionary.EmptyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void TryGetValue_PropertyStorage_False()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ object value;
+ var result = dict.TryGetValue("other", out value);
+
+ // Assert
+ Assert.False(result);
+ Assert.Null(value);
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void TryGetValue_PropertyStorage_True()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ object value;
+ var result = dict.TryGetValue("key", out value);
+
+ // Assert
+ Assert.True(result);
+ Assert.Equal("value", value);
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void TryGetValue_PropertyStorage_True_CaseInsensitive()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary(new { key = "value" });
+
+ // Act
+ object value;
+ var result = dict.TryGetValue("kEy", out value);
+
+ // Assert
+ Assert.True(result);
+ Assert.Equal("value", value);
+ Assert.IsType<RouteValueDictionary.PropertyStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void TryGetValue_ListStorage_False()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ object value;
+ var result = dict.TryGetValue("other", out value);
+
+ // Assert
+ Assert.False(result);
+ Assert.Null(value);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void TryGetValue_ListStorage_True()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ object value;
+ var result = dict.TryGetValue("key", out value);
+
+ // Assert
+ Assert.True(result);
+ Assert.Equal("value", value);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void TryGetValue_ListStorage_True_CaseInsensitive()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary()
+ {
+ { "key", "value" },
+ };
+
+ // Act
+ object value;
+ var result = dict.TryGetValue("kEy", out value);
+
+ // Assert
+ Assert.True(result);
+ Assert.Equal("value", value);
+ Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ }
+
+ [Fact]
+ public void ListStorage_DynamicallyAdjustsCapacity()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary();
+
+ // Act 1
+ dict.Add("key", "value");
+
+ // Assert 1
+ var storage = Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ Assert.Equal(4, storage.Capacity);
+
+ // Act 2
+ dict.Add("key2", "value2");
+ dict.Add("key3", "value3");
+ dict.Add("key4", "value4");
+ dict.Add("key5", "value5");
+
+ // Assert 2
+ Assert.Equal(8, storage.Capacity);
+ }
+
+ [Fact]
+ public void ListStorage_RemoveAt_RearrangesInnerArray()
+ {
+ // Arrange
+ var dict = new RouteValueDictionary();
+ dict.Add("key", "value");
+ dict.Add("key2", "value2");
+ dict.Add("key3", "value3");
+
+ // Assert 1
+ var storage = Assert.IsType<RouteValueDictionary.ListStorage>(dict._storage);
+ Assert.Equal(3, storage.Count);
+
+ // Act
+ dict.Remove("key2");
+
+ // Assert 2
+ Assert.Equal(2, storage.Count);
+ Assert.Equal("key", storage[0].Key);
+ Assert.Equal("value", storage[0].Value);
+ Assert.Equal("key3", storage[1].Key);
+ Assert.Equal("value3", storage[1].Value);
+
+ Assert.Throws<ArgumentOutOfRangeException>(() => storage[2]);
+ }
+
+ private class RegularType
+ {
+ public bool IsAwesome { get; set; }
+
+ public int CoolnessFactor { get; set; }
+ }
+
+ private class Visibility
+ {
+ private string PrivateYo { get; set; }
+
+ internal int ItsInternalDealWithIt { get; set; }
+
+ public bool IsPublic { get; set; }
+ }
+
+ private class StaticProperty
+ {
+ public static bool IsStatic { get; set; }
+ }
+
+ private class SetterOnly
+ {
+ private bool _coolSetOnly;
+
+ public bool CoolSetOnly { set { _coolSetOnly = value; } }
+ }
+
+ private class Base
+ {
+ public bool DerivedProperty { get; set; }
+ }
+
+ private class Derived : Base
+ {
+ public bool TotallySweetProperty { get; set; }
+ }
+
+ private class DerivedHiddenProperty : Base
+ {
+ public new int DerivedProperty { get; set; }
+ }
+
+ private class IndexerProperty
+ {
+ public bool this[string key]
+ {
+ get { return false; }
+ set { }
+ }
+ }
+
+ private class Address
+ {
+ public string City { get; set; }
+
+ public string State { get; set; }
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/VirtualPathDataTests.cs b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/VirtualPathDataTests.cs
new file mode 100644
index 0000000000..c21de9ba32
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/VirtualPathDataTests.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 Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class VirtualPathDataTests
+ {
+ [Fact]
+ public void Constructor_CreatesEmptyDataTokensIfNull()
+ {
+ // Arrange
+ var router = Mock.Of<IRouter>();
+ var path = "/virtual path";
+
+ // Act
+ var pathData = new VirtualPathData(router, path, null);
+
+ // Assert
+ Assert.Same(router, pathData.Router);
+ Assert.Equal(path, pathData.VirtualPath);
+ Assert.NotNull(pathData.DataTokens);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void Constructor_CopiesDataTokens()
+ {
+ // Arrange
+ var router = Mock.Of<IRouter>();
+ var path = "/virtual path";
+ var dataTokens = new RouteValueDictionary();
+ dataTokens["TestKey"] = "TestValue";
+
+ // Act
+ var pathData = new VirtualPathData(router, path, dataTokens);
+
+ // Assert
+ Assert.Same(router, pathData.Router);
+ Assert.Equal(path, pathData.VirtualPath);
+ Assert.NotNull(pathData.DataTokens);
+ Assert.Equal("TestValue", pathData.DataTokens["TestKey"]);
+ Assert.Single(pathData.DataTokens);
+ Assert.NotSame(dataTokens, pathData.DataTokens);
+ }
+
+ [Fact]
+ public void VirtualPath_ReturnsEmptyStringIfNull()
+ {
+ // Arrange
+ var router = Mock.Of<IRouter>();
+
+ // Act
+ var pathData = new VirtualPathData(router, virtualPath: null);
+
+ // Assert
+ Assert.Same(router, pathData.Router);
+ Assert.Empty(pathData.VirtualPath);
+ Assert.NotNull(pathData.DataTokens);
+ Assert.Empty(pathData.DataTokens);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs
new file mode 100644
index 0000000000..5e28fd8917
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/DecisionTreeBuilderTest.cs
@@ -0,0 +1,224 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Routing.DecisionTree
+{
+ public class DecisionTreeBuilderTest
+ {
+ [Fact]
+ public void BuildTree_Empty()
+ {
+ // Arrange
+ var items = new List<Item>();
+
+ // Act
+ var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
+
+ // Assert
+ Assert.Empty(tree.Criteria);
+ Assert.Empty(tree.Matches);
+ }
+
+ [Fact]
+ public void BuildTree_TrivialMatch()
+ {
+ // Arrange
+ var items = new List<Item>();
+
+ var item = new Item();
+ items.Add(item);
+
+ // Act
+ var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
+
+ // Assert
+ Assert.Empty(tree.Criteria);
+ Assert.Same(item, Assert.Single(tree.Matches));
+ }
+
+ [Fact]
+ public void BuildTree_WithMultipleCriteria()
+ {
+ // Arrange
+ var items = new List<Item>();
+
+ var item = new Item();
+ item.Criteria.Add("area", new DecisionCriterionValue(value: "Admin"));
+ item.Criteria.Add("controller", new DecisionCriterionValue(value: "Users"));
+ item.Criteria.Add("action", new DecisionCriterionValue(value: "AddUser"));
+ items.Add(item);
+
+ // Act
+ var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
+
+ // Assert
+ Assert.Empty(tree.Matches);
+
+ var area = Assert.Single(tree.Criteria);
+ Assert.Equal("area", area.Key);
+
+ var admin = Assert.Single(area.Branches);
+ Assert.Equal("Admin", admin.Key);
+ Assert.Empty(admin.Value.Matches);
+
+ var controller = Assert.Single(admin.Value.Criteria);
+ Assert.Equal("controller", controller.Key);
+
+ var users = Assert.Single(controller.Branches);
+ Assert.Equal("Users", users.Key);
+ Assert.Empty(users.Value.Matches);
+
+ var action = Assert.Single(users.Value.Criteria);
+ Assert.Equal("action", action.Key);
+
+ var addUser = Assert.Single(action.Branches);
+ Assert.Equal("AddUser", addUser.Key);
+ Assert.Empty(addUser.Value.Criteria);
+ Assert.Same(item, Assert.Single(addUser.Value.Matches));
+ }
+
+ [Fact]
+ public void BuildTree_WithMultipleItems()
+ {
+ // Arrange
+ var items = new List<Item>();
+
+ var item1 = new Item();
+ item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store"));
+ item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy"));
+ items.Add(item1);
+
+ var item2 = new Item();
+ item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store"));
+ item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout"));
+ items.Add(item2);
+
+ // Act
+ var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
+
+ // Assert
+ Assert.Empty(tree.Matches);
+
+ var action = Assert.Single(tree.Criteria);
+ Assert.Equal("action", action.Key);
+
+ var buy = action.Branches["Buy"];
+ Assert.Empty(buy.Matches);
+
+ var controller = Assert.Single(buy.Criteria);
+ Assert.Equal("controller", controller.Key);
+
+ var store = Assert.Single(controller.Branches);
+ Assert.Equal("Store", store.Key);
+ Assert.Empty(store.Value.Criteria);
+ Assert.Same(item1, Assert.Single(store.Value.Matches));
+
+ var checkout = action.Branches["Checkout"];
+ Assert.Empty(checkout.Matches);
+
+ controller = Assert.Single(checkout.Criteria);
+ Assert.Equal("controller", controller.Key);
+
+ store = Assert.Single(controller.Branches);
+ Assert.Equal("Store", store.Key);
+ Assert.Empty(store.Value.Criteria);
+ Assert.Same(item2, Assert.Single(store.Value.Matches));
+ }
+
+ [Fact]
+ public void BuildTree_WithInteriorMatch()
+ {
+ // Arrange
+ var items = new List<Item>();
+
+ var item1 = new Item();
+ item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store"));
+ item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy"));
+ items.Add(item1);
+
+ var item2 = new Item();
+ item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store"));
+ item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout"));
+ items.Add(item2);
+
+ var item3 = new Item();
+ item3.Criteria.Add("action", new DecisionCriterionValue(value: "Buy"));
+ items.Add(item3);
+
+ // Act
+ var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
+
+ // Assert
+ Assert.Empty(tree.Matches);
+
+ var action = Assert.Single(tree.Criteria);
+ Assert.Equal("action", action.Key);
+
+ var buy = action.Branches["Buy"];
+ Assert.Same(item3, Assert.Single(buy.Matches));
+ }
+
+ [Fact]
+ public void BuildTree_WithDivergentCriteria()
+ {
+ // Arrange
+ var items = new List<Item>();
+
+ var item1 = new Item();
+ item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store"));
+ item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy"));
+ items.Add(item1);
+
+ var item2 = new Item();
+ item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store"));
+ item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout"));
+ items.Add(item2);
+
+ var item3 = new Item();
+ item3.Criteria.Add("stub", new DecisionCriterionValue(value: "Bleh"));
+ items.Add(item3);
+
+ // Act
+ var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
+
+ // Assert
+ Assert.Empty(tree.Matches);
+
+ var action = tree.Criteria[0];
+ Assert.Equal("action", action.Key);
+
+ var stub = tree.Criteria[1];
+ Assert.Equal("stub", stub.Key);
+ }
+
+ private class Item
+ {
+ public Item()
+ {
+ Criteria = new Dictionary<string, DecisionCriterionValue>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ public Dictionary<string, DecisionCriterionValue> Criteria { get; private set; }
+ }
+
+ private class ItemClassifier : IClassifier<Item>
+ {
+ public IEqualityComparer<object> ValueComparer
+ {
+ get
+ {
+ return new RouteValueEqualityComparer();
+ }
+ }
+
+ public IDictionary<string, DecisionCriterionValue> GetCriteria(Item item)
+ {
+ return item.Criteria;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj b/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj
new file mode 100644
index 0000000000..3d127f0370
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests/Microsoft.AspNetCore.Routing.DecisionTree.Sources.Tests.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\..\shared\Microsoft.AspNetCore.Routing.DecisionTree.Sources\**\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj
new file mode 100644
index 0000000000..ee8a57c08f
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/Microsoft.AspNetCore.Routing.FunctionalTests.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
+ <ProjectReference Include="..\..\samples\RoutingSample.Web\RoutingSample.Web.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingSampleTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingSampleTest.cs
new file mode 100644
index 0000000000..8869103f40
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingSampleTest.cs
@@ -0,0 +1,84 @@
+// 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.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.TestHost;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.FunctionalTests
+{
+ public class RoutingSampleTest : IDisposable
+ {
+ private readonly HttpClient _client;
+ private readonly TestServer _testServer;
+
+ public RoutingSampleTest()
+ {
+ var webHostBuilder = RoutingSample.Web.Program.GetWebHostBuilder();
+ _testServer = new TestServer(webHostBuilder);
+ _client = _testServer.CreateClient();
+ _client.BaseAddress = new Uri("http://localhost");
+ }
+
+ [Fact]
+ public async Task Routing_CanRouteRequestDelegate_ToSpecificHttpVerb()
+ {
+ // Arrange
+ var message = new HttpRequestMessage(HttpMethod.Get, "api/get/5");
+
+ // Act
+ var response = await _client.SendAsync(message);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal($"API Get 5", await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task Routing_CanRouteRequest_ToSpecificMiddleware()
+ {
+ // Arrange
+ var message = new HttpRequestMessage(HttpMethod.Get, "api/middleware");
+
+ // Act
+ var response = await _client.SendAsync(message);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal($"Middleware!", await response.Content.ReadAsStringAsync());
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("POST")]
+ [InlineData("PUT")]
+ [InlineData("PATCH")]
+ [InlineData("DELETE")]
+ [InlineData("HEAD")]
+ [InlineData("OPTIONS")]
+ public async Task Routing_CanRouteRequest_ToDefaultHandler(string httpVerb)
+ {
+ // Arrange
+ var message = new HttpRequestMessage(new HttpMethod(httpVerb), "api/all/Joe/Duf");
+ var expectedBody = $"Verb = {httpVerb} - Path = /api/all/Joe/Duf - Route values - [name, Joe], [lastName, Duf]";
+
+ // Act
+ var response = await _client.SendAsync(message);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.Equal(expectedBody, body);
+ }
+
+ public void Dispose()
+ {
+ _testServer.Dispose();
+ _client.Dispose();
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingTestFixture.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingTestFixture.cs
new file mode 100644
index 0000000000..1ced141956
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/RoutingTestFixture.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;
+using System.Net.Http;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+
+namespace Microsoft.AspNetCore.Routing.FunctionalTests
+{
+ public class RoutingTestFixture<TStartup> : IDisposable
+ {
+ private readonly TestServer _server;
+
+ public RoutingTestFixture()
+ {
+ var builder = new WebHostBuilder()
+ .UseStartup(typeof(TStartup));
+
+ _server = new TestServer(builder);
+
+ Client = _server.CreateClient();
+ Client.BaseAddress = new Uri("http://localhost");
+ }
+
+ public HttpClient Client { get; }
+
+ public void Dispose()
+ {
+ Client.Dispose();
+ _server.Dispose();
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/WebHostBuilderExtensionsTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/WebHostBuilderExtensionsTest.cs
new file mode 100644
index 0000000000..7466006026
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.FunctionalTests/WebHostBuilderExtensionsTest.cs
@@ -0,0 +1,101 @@
+// 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 System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.FunctionalTests
+{
+ public class WebHostBuilderExtensionsTest
+ {
+ public static TheoryData<Action<IRouteBuilder>, HttpRequestMessage, string> MatchesRequest
+ {
+ get
+ {
+ return new TheoryData<Action<IRouteBuilder>, HttpRequestMessage, string>()
+ {
+ {
+ (rb) => rb.MapGet("greeting/{name}", (req, resp, routeData) => resp.WriteAsync($"Hello! {routeData.Values["name"]}")),
+ new HttpRequestMessage(HttpMethod.Get, "greeting/James"),
+ "Hello! James"
+ },
+ {
+ (rb) => rb.MapPost(
+ "greeting/{name}",
+ async (req, resp, routeData) =>
+ {
+ var streamReader = new StreamReader(req.Body);
+ var data = await streamReader.ReadToEndAsync();
+ await resp.WriteAsync($"{routeData.Values["name"]} {data}");
+ }),
+ new HttpRequestMessage(HttpMethod.Post, "greeting/James") { Content = new StringContent("Biography") },
+ "James Biography"
+ },
+ {
+ (rb) => rb.MapPut(
+ "greeting/{name}",
+ async (req, resp, routeData) =>
+ {
+ var streamReader = new StreamReader(req.Body);
+ var data = await streamReader.ReadToEndAsync();
+ await resp.WriteAsync($"{routeData.Values["name"]} {data}");
+ }),
+ new HttpRequestMessage(HttpMethod.Put, "greeting/James") { Content = new StringContent("Biography") },
+ "James Biography"
+ },
+ {
+ (rb) => rb.MapDelete("greeting/{name}", (req, resp, routeData) => resp.WriteAsync($"Hello! {routeData.Values["name"]}")),
+ new HttpRequestMessage(HttpMethod.Delete, "greeting/James"),
+ "Hello! James"
+ },
+ {
+ (rb) => rb.MapVerb(
+ "POST",
+ "greeting/{name}",
+ async (req, resp, routeData) =>
+ {
+ var streamReader = new StreamReader(req.Body);
+ var data = await streamReader.ReadToEndAsync();
+ await resp.WriteAsync($"{routeData.Values["name"]} {data}");
+ }),
+ new HttpRequestMessage(HttpMethod.Post, "greeting/James") { Content = new StringContent("Biography") },
+ "James Biography"
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(MatchesRequest))]
+ public async Task UseRouter_MapGet_MatchesRequest(Action<IRouteBuilder> routeBuilder, HttpRequestMessage request, string expected)
+ {
+ // Arrange
+ var webhostbuilder = new WebHostBuilder();
+ webhostbuilder
+ .ConfigureServices(services => services.AddRouting())
+ .Configure(app =>
+ {
+ app.UseRouter(routeBuilder);
+ });
+ var testServer = new TestServer(webhostbuilder);
+ var client = testServer.CreateClient();
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var actual = await response.Content.ReadAsStringAsync();
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/BuilderExtensionsTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/BuilderExtensionsTest.cs
new file mode 100644
index 0000000000..ca297f89d2
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/BuilderExtensionsTest.cs
@@ -0,0 +1,35 @@
+// 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.Routing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ public class BuilderExtensionsTest
+ {
+ [Fact]
+ public void UseRouter_ThrowsInvalidOperationException_IfRoutingMarkerServiceIsNotRegistered()
+ {
+ // Arrange
+ var applicationBuilderMock = new Mock<IApplicationBuilder>();
+ applicationBuilderMock
+ .Setup(s => s.ApplicationServices)
+ .Returns(Mock.Of<IServiceProvider>());
+
+ var router = Mock.Of<IRouter>();
+
+ // Act & Assert
+ var exception = Assert.Throws<InvalidOperationException>(
+ () => applicationBuilderMock.Object.UseRouter(router));
+
+ Assert.Equal(
+ "Unable to find the required services. Please add all the required services by calling " +
+ "'IServiceCollection.AddRouting' inside the call to 'ConfigureServices(...)'" +
+ " in the application startup code.",
+ exception.Message);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/ConstraintMatcherTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/ConstraintMatcherTest.cs
new file mode 100644
index 0000000000..4d9b436e81
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/ConstraintMatcherTest.cs
@@ -0,0 +1,252 @@
+// 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 Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Logging.Testing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class ConstraintMatcherTest
+ {
+ private const string _name = "name";
+
+ [Fact]
+ public void MatchUrlGeneration_DoesNotLogData()
+ {
+ // Arrange
+ var sink = new TestSink();
+ var logger = new TestLogger(_name, sink, enabled: true);
+
+ var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" });
+ var constraints = new Dictionary<string, IRouteConstraint>
+ {
+ {"a", new PassConstraint()},
+ {"b", new FailConstraint()}
+ };
+
+ // Act
+ RouteConstraintMatcher.Match(
+ constraints: constraints,
+ routeValues: routeValueDictionary,
+ httpContext: new Mock<HttpContext>().Object,
+ route: new Mock<IRouter>().Object,
+ routeDirection: RouteDirection.UrlGeneration,
+ logger: logger);
+
+ // Assert
+ // There are no BeginScopes called.
+ Assert.Empty(sink.Scopes);
+
+ // There are no WriteCores called.
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void MatchFail_LogsCorrectData()
+ {
+ // Arrange & Act
+ var constraints = new Dictionary<string, IRouteConstraint>
+ {
+ {"a", new PassConstraint()},
+ {"b", new FailConstraint()}
+ };
+ var sink = SetUpMatch(constraints, loggerEnabled: true);
+ var expectedMessage = "Route value 'value' with key 'b' did not match the constraint " +
+ $"'{typeof(FailConstraint).FullName}'.";
+
+ // Assert
+ Assert.Empty(sink.Scopes);
+ var write = Assert.Single(sink.Writes);
+ Assert.Equal(expectedMessage, write.State?.ToString());
+ }
+
+ [Fact]
+ public void MatchSuccess_DoesNotLog()
+ {
+ // Arrange & Act
+ var constraints = new Dictionary<string, IRouteConstraint>
+ {
+ {"a", new PassConstraint()},
+ {"b", new PassConstraint()}
+ };
+ var sink = SetUpMatch(constraints, false);
+
+ // Assert
+ Assert.Empty(sink.Scopes);
+ Assert.Empty(sink.Writes);
+ }
+
+ [Fact]
+ public void ReturnsTrueOnValidConstraints()
+ {
+ var constraints = new Dictionary<string, IRouteConstraint>
+ {
+ {"a", new PassConstraint()},
+ {"b", new PassConstraint()}
+ };
+
+ var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" });
+
+ Assert.True(RouteConstraintMatcher.Match(
+ constraints: constraints,
+ routeValues: routeValueDictionary,
+ httpContext: new Mock<HttpContext>().Object,
+ route: new Mock<IRouter>().Object,
+ routeDirection: RouteDirection.IncomingRequest,
+ logger: NullLogger.Instance));
+ }
+
+ [Fact]
+ public void ConstraintsGetTheRightKey()
+ {
+ var constraints = new Dictionary<string, IRouteConstraint>
+ {
+ {"a", new PassConstraint("a")},
+ {"b", new PassConstraint("b")}
+ };
+
+ var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" });
+
+ Assert.True(RouteConstraintMatcher.Match(
+ constraints: constraints,
+ routeValues: routeValueDictionary,
+ httpContext: new Mock<HttpContext>().Object,
+ route: new Mock<IRouter>().Object,
+ routeDirection: RouteDirection.IncomingRequest,
+ logger: NullLogger.Instance));
+ }
+
+ [Fact]
+ public void ReturnsFalseOnInvalidConstraintsThatDontMatch()
+ {
+ var constraints = new Dictionary<string, IRouteConstraint>
+ {
+ {"a", new FailConstraint()},
+ {"b", new FailConstraint()}
+ };
+
+ var routeValueDictionary = new RouteValueDictionary(new { c = "value", d = "value" });
+
+ Assert.False(RouteConstraintMatcher.Match(
+ constraints: constraints,
+ routeValues: routeValueDictionary,
+ httpContext: new Mock<HttpContext>().Object,
+ route: new Mock<IRouter>().Object,
+ routeDirection: RouteDirection.IncomingRequest,
+ logger: NullLogger.Instance));
+ }
+
+ [Fact]
+ public void ReturnsFalseOnInvalidConstraintsThatMatch()
+ {
+ var constraints = new Dictionary<string, IRouteConstraint>
+ {
+ {"a", new FailConstraint()},
+ {"b", new FailConstraint()}
+ };
+
+ var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" });
+
+ Assert.False(RouteConstraintMatcher.Match(
+ constraints: constraints,
+ routeValues: routeValueDictionary,
+ httpContext: new Mock<HttpContext>().Object,
+ route: new Mock<IRouter>().Object,
+ routeDirection: RouteDirection.IncomingRequest,
+ logger: NullLogger.Instance));
+ }
+
+ [Fact]
+ public void ReturnsFalseOnValidAndInvalidConstraintsMixThatMatch()
+ {
+ var constraints = new Dictionary<string, IRouteConstraint>
+ {
+ {"a", new PassConstraint()},
+ {"b", new FailConstraint()}
+ };
+
+ var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" });
+
+ Assert.False(RouteConstraintMatcher.Match(
+ constraints: constraints,
+ routeValues: routeValueDictionary,
+ httpContext: new Mock<HttpContext>().Object,
+ route: new Mock<IRouter>().Object,
+ routeDirection: RouteDirection.IncomingRequest,
+ logger: NullLogger.Instance));
+ }
+
+ [Fact]
+ public void ReturnsTrueOnNullInput()
+ {
+ Assert.True(RouteConstraintMatcher.Match(
+ constraints: null,
+ routeValues: new RouteValueDictionary(),
+ httpContext: new Mock<HttpContext>().Object,
+ route: new Mock<IRouter>().Object,
+ routeDirection: RouteDirection.IncomingRequest,
+ logger: NullLogger.Instance));
+ }
+
+ private TestSink SetUpMatch(Dictionary<string, IRouteConstraint> constraints, bool loggerEnabled)
+ {
+ // Arrange
+ var sink = new TestSink();
+ var logger = new TestLogger(_name, sink, loggerEnabled);
+
+ var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" });
+
+ // Act
+ RouteConstraintMatcher.Match(
+ constraints: constraints,
+ routeValues: routeValueDictionary,
+ httpContext: new Mock<HttpContext>().Object,
+ route: new Mock<IRouter>().Object,
+ routeDirection: RouteDirection.IncomingRequest,
+ logger: logger);
+ return sink;
+ }
+
+ private class PassConstraint : IRouteConstraint
+ {
+ private readonly string _expectedKey;
+
+ public PassConstraint(string expectedKey = null)
+ {
+ _expectedKey = expectedKey;
+ }
+
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ if (_expectedKey != null)
+ {
+ Assert.Equal(_expectedKey, routeKey);
+ }
+
+ return true;
+ }
+ }
+
+ private class FailConstraint : IRouteConstraint
+ {
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ return false;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/AlphaRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/AlphaRouteConstraintTests.cs
new file mode 100644
index 0000000000..2ba9ef5c27
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/AlphaRouteConstraintTests.cs
@@ -0,0 +1,32 @@
+// 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.Routing.Constraints;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class AlphaRouteConstraintTests
+ {
+ [Theory]
+ [InlineData("alpha", true)]
+ [InlineData("a1pha", false)]
+ [InlineData("ALPHA", true)]
+ [InlineData("A1PHA", false)]
+ [InlineData("alPHA", true)]
+ [InlineData("A1pHA", false)]
+ [InlineData("AlpHA╥", false)]
+ [InlineData("", true)]
+ public void AlphaRouteConstraintTest(string parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new AlphaRouteConstraint();
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/BoolRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/BoolRouteConstraintTests.cs
new file mode 100644
index 0000000000..32a2aa595e
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/BoolRouteConstraintTests.cs
@@ -0,0 +1,40 @@
+// 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.Expressions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Constraints;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class BoolRouteConstraintTests
+ {
+ [Theory]
+ [InlineData("true", true)]
+ [InlineData("TruE", true)]
+ [InlineData("false", true)]
+ [InlineData("FalSe", true)]
+ [InlineData(" FalSe", true)]
+ [InlineData("True ", true)]
+ [InlineData(" False ", true)]
+ [InlineData(true, true)]
+ [InlineData(false, true)]
+ [InlineData(1, false)]
+ [InlineData("not-parseable-as-bool", false)]
+ public void BoolRouteConstraint(object parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new BoolRouteConstraint();
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/CompositeRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/CompositeRouteConstraintTests.cs
new file mode 100644
index 0000000000..b4df74c262
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/CompositeRouteConstraintTests.cs
@@ -0,0 +1,54 @@
+// 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.Expressions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Constraints;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class CompositeRouteConstraintTests
+ {
+ [Theory]
+ [InlineData(true, true, true)]
+ [InlineData(true, false, false)]
+ [InlineData(false, true, false)]
+ [InlineData(false, false, false)]
+ public void CompositeRouteConstraint_Match_CallsMatchOnInnerConstraints(
+ bool inner1Result,
+ bool inner2Result,
+ bool expected)
+ {
+ // Arrange
+ var inner1 = MockConstraintWithResult(inner1Result);
+ var inner2 = MockConstraintWithResult(inner2Result);
+
+ // Act
+ var constraint = new CompositeRouteConstraint(new[] { inner1.Object, inner2.Object });
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, null);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ static Expression<Func<IRouteConstraint, bool>> ConstraintMatchMethodExpression =
+ c => c.Match(
+ It.IsAny<HttpContext>(),
+ It.IsAny<IRouter>(),
+ It.IsAny<string>(),
+ It.IsAny<RouteValueDictionary>(),
+ It.IsAny<RouteDirection>());
+
+ private static Mock<IRouteConstraint> MockConstraintWithResult(bool result)
+ {
+ var mock = new Mock<IRouteConstraint>();
+ mock.Setup(ConstraintMatchMethodExpression)
+ .Returns(result)
+ .Verifiable();
+ return mock;
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/ConstraintsTestHelper.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/ConstraintsTestHelper.cs
new file mode 100644
index 0000000000..a86c7f4910
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/ConstraintsTestHelper.cs
@@ -0,0 +1,30 @@
+// 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 Microsoft.AspNetCore.Http;
+using Moq;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class ConstraintsTestHelper
+ {
+ public static bool TestConstraint(IRouteConstraint constraint, object value, Action<IRouter> routeConfig = null)
+ {
+ var context = new Mock<HttpContext>();
+
+ var route = new RouteCollection();
+
+ if (routeConfig != null)
+ {
+ routeConfig(route);
+ }
+
+ var parameterName = "fake";
+ var values = new RouteValueDictionary() { { parameterName, value } };
+ var routeDirection = RouteDirection.IncomingRequest;
+ return constraint.Match(context.Object, route, parameterName, values, routeDirection);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DateTimeRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DateTimeRouteConstraintTests.cs
new file mode 100644
index 0000000000..fb34db3994
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DateTimeRouteConstraintTests.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.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Routing.Constraints;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class DateTimeRouteConstraintTests
+ {
+ public static IEnumerable<object[]> GetDateTimeObject
+ {
+ get
+ {
+ yield return new object[]
+ {
+ DateTime.Now,
+ true
+ };
+ }
+ }
+
+ [Theory]
+ [InlineData("12/25/2009", true)]
+ [InlineData("25/12/2009 11:45:00 PM", false)]
+ [InlineData("25/12/2009", false)]
+ [InlineData("11:45:00 PM", true)]
+ [InlineData("11:45:00", true)]
+ [InlineData("11:45", true)]
+ [InlineData("11", false)]
+ [InlineData("", false)]
+ [InlineData("Apr 5 2009 11:45:00 PM", true)]
+ [InlineData("April 5 2009 11:45:00 PM", true)]
+ [InlineData("12/25/2009 11:45:00 PM", true)]
+ [InlineData("2009-05-12T11:45:00Z", true)]
+ [InlineData("not-parseable-as-date", false)]
+ [InlineData(false, false)]
+ [MemberData(nameof(GetDateTimeObject))]
+ public void DateTimeRouteConstraint(object parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new DateTimeRouteConstraint();
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DecimalRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DecimalRouteConstraintTests.cs
new file mode 100644
index 0000000000..3975317fac
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DecimalRouteConstraintTests.cs
@@ -0,0 +1,43 @@
+// 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 Microsoft.AspNetCore.Routing.Constraints;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class DecimalRouteConstraintTests
+ {
+ public static IEnumerable<object[]> GetDecimalObject
+ {
+ get
+ {
+ yield return new object[]
+ {
+ 2m,
+ true
+ };
+ }
+ }
+
+ [Theory]
+ [InlineData("3.14", true)]
+ [InlineData("9223372036854775808.9223372036854775808", true)]
+ [InlineData("1.79769313486232E+300", false)]
+ [InlineData("not-parseable-as-decimal", false)]
+ [InlineData(false, false)]
+ [MemberData(nameof(GetDecimalObject))]
+ public void DecimalRouteConstraint_ApplyConstraint(object parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new DecimalRouteConstraint();
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DoubleRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DoubleRouteConstraintTests.cs
new file mode 100644
index 0000000000..7f3a2c065d
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/DoubleRouteConstraintTests.cs
@@ -0,0 +1,30 @@
+// 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.Routing.Constraints;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class DoubleRouteConstraintTests
+ {
+ [Theory]
+ [InlineData("3.14", true)]
+ [InlineData(3.14f, true)]
+ [InlineData(3.14d, true)]
+ [InlineData("1.79769313486232E+300", true)]
+ [InlineData("not-parseable-as-double", false)]
+ [InlineData(false, false)]
+ public void DoubleRouteConstraint_ApplyConstraint(object parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new DoubleRouteConstraint();
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/FloatRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/FloatRouteConstraintTests.cs
new file mode 100644
index 0000000000..0fd1710c1c
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/FloatRouteConstraintTests.cs
@@ -0,0 +1,29 @@
+// 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.Routing.Constraints;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class FloatRouteConstraintTests
+ {
+ [Theory]
+ [InlineData("3.14", true)]
+ [InlineData(3.14, true)]
+ [InlineData("not-parseable-as-float", false)]
+ [InlineData(false, false)]
+ [InlineData("1.79769313486232E+300", false)]
+ public void FloatRouteConstraint_ApplyConstraint(object parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new FloatRouteConstraint();
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/GuidRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/GuidRouteConstraintTests.cs
new file mode 100644
index 0000000000..d53218a83b
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/GuidRouteConstraintTests.cs
@@ -0,0 +1,37 @@
+// 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 Microsoft.AspNetCore.Routing.Constraints;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class GuidRouteConstraintTests
+ {
+ [Theory]
+ [InlineData("12345678-1234-1234-1234-123456789012", false, true)]
+ [InlineData("12345678-1234-1234-1234-123456789012", true, true)]
+ [InlineData("12345678901234567890123456789012", false, true)]
+ [InlineData("not-parseable-as-guid", false, false)]
+ [InlineData(12, false, false)]
+
+ public void GuidRouteConstraint_ApplyConstraint(object parameterValue, bool parseBeforeTest, bool expected)
+ {
+ // Arrange
+ if (parseBeforeTest)
+ {
+ parameterValue = Guid.Parse(parameterValue.ToString());
+ }
+
+ var constraint = new GuidRouteConstraint();
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/HttpMethodRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/HttpMethodRouteConstraintTests.cs
new file mode 100644
index 0000000000..dec3739775
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/HttpMethodRouteConstraintTests.cs
@@ -0,0 +1,94 @@
+// 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;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ public class HttpMethodRouteConstraintTests
+ {
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("PosT")]
+ public void HttpMethodRouteConstraint_IncomingRequest_AcceptsAllowedMethods(string httpMethod)
+ {
+ // Arrange
+ var constraint = new HttpMethodRouteConstraint("GET", "post");
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Method = httpMethod;
+ var route = Mock.Of<IRouter>();
+
+ var values = new RouteValueDictionary(new { });
+
+ // Act
+ var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Theory]
+ [InlineData("OPTIONS")]
+ [InlineData("SomeRandomThing")]
+ public void HttpMethodRouteConstraint_IncomingRequest_RejectsOtherMethods(string httpMethod)
+ {
+ // Arrange
+ var constraint = new HttpMethodRouteConstraint("GET", "post");
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Method = httpMethod;
+ var route = Mock.Of<IRouter>();
+
+ var values = new RouteValueDictionary(new { });
+
+ // Act
+ var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("PosT")]
+ public void HttpMethodRouteConstraint_UrlGeneration_AcceptsAllowedMethods(string httpMethod)
+ {
+ // Arrange
+ var constraint = new HttpMethodRouteConstraint("GET", "post");
+
+ var httpContext = new DefaultHttpContext();
+ var route = Mock.Of<IRouter>();
+
+ var values = new RouteValueDictionary(new { httpMethod = httpMethod });
+
+ // Act
+ var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.UrlGeneration);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Theory]
+ [InlineData("OPTIONS")]
+ [InlineData("SomeRandomThing")]
+ public void HttpMethodRouteConstraint_UrlGeneration_RejectsOtherMethods(string httpMethod)
+ {
+ // Arrange
+ var constraint = new HttpMethodRouteConstraint("GET", "post");
+
+ var httpContext = new DefaultHttpContext();
+ var route = Mock.Of<IRouter>();
+
+ var values = new RouteValueDictionary(new { httpMethod = httpMethod });
+
+ // Act
+ var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.UrlGeneration);
+
+ // Assert
+ Assert.False(result);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/IntRouteConstraintsTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/IntRouteConstraintsTests.cs
new file mode 100644
index 0000000000..ff70fe21b7
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/IntRouteConstraintsTests.cs
@@ -0,0 +1,29 @@
+// 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.Routing.Constraints;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class IntRouteConstraintsTests
+ {
+ [Theory]
+ [InlineData(42, true)]
+ [InlineData("42", true)]
+ [InlineData(3.14, false)]
+ [InlineData("43.567", false)]
+ [InlineData("42a", false)]
+ public void IntRouteConstraint_Match_AppliesConstraint(object parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new IntRouteConstraint();
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LengthRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LengthRouteConstraintTests.cs
new file mode 100644
index 0000000000..707056a01c
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LengthRouteConstraintTests.cs
@@ -0,0 +1,103 @@
+// 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.Routing.Constraints;
+using Microsoft.AspNetCore.Testing;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class LengthRouteConstraintTests
+ {
+ [Theory]
+ [InlineData(3, "123", true)]
+ [InlineData(3, "1234", false)]
+ [InlineData(0, "", true)]
+ public void LengthRouteConstraint_ExactLength_Tests(int length, string parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new LengthRouteConstraint(length);
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(3, 5, "12", false)]
+ [InlineData(3, 5, "123", true)]
+ [InlineData(3, 5, "1234", true)]
+ [InlineData(3, 5, "12345", true)]
+ [InlineData(3, 5, "123456", false)]
+ public void LengthRouteConstraint_Range_Tests(int min, int max, string parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new LengthRouteConstraint(min, max);
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void LengthRouteConstraint_SettingLengthLessThanZero_Throws()
+ {
+ // Arrange
+ var expectedMessage = "Value must be greater than or equal to 0.";
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgumentOutOfRange(
+ () => new LengthRouteConstraint(-1),
+ "length",
+ expectedMessage,
+ -1);
+ }
+
+ [Fact]
+ public void LengthRouteConstraint_SettingMinLengthLessThanZero_Throws()
+ {
+ // Arrange
+ var expectedMessage = "Value must be greater than or equal to 0.";
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgumentOutOfRange(
+ () => new LengthRouteConstraint(-1, 3),
+ "minLength",
+ expectedMessage,
+ -1);
+ }
+
+ [Fact]
+ public void LengthRouteConstraint_SettingMaxLengthLessThanZero_Throws()
+ {
+ // Arrange
+ var expectedMessage = "Value must be greater than or equal to 0.";
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgumentOutOfRange(
+ () => new LengthRouteConstraint(0, -1),
+ "maxLength",
+ expectedMessage,
+ -1);
+ }
+
+ [Fact]
+ public void LengthRouteConstraint_MinGreaterThanMax_Throws()
+ {
+ // Arrange
+ var expectedMessage = "The value for argument 'minLength' should be less than or equal to the " +
+ "value for the argument 'maxLength'.";
+
+ // Arrange Act & Assert
+ ExceptionAssert.ThrowsArgumentOutOfRange(
+ () => new LengthRouteConstraint(3, 2),
+ "minLength",
+ expectedMessage,
+ 3);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LongRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LongRouteConstraintTests.cs
new file mode 100644
index 0000000000..99977ee753
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/LongRouteConstraintTests.cs
@@ -0,0 +1,31 @@
+// 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.Routing.Constraints;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class LongRouteConstraintTests
+ {
+ [Theory]
+ [InlineData(42, true)]
+ [InlineData(42L, true)]
+ [InlineData("42", true)]
+ [InlineData("9223372036854775807", true)]
+ [InlineData(3.14, false)]
+ [InlineData("43.567", false)]
+ [InlineData("42a", false)]
+ public void LongRouteConstraintTest(object parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new LongRouteConstraint();
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxLengthRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxLengthRouteConstraintTests.cs
new file mode 100644
index 0000000000..5d603705e2
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxLengthRouteConstraintTests.cs
@@ -0,0 +1,43 @@
+// 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.Routing.Constraints;
+using Microsoft.AspNetCore.Testing;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class MaxLengthRouteConstraintTests
+ {
+ [Theory]
+ [InlineData(3, "", true)]
+ [InlineData(3, "12", true)]
+ [InlineData(3, "123", true)]
+ [InlineData(3, "1234", false)]
+ public void MaxLengthRouteConstraint_ApplyConstraint(int min, string parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new MaxLengthRouteConstraint(min);
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void MaxLengthRouteConstraint_SettingMaxLengthLessThanZero_Throws()
+ {
+ // Arrange
+ var expectedMessage = "Value must be greater than or equal to 0.";
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgumentOutOfRange(
+ () => new MaxLengthRouteConstraint(-1),
+ "maxLength",
+ expectedMessage,
+ -1);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxRouteConstraintTests.cs
new file mode 100644
index 0000000000..dc962d0948
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MaxRouteConstraintTests.cs
@@ -0,0 +1,27 @@
+// 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.Routing.Constraints;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class MaxRouteConstraintTests
+ {
+ [Theory]
+ [InlineData(3, 2, true)]
+ [InlineData(3, 3, true)]
+ [InlineData(3, 4, false)]
+ public void MaxRouteConstraint_ApplyConstraint(long max, int parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new MaxRouteConstraint(max);
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinLengthRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinLengthRouteConstraintTests.cs
new file mode 100644
index 0000000000..c8e7bab2de
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinLengthRouteConstraintTests.cs
@@ -0,0 +1,43 @@
+// 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.Routing.Constraints;
+using Microsoft.AspNetCore.Testing;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class MinLengthRouteConstraintTests
+ {
+ [Theory]
+ [InlineData(3, "1234", true)]
+ [InlineData(3, "123", true)]
+ [InlineData(3, "12", false)]
+ [InlineData(3, "", false)]
+ public void MinLengthRouteConstraint_ApplyConstraint(int min, string parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new MinLengthRouteConstraint(min);
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void MinLengthRouteConstraint_SettingMinLengthLessThanZero_Throws()
+ {
+ // Arrange
+ var expectedMessage = "Value must be greater than or equal to 0.";
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgumentOutOfRange(
+ () => new MinLengthRouteConstraint(-1),
+ "minLength",
+ expectedMessage,
+ -1);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinRouteConstraintTests.cs
new file mode 100644
index 0000000000..b21b2ded74
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/MinRouteConstraintTests.cs
@@ -0,0 +1,27 @@
+// 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.Routing.Constraints;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class MinRouteConstraintTests
+ {
+ [Theory]
+ [InlineData(3, 4, true)]
+ [InlineData(3, 3, true)]
+ [InlineData(3, 2, false)]
+ public void MinRouteConstraint_ApplyConstraint(long min, int parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new MinRouteConstraint(min);
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RangeRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RangeRouteConstraintTests.cs
new file mode 100644
index 0000000000..99d9c64ba1
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RangeRouteConstraintTests.cs
@@ -0,0 +1,48 @@
+// 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.Routing.Constraints;
+using Microsoft.AspNetCore.Testing;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class RangeRouteConstraintTests
+ {
+ [Theory]
+ [InlineData(long.MinValue, long.MaxValue, 2, true)]
+ [InlineData(3, 5, 3, true)]
+ [InlineData(3, 5, 4, true)]
+ [InlineData(3, 5, 5, true)]
+ [InlineData(3, 5, 6, false)]
+ [InlineData(3, 5, 2, false)]
+ [InlineData(3, 3, 2, false)]
+ [InlineData(3, 3, 3, true)]
+ public void RangeRouteConstraintTest_ValidValue_ApplyConstraint(long min, long max, int parameterValue, bool expected)
+ {
+ // Arrange
+ var constraint = new RangeRouteConstraint(min, max);
+
+ // Act
+ var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void RangeRouteConstraint_MinGreaterThanMax_Throws()
+ {
+ // Arrange
+ var expectedMessage = "The value for argument 'min' should be less than or equal to the value for the " +
+ "argument 'max'.";
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgumentOutOfRange(
+ () => new RangeRouteConstraint(3, 2),
+ "min",
+ expectedMessage,
+ 3L);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexInlineRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexInlineRouteConstraintTests.cs
new file mode 100644
index 0000000000..a87f6519c7
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexInlineRouteConstraintTests.cs
@@ -0,0 +1,92 @@
+// 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;
+using Microsoft.AspNetCore.Routing.Constraints;
+using Microsoft.AspNetCore.Testing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class RegexInlineRouteConstraintTests
+ {
+ [Theory]
+ [InlineData("abc", "abc", true)] // simple match
+ [InlineData("Abc", "abc", true)] // case insensitive match
+ [InlineData("Abc ", "abc", true)] // Extra space on input match (because we don't add ^({0})$
+ [InlineData("Abcd", "abc", true)] // Extra char
+ [InlineData("^Abcd", "abc", true)] // Extra special char
+ [InlineData("Abc", " abc", false)] // Missing char
+ public void RegexInlineConstraintBuildRegexVerbatimFromInput(
+ string routeValue,
+ string constraintValue,
+ bool shouldMatch)
+ {
+ // Arrange
+ var constraint = new RegexInlineRouteConstraint(constraintValue);
+ var values = new RouteValueDictionary(new { controller = routeValue });
+
+ // Act
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: values,
+ routeDirection: RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.Equal(shouldMatch, match);
+ }
+
+ [Fact]
+ public void RegexInlineConstraint_FailsIfKeyIsNotFoundInRouteValues()
+ {
+ // Arrange
+ var constraint = new RegexInlineRouteConstraint("^abc$");
+ var values = new RouteValueDictionary(new { action = "abc" });
+
+ // Act
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: values,
+ routeDirection: RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Theory]
+ [InlineData("tr-TR")]
+ [InlineData("en-US")]
+ public void RegexInlineConstraint_IsCultureInsensitive(string culture)
+ {
+ if (TestPlatformHelper.IsMono)
+ {
+ // The Regex in Mono returns true when matching the Turkish I for the a-z range which causes the test
+ // to fail. Tracked via #100.
+ return;
+ }
+
+ // Arrange
+ var constraint = new RegexInlineRouteConstraint("^([a-z]+)$");
+ var values = new RouteValueDictionary(new { controller = "\u0130" }); // Turkish upper-case dotted I
+
+ using (new CultureReplacer(culture))
+ {
+ // Act
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: values,
+ routeDirection: RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.False(match);
+ }
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexRouteConstraintTests.cs
new file mode 100644
index 0000000000..7affd5c034
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RegexRouteConstraintTests.cs
@@ -0,0 +1,134 @@
+// 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.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Constraints;
+using Microsoft.AspNetCore.Testing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class RegexRouteConstraintTests
+ {
+ [Theory]
+ [InlineData("abc", "abc", true)] // simple match
+ [InlineData("Abc", "abc", true)] // case insensitive match
+ [InlineData("Abc ", "abc", true)] // Extra space on input match (because we don't add ^({0})$
+ [InlineData("Abcd", "abc", true)] // Extra char
+ [InlineData("^Abcd", "abc", true)] // Extra special char
+ [InlineData("Abc", " abc", false)] // Missing char
+ [InlineData("123-456-2334", @"^\d{3}-\d{3}-\d{4}$", true)] // ssn
+ [InlineData(@"12/4/2013", @"^\d{1,2}\/\d{1,2}\/\d{4}$", true)] // date
+ [InlineData(@"abc@def.com", @"^\w+[\w\.]*\@\w+((-\w+)|(\w*))\.[a-z]{2,3}$", true)] // email
+ public void RegexConstraintBuildRegexVerbatimFromInput(
+ string routeValue,
+ string constraintValue,
+ bool shouldMatch)
+ {
+ // Arrange
+ var constraint = new RegexRouteConstraint(constraintValue);
+ var values = new RouteValueDictionary(new { controller = routeValue });
+
+ // Act
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: values,
+ routeDirection: RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.Equal(shouldMatch, match);
+ }
+
+ [Fact]
+ public void RegexConstraint_TakesRegexAsInput_SimpleMatch()
+ {
+ // Arrange
+ var constraint = new RegexRouteConstraint(new Regex("^abc$"));
+ var values = new RouteValueDictionary(new { controller = "abc" });
+
+ // Act
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: values,
+ routeDirection: RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.True(match);
+ }
+
+ [Fact]
+ public void RegexConstraintConstructedWithRegex_SimpleFailedMatch()
+ {
+ // Arrange
+ var constraint = new RegexRouteConstraint(new Regex("^abc$"));
+ var values = new RouteValueDictionary(new { controller = "Abc" });
+
+ // Act
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: values,
+ routeDirection: RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Fact]
+ public void RegexConstraintFailsIfKeyIsNotFoundInRouteValues()
+ {
+ // Arrange
+ var constraint = new RegexRouteConstraint(new Regex("^abc$"));
+ var values = new RouteValueDictionary(new { action = "abc" });
+
+ // Act
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: values,
+ routeDirection: RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Theory]
+ [InlineData("tr-TR")]
+ [InlineData("en-US")]
+ public void RegexConstraintIsCultureInsensitiveWhenConstructedWithString(string culture)
+ {
+ if (TestPlatformHelper.IsMono)
+ {
+ // The Regex in Mono returns true when matching the Turkish I for the a-z range which causes the test
+ // to fail. Tracked via #100.
+ return;
+ }
+
+ // Arrange
+ var constraint = new RegexRouteConstraint("^([a-z]+)$");
+ var values = new RouteValueDictionary(new { controller = "\u0130" }); // Turkish upper-case dotted I
+
+ using (new CultureReplacer(culture))
+ {
+ // Act
+ var match = constraint.Match(
+ httpContext: new Mock<HttpContext>().Object,
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: values,
+ routeDirection: RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.False(match);
+ }
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RequiredRouteConstraintTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RequiredRouteConstraintTests.cs
new file mode 100644
index 0000000000..31ec2b2bf0
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/RequiredRouteConstraintTests.cs
@@ -0,0 +1,93 @@
+// 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;
+using Microsoft.AspNetCore.Routing.Constraints;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class RequiredRouteConstraintTests
+ {
+ [Theory]
+ [InlineData(RouteDirection.IncomingRequest)]
+ [InlineData(RouteDirection.UrlGeneration)]
+ public void RequiredRouteConstraint_NoValue(RouteDirection direction)
+ {
+ // Arrange
+ var constraint = new RequiredRouteConstraint();
+
+ // Act
+ var result = constraint.Match(
+ new DefaultHttpContext(),
+ Mock.Of<IRouter>(),
+ "area",
+ new RouteValueDictionary(new { controller = "Home", action = "Index" }),
+ direction);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Theory]
+ [InlineData(RouteDirection.IncomingRequest)]
+ [InlineData(RouteDirection.UrlGeneration)]
+ public void RequiredRouteConstraint_Null(RouteDirection direction)
+ {
+ // Arrange
+ var constraint = new RequiredRouteConstraint();
+
+ // Act
+ var result = constraint.Match(
+ new DefaultHttpContext(),
+ Mock.Of<IRouter>(),
+ "area",
+ new RouteValueDictionary(new { controller = "Home", action = "Index", area = (string)null }),
+ direction);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Theory]
+ [InlineData(RouteDirection.IncomingRequest)]
+ [InlineData(RouteDirection.UrlGeneration)]
+ public void RequiredRouteConstraint_EmptyString(RouteDirection direction)
+ {
+ // Arrange
+ var constraint = new RequiredRouteConstraint();
+
+ // Act
+ var result = constraint.Match(
+ new DefaultHttpContext(),
+ Mock.Of<IRouter>(),
+ "area",
+ new RouteValueDictionary(new { controller = "Home", action = "Index", area = string.Empty}),
+ direction);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Theory]
+ [InlineData(RouteDirection.IncomingRequest)]
+ [InlineData(RouteDirection.UrlGeneration)]
+ public void RequiredRouteConstraint_WithValue(RouteDirection direction)
+ {
+ // Arrange
+ var constraint = new RequiredRouteConstraint();
+
+ // Act
+ var result = constraint.Match(
+ new DefaultHttpContext(),
+ Mock.Of<IRouter>(),
+ "area",
+ new RouteValueDictionary(new { controller = "Home", action = "Index", area = "Store" }),
+ direction);
+
+ // Assert
+ Assert.True(result);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/StringRouteConstraintTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/StringRouteConstraintTest.cs
new file mode 100644
index 0000000000..9e9f178286
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Constraints/StringRouteConstraintTest.cs
@@ -0,0 +1,157 @@
+// 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;
+using Microsoft.AspNetCore.Routing.Constraints;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Constraints
+{
+ public class StringRouteConstraintTest
+ {
+ [Fact]
+ public void StringRouteConstraintSimpleTrueWithRouteDirectionIncomingRequestTest()
+ {
+ // Arrange
+ var constraint = new StringRouteConstraint("home");
+
+ // Act
+ var values = new RouteValueDictionary(new { controller = "home" });
+
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: values,
+ routeDirection: RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.True(match);
+ }
+
+ [Fact]
+ public void StringRouteConstraintSimpleTrueWithRouteDirectionUrlGenerationTest()
+ {
+ // Arrange
+ var constraint = new StringRouteConstraint("home");
+
+ // Act
+ var values = new RouteValueDictionary(new { controller = "home" });
+
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: values,
+ routeDirection: RouteDirection.UrlGeneration);
+
+ // Assert
+ Assert.True(match);
+ }
+
+ [Fact]
+ public void StringRouteConstraintSimpleFalseWithRouteDirectionIncomingRequestTest()
+ {
+ // Arrange
+ var constraint = new StringRouteConstraint("admin");
+
+ // Act
+ var values = new RouteValueDictionary(new { controller = "home" });
+
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: values,
+ routeDirection: RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Fact]
+ public void StringRouteConstraintSimpleFalseWithRouteDirectionUrlGenerationTest()
+ {
+ // Arrange
+ var constraint = new StringRouteConstraint("admin");
+
+ // Act
+ var values = new RouteValueDictionary(new { controller = "home" });
+
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: values,
+ routeDirection: RouteDirection.UrlGeneration);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Fact]
+ public void StringRouteConstraintKeyNotFoundWithRouteDirectionIncomingRequestTest()
+ {
+ // Arrange
+ var constraint = new StringRouteConstraint("admin");
+
+ // Act
+ var values = new RouteValueDictionary(new { controller = "admin" });
+
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "action",
+ values: values,
+ routeDirection: RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Fact]
+ public void StringRouteConstraintKeyNotFoundWithRouteDirectionUrlGenerationTest()
+ {
+ // Arrange
+ var constraint = new StringRouteConstraint("admin");
+
+ // Act
+ var values = new RouteValueDictionary(new { controller = "admin" });
+
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "action",
+ values: values,
+ routeDirection: RouteDirection.UrlGeneration);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Theory]
+ [InlineData("User", "uSer", true)]
+ [InlineData("User.Admin", "User.Admin", true)]
+ [InlineData(@"User\Admin", "User\\Admin", true)]
+ [InlineData(null, "user", false)]
+ public void StringRouteConstraintEscapingCaseSensitiveAndRouteNullTest(string routeValue, string constraintValue, bool expected)
+ {
+ // Arrange
+ var constraint = new StringRouteConstraint(constraintValue);
+
+ // Act
+ var values = new RouteValueDictionary(new { controller = routeValue });
+
+ var match = constraint.Match(
+ new DefaultHttpContext(),
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: values,
+ routeDirection: RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.Equal(expected, match);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultInlineConstraintResolverTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultInlineConstraintResolverTest.cs
new file mode 100644
index 0000000000..bba78fb6b2
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/DefaultInlineConstraintResolverTest.cs
@@ -0,0 +1,372 @@
+// 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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Constraints;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class DefaultInlineConstraintResolverTest
+ {
+ private IInlineConstraintResolver _constraintResolver;
+
+ public DefaultInlineConstraintResolverTest()
+ {
+ var routeOptions = new RouteOptions();
+ _constraintResolver = GetInlineConstraintResolver(routeOptions);
+ }
+
+ [Fact]
+ public void ResolveConstraint_RequiredConstraint_ResolvesCorrectly()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("required");
+
+ // Assert
+ Assert.IsType<RequiredRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_IntConstraint_ResolvesCorrectly()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("int");
+
+ // Assert
+ Assert.IsType<IntRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_IntConstraintWithArgument_Throws()
+ {
+ // Arrange, Act & Assert
+ var ex = Assert.Throws<RouteCreationException>(
+ () => _constraintResolver.ResolveConstraint("int(5)"));
+
+ Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'" +
+ " with the following number of parameters: 1.",
+ ex.Message);
+ }
+
+ [Fact]
+ public void ResolveConstraint_AlphaConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("alpha");
+
+ // Assert
+ Assert.IsType<AlphaRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_RegexInlineConstraint_WithAComma_PassesAsASingleArgument()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("regex(ab,1)");
+
+ // Assert
+ Assert.IsType<RegexInlineRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_RegexInlineConstraint_WithCurlyBraces_Balanced()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint(
+ @"regex(\\b(?<month>\\d{1,2})/(?<day>\\d{1,2})/(?<year>\\d{2,4})\\b)");
+
+ // Assert
+ Assert.IsType<RegexInlineRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_BoolConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("bool");
+
+ // Assert
+ Assert.IsType<BoolRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_CompositeConstraintIsNotRegistered()
+ {
+ // Arrange, Act & Assert
+ Assert.Null(_constraintResolver.ResolveConstraint("composite"));
+ }
+
+ [Fact]
+ public void ResolveConstraint_DateTimeConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("datetime");
+
+ // Assert
+ Assert.IsType<DateTimeRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_DecimalConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("decimal");
+
+ // Assert
+ Assert.IsType<DecimalRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_DoubleConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("double");
+
+ // Assert
+ Assert.IsType<DoubleRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_FloatConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("float");
+
+ // Assert
+ Assert.IsType<FloatRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_GuidConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("guid");
+
+ // Assert
+ Assert.IsType<GuidRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_IntConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("int");
+
+ // Assert
+ Assert.IsType<IntRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_LengthConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("length(5)");
+
+ // Assert
+ Assert.IsType<LengthRouteConstraint>(constraint);
+ Assert.Equal(5, ((LengthRouteConstraint)constraint).MinLength);
+ Assert.Equal(5, ((LengthRouteConstraint)constraint).MaxLength);
+ }
+
+ [Fact]
+ public void ResolveConstraint_LengthRangeConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("length(5, 10)");
+
+ // Assert
+ var lengthConstraint = Assert.IsType<LengthRouteConstraint>(constraint);
+ Assert.Equal(5, lengthConstraint.MinLength);
+ Assert.Equal(10, lengthConstraint.MaxLength);
+ }
+
+ [Fact]
+ public void ResolveConstraint_LongRangeConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("long");
+
+ // Assert
+ Assert.IsType<LongRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_MaxConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("max(10)");
+
+ // Assert
+ Assert.IsType<MaxRouteConstraint>(constraint);
+ Assert.Equal(10, ((MaxRouteConstraint)constraint).Max);
+ }
+
+ [Fact]
+ public void ResolveConstraint_MaxLengthConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("maxlength(10)");
+
+ // Assert
+ Assert.IsType<MaxLengthRouteConstraint>(constraint);
+ Assert.Equal(10, ((MaxLengthRouteConstraint)constraint).MaxLength);
+ }
+
+ [Fact]
+ public void ResolveConstraint_MinConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("min(3)");
+
+ // Assert
+ Assert.IsType<MinRouteConstraint>(constraint);
+ Assert.Equal(3, ((MinRouteConstraint)constraint).Min);
+ }
+
+ [Fact]
+ public void ResolveConstraint_MinLengthConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("minlength(3)");
+
+ // Assert
+ Assert.IsType<MinLengthRouteConstraint>(constraint);
+ Assert.Equal(3, ((MinLengthRouteConstraint)constraint).MinLength);
+ }
+
+ [Fact]
+ public void ResolveConstraint_RangeConstraint()
+ {
+ // Arrange & Act
+ var constraint = _constraintResolver.ResolveConstraint("range(5, 10)");
+
+ // Assert
+ Assert.IsType<RangeRouteConstraint>(constraint);
+ var rangeConstraint = (RangeRouteConstraint)constraint;
+ Assert.Equal(5, rangeConstraint.Min);
+ Assert.Equal(10, rangeConstraint.Max);
+ }
+
+ [Fact]
+ public void ResolveConstraint_SupportsCustomConstraints()
+ {
+ // Arrange
+ var routeOptions = new RouteOptions();
+ routeOptions.ConstraintMap.Add("custom", typeof(CustomRouteConstraint));
+ var resolver = GetInlineConstraintResolver(routeOptions);
+
+ // Act
+ var constraint = resolver.ResolveConstraint("custom(argument)");
+
+ // Assert
+ Assert.IsType<CustomRouteConstraint>(constraint);
+ }
+
+ [Fact]
+ public void ResolveConstraint_CustomConstraintThatDoesNotImplementIRouteConstraint_Throws()
+ {
+ // Arrange
+ var routeOptions = new RouteOptions();
+ routeOptions.ConstraintMap.Add("custom", typeof(string));
+ var resolver = GetInlineConstraintResolver(routeOptions);
+
+ // Act & Assert
+ var ex = Assert.Throws<RouteCreationException>(() => resolver.ResolveConstraint("custom"));
+ Assert.Equal("The constraint type 'System.String' which is mapped to constraint key 'custom'" +
+ " must implement the 'IRouteConstraint' interface.",
+ ex.Message);
+ }
+
+ [Fact]
+ public void ResolveConstraint_AmbiguousConstructors_Throws()
+ {
+ // Arrange
+ var routeOptions = new RouteOptions();
+ routeOptions.ConstraintMap.Add("custom", typeof(MultiConstructorRouteConstraint));
+ var resolver = GetInlineConstraintResolver(routeOptions);
+
+ // Act & Assert
+ var ex = Assert.Throws<RouteCreationException>(() => resolver.ResolveConstraint("custom(5,6)"));
+ Assert.Equal("The constructor to use for activating the constraint type 'MultiConstructorRouteConstraint' is ambiguous." +
+ " Multiple constructors were found with the following number of parameters: 2.",
+ ex.Message);
+ }
+
+ // These are cases which parsing does not catch and we'll end up here
+ [Theory]
+ [InlineData("regex(abc")]
+ [InlineData("int/")]
+ [InlineData("in{t")]
+ public void ResolveConstraint_Invalid_Throws(string constraint)
+ {
+ // Arrange
+ var routeOptions = new RouteOptions();
+ var resolver = GetInlineConstraintResolver(routeOptions);
+
+ // Act & Assert
+ Assert.Null(resolver.ResolveConstraint(constraint));
+ }
+
+ [Fact]
+ public void ResolveConstraint_NoMatchingConstructor_Throws()
+ {
+ // Arrange
+ // Act & Assert
+ var ex = Assert.Throws<RouteCreationException>(() => _constraintResolver.ResolveConstraint("int(5,6)"));
+ Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'" +
+ " with the following number of parameters: 2.",
+ ex.Message);
+ }
+
+ private IInlineConstraintResolver GetInlineConstraintResolver(RouteOptions routeOptions)
+ {
+ var optionsAccessor = new Mock<IOptions<RouteOptions>>();
+ optionsAccessor.SetupGet(o => o.Value).Returns(routeOptions);
+ return new DefaultInlineConstraintResolver(optionsAccessor.Object);
+ }
+
+ private class MultiConstructorRouteConstraint : IRouteConstraint
+ {
+ public MultiConstructorRouteConstraint(string pattern, int intArg)
+ {
+ }
+
+ public MultiConstructorRouteConstraint(int intArg, string pattern)
+ {
+ }
+
+ public bool Match(HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ return true;
+ }
+ }
+
+ private class CustomRouteConstraint : IRouteConstraint
+ {
+ public CustomRouteConstraint(string pattern)
+ {
+ Pattern = pattern;
+ }
+
+ public string Pattern { get; private set; }
+ public bool Match(HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/InlineRouteParameterParserTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/InlineRouteParameterParserTests.cs
new file mode 100644
index 0000000000..0e570c49cc
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/InlineRouteParameterParserTests.cs
@@ -0,0 +1,992 @@
+// 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.Http;
+using Microsoft.AspNetCore.Routing.Template;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class InlineRouteParameterParserTests
+ {
+ [Theory]
+ [InlineData("=")]
+ [InlineData(":")]
+ public void ParseRouteParameter_WithoutADefaultValue(string parameterName)
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(parameterName);
+
+ // Assert
+ Assert.Equal(parameterName, templatePart.Name);
+ Assert.Null(templatePart.DefaultValue);
+ Assert.Empty(templatePart.InlineConstraints);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_WithEmptyDefaultValue()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter("param=");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("", templatePart.DefaultValue);
+ Assert.Empty(templatePart.InlineConstraints);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_WithoutAConstraintName()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter("param:");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Null(templatePart.DefaultValue);
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Empty(constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_WithoutAConstraintNameOrParameterName()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter("param:=");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("", templatePart.DefaultValue);
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Empty(constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_WithADefaultValueContainingConstraintSeparator()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter("param=:");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal(":", templatePart.DefaultValue);
+ Assert.Empty(templatePart.InlineConstraints);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintAndDefault_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter("param:int=111111");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("111111", templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal("int", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithArgumentsAndDefault_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\d+)=111111");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("111111", templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\d+)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintAndOptional_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:int?");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.True(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal("int", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:int=12?");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("12", templatePart.DefaultValue);
+ Assert.True(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal("int", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValueWithQuestionMark_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:int=12??");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("12?", templatePart.DefaultValue);
+ Assert.True(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal("int", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\d+)?");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.True(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\d+)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\d+)=abc?");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.True(templatePart.IsOptional);
+
+ Assert.Equal("abc", templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\d+)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ChainedConstraints_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(d+):test(w+)");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ Assert.Collection(templatePart.InlineConstraints,
+ constraint => Assert.Equal(@"test(d+)", constraint.Constraint),
+ constraint => Assert.Equal(@"test(w+)", constraint.Constraint));
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ChainedConstraints_DoubleDelimiters_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param::test(d+)::test(w+)");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ Assert.Collection(templatePart.InlineConstraints,
+ constraint => Assert.Empty(constraint.Constraint),
+ constraint => Assert.Equal(@"test(d+)", constraint.Constraint),
+ constraint => Assert.Empty(constraint.Constraint),
+ constraint => Assert.Equal(@"test(w+)", constraint.Constraint));
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ChainedConstraints_ColonInPattern_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\d+):test(\w:+)");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ Assert.Collection(templatePart.InlineConstraints,
+ constraint => Assert.Equal(@"test(\d+)", constraint.Constraint),
+ constraint => Assert.Equal(@"test(\w:+)", constraint.Constraint));
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\d+):test(\w+)=qwer");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ Assert.Equal("qwer", templatePart.DefaultValue);
+
+ Assert.Collection(templatePart.InlineConstraints,
+ constraint => Assert.Equal(@"test(\d+)", constraint.Constraint),
+ constraint => Assert.Equal(@"test(\w+)", constraint.Constraint));
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_DoubleDelimiters_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\d+)::test(\w+)==qwer");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ Assert.Equal("=qwer", templatePart.DefaultValue);
+
+ Assert.Collection(templatePart.InlineConstraints,
+ constraint => Assert.Equal(@"test(\d+)", constraint.Constraint),
+ constraint => Assert.Empty(constraint.Constraint),
+ constraint => Assert.Equal(@"test(\w+)", constraint.Constraint));
+ }
+
+ [Theory]
+ [InlineData("=")]
+ [InlineData("+=")]
+ [InlineData(">= || <= || ==")]
+ public void ParseRouteParameter_WithDefaultValue_ContainingDelimiter(string defaultValue)
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter($"comparison-operator:length(6)={defaultValue}");
+
+ // Assert
+ Assert.Equal("comparison-operator", templatePart.Name);
+ Assert.Equal(defaultValue, templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal("length(6)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var template = ParseRouteTemplate(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}");
+
+ // Assert
+ var parameters = template.Parameters.ToArray();
+
+ var param1 = parameters[0];
+ Assert.Equal("p1", param1.Name);
+ Assert.Equal("hello", param1.DefaultValue);
+ Assert.False(param1.IsOptional);
+
+ Assert.Collection(param1.InlineConstraints,
+ constraint => Assert.Equal("int", constraint.Constraint),
+ constraint => Assert.Equal("test(3)", constraint.Constraint)
+ );
+
+ var param2 = parameters[1];
+ Assert.Equal("p2", param2.Name);
+ Assert.Equal("abc", param2.DefaultValue);
+ Assert.False(param2.IsOptional);
+
+ var param3 = parameters[2];
+ Assert.Equal("p3", param3.Name);
+ Assert.True(param3.IsOptional);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_NoTokens_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter("world");
+
+ // Assert
+ Assert.Equal("world", templatePart.Name);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ParamDefault_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter("param=world");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("world", templatePart.DefaultValue);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_ClosingBraceIsParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\})");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\})", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\})=wer");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ Assert.Equal("wer", templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\})", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithClosingParenInPattern_ClosingParenIsParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\))");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\))", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithClosingParenInPattern_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\))=fsd");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ Assert.Equal("fsd", templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\))", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithColonInPattern_ColonIsParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(:)");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(:)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithColonInPattern_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(:)=mnf");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ Assert.Equal("mnf", templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(:)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithColonsInPattern_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(a:b:c)");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(a:b:c)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithColonInParamName_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@":param:test=12");
+
+ // Assert
+ Assert.Equal(":param", templatePart.Name);
+
+ Assert.Equal("12", templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal("test", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithTwoColonInParamName_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@":param::test=12");
+
+ // Assert
+ Assert.Equal(":param", templatePart.Name);
+
+ Assert.Equal("12", templatePart.DefaultValue);
+
+ Assert.Collection(templatePart.InlineConstraints,
+ constraint => Assert.Empty(constraint.Constraint),
+ constraint => Assert.Equal("test", constraint.Constraint));
+ }
+
+ [Fact]
+ public void ParseRouteParameter_EmptyConstraint_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@":param:test:");
+
+ // Assert
+ Assert.Equal(":param", templatePart.Name);
+
+ Assert.Collection(templatePart.InlineConstraints,
+ constraint => Assert.Equal("test", constraint.Constraint),
+ constraint => Assert.Empty(constraint.Constraint));
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithCommaInPattern_PatternIsParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\w,\w)");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\w,\w)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithCommaInName_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"par,am:test(\w)");
+
+ // Assert
+ Assert.Equal("par,am", templatePart.Name);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\w)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithCommaInPattern_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\w,\w)=jsd");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ Assert.Equal("jsd", templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\w,\w)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithEqualsFollowedByQuestionMark_PatternIsParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:int=?");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("", templatePart.DefaultValue);
+
+ Assert.True(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal("int", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_PatternIsParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(=)");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Null(templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal("test(=)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_EqualsSignInDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param=test=bar");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("test=bar", templatePart.DefaultValue);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(a==b)");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Null(templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal("test(a==b)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(a==b)=dvds");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("dvds", templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal("test(a==b)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_EqualEqualSignInName_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"par==am:test=dvds");
+
+ // Assert
+ Assert.Equal("par", templatePart.Name);
+ Assert.Equal("=am:test=dvds", templatePart.DefaultValue);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_EqualEqualSignInDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test==dvds");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("=dvds", templatePart.DefaultValue);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_DefaultValueWithColonAndParens_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"par=am:test(asd)");
+
+ // Assert
+ Assert.Equal("par", templatePart.Name);
+ Assert.Equal("am:test(asd)", templatePart.DefaultValue);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_DefaultValueWithEqualsSignIn_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"par=test(am):est=asd");
+
+ // Assert
+ Assert.Equal("par", templatePart.Name);
+ Assert.Equal("test(am):est=asd", templatePart.DefaultValue);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(=)=sds");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("sds", templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal("test(=)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_PatternIsParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\{)");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\{)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithOpenBraceInName_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"par{am:test(\sd)");
+
+ // Assert
+ Assert.Equal("par{am", templatePart.Name);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\sd)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\{)=xvc");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ Assert.Equal("xvc", templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\{)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithOpenParenInName_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"par(am:test(\()");
+
+ // Assert
+ Assert.Equal("par(am", templatePart.Name);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\()", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithOpenParenInPattern_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\()");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\()", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithOpenParenNoCloseParen_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(#$%");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal("test(#$%", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithOpenParenAndColon_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(#:test1");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ Assert.Collection(templatePart.InlineConstraints,
+ constraint => Assert.Equal(@"test(#", constraint.Constraint),
+ constraint => Assert.Equal(@"test1", constraint.Constraint));
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithOpenParenAndColonWithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(abc:somevalue):name(test1:differentname=default-value");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("default-value", templatePart.DefaultValue);
+
+ Assert.Collection(templatePart.InlineConstraints,
+ constraint => Assert.Equal(@"test(abc:somevalue)", constraint.Constraint),
+ constraint => Assert.Equal(@"name(test1", constraint.Constraint),
+ constraint => Assert.Equal(@"differentname", constraint.Constraint));
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithOpenParenAndDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(constraintvalue=test1");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("test1", templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(constraintvalue", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithOpenParenInPattern_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\()=djk");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+
+ Assert.Equal("djk", templatePart.DefaultValue);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\()", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_PatternIsParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\?)");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Null(templatePart.DefaultValue);
+ Assert.False(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\?)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_PatternIsParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\?)?");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Null(templatePart.DefaultValue);
+ Assert.True(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\?)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\?)=sdf");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("sdf", templatePart.DefaultValue);
+ Assert.False(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\?)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_WithDefaultValue_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(\?)=sdf?");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Equal("sdf", templatePart.DefaultValue);
+ Assert.True(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\?)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithQuestionMarkInName_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"par?am:test(\?)");
+
+ // Assert
+ Assert.Equal("par?am", templatePart.Name);
+ Assert.Null(templatePart.DefaultValue);
+ Assert.False(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(\?)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithClosedParenAndColonInPattern_ParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(#):$)");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Null(templatePart.DefaultValue);
+ Assert.False(templatePart.IsOptional);
+
+ Assert.Collection(templatePart.InlineConstraints,
+ constraint => Assert.Equal(@"test(#)", constraint.Constraint),
+ constraint => Assert.Equal(@"$)", constraint.Constraint));
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithColonAndClosedParenInPattern_PatternIsParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"param:test(#:)$)");
+
+ // Assert
+ Assert.Equal("param", templatePart.Name);
+ Assert.Null(templatePart.DefaultValue);
+ Assert.False(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"test(#:)$)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ContainingMultipleUnclosedParenthesisInConstraint()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"foo:regex(\\(\\(\\(\\()");
+
+ // Assert
+ Assert.Equal("foo", templatePart.Name);
+ Assert.Null(templatePart.DefaultValue);
+ Assert.False(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"regex(\\(\\(\\(\\()", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithBraces_PatternIsParsedCorrectly()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)"); // ssn
+
+ // Assert
+ Assert.Equal("p1", templatePart.Name);
+ Assert.Null(templatePart.DefaultValue);
+ Assert.False(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Constraint);
+ }
+
+ [Fact]
+ public void ParseRouteParameter_ConstraintWithBraces_WithDefaultValue()
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)=123-456-7890"); // ssn
+
+ // Assert
+ Assert.Equal("p1", templatePart.Name);
+ Assert.Equal("123-456-7890", templatePart.DefaultValue);
+ Assert.False(templatePart.IsOptional);
+
+ var constraint = Assert.Single(templatePart.InlineConstraints);
+ Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Constraint);
+ }
+
+ [Theory]
+ [InlineData("", "")]
+ [InlineData("?", "")]
+ [InlineData("*", "")]
+ [InlineData(" ", " ")]
+ [InlineData("\t", "\t")]
+ [InlineData("#!@#$%Q@#@%", "#!@#$%Q@#@%")]
+ [InlineData(",,,", ",,,")]
+ public void ParseRouteParameter_ParameterWithoutInlineConstraint_ReturnsTemplatePartWithEmptyInlineValues(
+ string parameter,
+ string expectedParameterName)
+ {
+ // Arrange & Act
+ var templatePart = ParseParameter(parameter);
+
+ // Assert
+ Assert.Equal(expectedParameterName, templatePart.Name);
+ Assert.Empty(templatePart.InlineConstraints);
+ Assert.Null(templatePart.DefaultValue);
+ }
+
+
+ private TemplatePart ParseParameter(string routeParameter)
+ {
+ var _constraintResolver = GetConstraintResolver();
+ var templatePart = InlineRouteParameterParser.ParseRouteParameter(routeParameter);
+ return templatePart;
+ }
+
+ private static RouteTemplate ParseRouteTemplate(string template)
+ {
+ var _constraintResolver = GetConstraintResolver();
+ return TemplateParser.Parse(template);
+ }
+
+ private static IInlineConstraintResolver GetConstraintResolver()
+ {
+ var services = new ServiceCollection().AddOptions();
+ services.Configure<RouteOptions>(options =>
+ options
+ .ConstraintMap
+ .Add("test", typeof(TestRouteConstraint)));
+ var serviceProvider = services.BuildServiceProvider();
+ var accessor = serviceProvider.GetRequiredService<IOptions<RouteOptions>>();
+ return new DefaultInlineConstraintResolver(accessor);
+ }
+
+ private class TestRouteConstraint : IRouteConstraint
+ {
+ public TestRouteConstraint(string pattern)
+ {
+ Pattern = pattern;
+ }
+
+ public string Pattern { get; private set; }
+ public bool Match(HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs
new file mode 100644
index 0000000000..8a35edcf13
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs
@@ -0,0 +1,339 @@
+// 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.Linq;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Template;
+using Microsoft.AspNetCore.Routing.Tree;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Internal.Routing
+{
+ public class LinkGenerationDecisionTreeTest
+ {
+ [Fact]
+ public void SelectSingleEntry_NoCriteria()
+ {
+ // Arrange
+ var entries = new List<OutboundMatch>();
+
+ var entry = CreateMatch(new { });
+ entries.Add(entry);
+
+ var tree = new LinkGenerationDecisionTree(entries);
+
+ var context = CreateContext(new { });
+
+ // Act
+ var matches = tree.GetMatches(context);
+
+ // Assert
+ Assert.Same(entry, Assert.Single(matches).Match);
+ }
+
+ [Fact]
+ public void SelectSingleEntry_MultipleCriteria()
+ {
+ // Arrange
+ var entries = new List<OutboundMatch>();
+
+ var entry = CreateMatch(new { controller = "Store", action = "Buy" });
+ entries.Add(entry);
+
+ var tree = new LinkGenerationDecisionTree(entries);
+
+ var context = CreateContext(new { controller = "Store", action = "Buy" });
+
+ // Act
+ var matches = tree.GetMatches(context);
+
+ // Assert
+ Assert.Same(entry, Assert.Single(matches).Match);
+ }
+
+ [Fact]
+ public void SelectSingleEntry_MultipleCriteria_AmbientValues()
+ {
+ // Arrange
+ var entries = new List<OutboundMatch>();
+
+ var entry = CreateMatch(new { controller = "Store", action = "Buy" });
+ entries.Add(entry);
+
+ var tree = new LinkGenerationDecisionTree(entries);
+
+ var context = CreateContext(values: null, ambientValues: new { controller = "Store", action = "Buy" });
+
+ // Act
+ var matches = tree.GetMatches(context);
+
+ // Assert
+ var match = Assert.Single(matches);
+ Assert.Same(entry, match.Match);
+ Assert.False(match.IsFallbackMatch);
+ }
+
+ [Fact]
+ public void SelectSingleEntry_MultipleCriteria_Replaced()
+ {
+ // Arrange
+ var entries = new List<OutboundMatch>();
+
+ var entry = CreateMatch(new { controller = "Store", action = "Buy" });
+ entries.Add(entry);
+
+ var tree = new LinkGenerationDecisionTree(entries);
+
+ var context = CreateContext(
+ values: new { action = "Buy" },
+ ambientValues: new { controller = "Store", action = "Cart" });
+
+ // Act
+ var matches = tree.GetMatches(context);
+
+ // Assert
+ var match = Assert.Single(matches);
+ Assert.Same(entry, match.Match);
+ Assert.False(match.IsFallbackMatch);
+ }
+
+ [Fact]
+ public void SelectSingleEntry_MultipleCriteria_AmbientValue_Ignored()
+ {
+ // Arrange
+ var entries = new List<OutboundMatch>();
+
+ var entry = CreateMatch(new { controller = "Store", action = (string)null });
+ entries.Add(entry);
+
+ var tree = new LinkGenerationDecisionTree(entries);
+
+ var context = CreateContext(
+ values: new { controller = "Store" },
+ ambientValues: new { controller = "Store", action = "Buy" });
+
+ // Act
+ var matches = tree.GetMatches(context);
+
+ // Assert
+ var match = Assert.Single(matches);
+ Assert.Same(entry, match.Match);
+ Assert.True(match.IsFallbackMatch);
+ }
+
+ [Fact]
+ public void SelectSingleEntry_MultipleCriteria_NoMatch()
+ {
+ // Arrange
+ var entries = new List<OutboundMatch>();
+
+ var entry = CreateMatch(new { controller = "Store", action = "Buy" });
+ entries.Add(entry);
+
+ var tree = new LinkGenerationDecisionTree(entries);
+
+ var context = CreateContext(new { controller = "Store", action = "AddToCart" });
+
+ // Act
+ var matches = tree.GetMatches(context);
+
+ // Assert
+ Assert.Empty(matches);
+ }
+
+ [Fact]
+ public void SelectSingleEntry_MultipleCriteria_AmbientValue_NoMatch()
+ {
+ // Arrange
+ var entries = new List<OutboundMatch>();
+
+ var entry = CreateMatch(new { controller = "Store", action = "Buy" });
+ entries.Add(entry);
+
+ var tree = new LinkGenerationDecisionTree(entries);
+
+ var context = CreateContext(
+ values: new { controller = "Store" },
+ ambientValues: new { controller = "Store", action = "Cart" });
+
+ // Act
+ var matches = tree.GetMatches(context);
+
+ // Assert
+ Assert.Empty(matches);
+ }
+
+ [Fact]
+ public void SelectMultipleEntries_OneDoesntMatch()
+ {
+ // Arrange
+ var entries = new List<OutboundMatch>();
+
+ var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
+ entries.Add(entry1);
+
+ var entry2 = CreateMatch(new { controller = "Store", action = "Cart" });
+ entries.Add(entry2);
+
+ var tree = new LinkGenerationDecisionTree(entries);
+
+ var context = CreateContext(
+ values: new { controller = "Store" },
+ ambientValues: new { controller = "Store", action = "Buy" });
+
+ // Act
+ var matches = tree.GetMatches(context);
+
+ // Assert
+ Assert.Same(entry1, Assert.Single(matches).Match);
+ }
+
+ [Fact]
+ public void SelectMultipleEntries_BothMatch_CriteriaSubset()
+ {
+ // Arrange
+ var entries = new List<OutboundMatch>();
+
+ var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
+ entries.Add(entry1);
+
+ var entry2 = CreateMatch(new { controller = "Store" });
+ entry2.Entry.Order = 1;
+ entries.Add(entry2);
+
+ var tree = new LinkGenerationDecisionTree(entries);
+
+ var context = CreateContext(
+ values: new { controller = "Store" },
+ ambientValues: new { controller = "Store", action = "Buy" });
+
+ // Act
+ var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
+
+ // Assert
+ Assert.Equal(entries, matches);
+ }
+
+ [Fact]
+ public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria()
+ {
+ // Arrange
+ var entries = new List<OutboundMatch>();
+
+ var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
+ entries.Add(entry1);
+
+ var entry2 = CreateMatch(new { slug = "1234" });
+ entry2.Entry.Order = 1;
+ entries.Add(entry2);
+
+ var tree = new LinkGenerationDecisionTree(entries);
+
+ var context = CreateContext(new { controller = "Store", action = "Buy", slug = "1234" });
+
+ // Act
+ var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
+
+ // Assert
+ Assert.Equal(entries, matches);
+ }
+
+ // Precedence is ignored for sorting because they have different order
+ [Fact]
+ public void SelectMultipleEntries_BothMatch_OrderedByOrder()
+ {
+ // Arrange
+ var entries = new List<OutboundMatch>();
+
+ var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
+ entry1.Entry.Precedence = 0;
+ entries.Add(entry1);
+
+ var entry2 = CreateMatch(new { controller = "Store", action = "Buy" });
+ entry2.Entry.Order = 1;
+ entry2.Entry.Precedence = 1;
+ entries.Add(entry2);
+
+ var tree = new LinkGenerationDecisionTree(entries);
+
+ var context = CreateContext(new { controller = "Store", action = "Buy" });
+
+ // Act
+ var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
+
+ // Assert
+ Assert.Equal(entries, matches);
+ }
+
+ // Precedence is used for sorting because they have the same order
+ [Fact]
+ public void SelectMultipleEntries_BothMatch_OrderedByPrecedence()
+ {
+ // Arrange
+ var entries = new List<OutboundMatch>();
+
+ var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
+ entry1.Entry.Precedence = 1;
+ entries.Add(entry1);
+
+ var entry2 = CreateMatch(new { controller = "Store", action = "Buy" });
+ entry2.Entry.Precedence = 0;
+ entries.Add(entry2);
+
+ var tree = new LinkGenerationDecisionTree(entries);
+
+ var context = CreateContext(new { controller = "Store", action = "Buy" });
+
+ // Act
+ var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
+
+ // Assert
+ Assert.Equal(entries, matches);
+ }
+
+ // Template is used for sorting because they have the same order
+ [Fact]
+ public void SelectMultipleEntries_BothMatch_OrderedByTemplate()
+ {
+ // Arrange
+ var entries = new List<OutboundMatch>();
+
+ var entry1 = CreateMatch(new { controller = "Store", action = "Buy" });
+ entry1.Entry.RouteTemplate = TemplateParser.Parse("a");
+ entries.Add(entry1);
+
+ var entry2 = CreateMatch(new { controller = "Store", action = "Buy" });
+ entry2.Entry.RouteTemplate = TemplateParser.Parse("b");
+ entries.Add(entry2);
+
+ var tree = new LinkGenerationDecisionTree(entries);
+
+ var context = CreateContext(new { controller = "Store", action = "Buy" });
+
+ // Act
+ var matches = tree.GetMatches(context).Select(m => m.Match).ToList();
+
+ // Assert
+ Assert.Equal(entries, matches);
+ }
+
+ private OutboundMatch CreateMatch(object requiredValues)
+ {
+ var match = new OutboundMatch();
+ match.Entry = new OutboundRouteEntry();
+ match.Entry.RequiredLinkValues = new RouteValueDictionary(requiredValues);
+ return match;
+ }
+
+ private VirtualPathContext CreateContext(object values, object ambientValues = null)
+ {
+ var context = new VirtualPathContext(
+ new DefaultHttpContext(),
+ new RouteValueDictionary(ambientValues),
+ new RouteValueDictionary(values));
+
+ return context;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/PathTokenizerTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/PathTokenizerTest.cs
new file mode 100644
index 0000000000..78b9685e63
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Internal/PathTokenizerTest.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 Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Internal
+{
+ public class PathTokenizerTest
+ {
+ public static TheoryData<string, StringSegment[]> TokenizationData
+ {
+ get
+ {
+ return new TheoryData<string, StringSegment[]>
+ {
+ { string.Empty, new StringSegment[] { } },
+ { "/", new StringSegment[] { } },
+ { "//", new StringSegment[] { new StringSegment("//", 1, 0) } },
+ {
+ "///",
+ new StringSegment[]
+ {
+ new StringSegment("///", 1, 0),
+ new StringSegment("///", 2, 0),
+ }
+ },
+ {
+ "////",
+ new StringSegment[]
+ {
+ new StringSegment("////", 1, 0),
+ new StringSegment("////", 2, 0),
+ new StringSegment("////", 3, 0),
+ }
+ },
+ { "/zero", new StringSegment[] { new StringSegment("/zero", 1, 4) } },
+ { "/zero/", new StringSegment[] { new StringSegment("/zero/", 1, 4) } },
+ {
+ "/zero/one",
+ new StringSegment[]
+ {
+ new StringSegment("/zero/one", 1, 4),
+ new StringSegment("/zero/one", 6, 3),
+ }
+ },
+ {
+ "/zero/one/",
+ new StringSegment[]
+ {
+ new StringSegment("/zero/one/", 1, 4),
+ new StringSegment("/zero/one/", 6, 3),
+ }
+ },
+ {
+ "/zero/one/two",
+ new StringSegment[]
+ {
+ new StringSegment("/zero/one/two", 1, 4),
+ new StringSegment("/zero/one/two", 6, 3),
+ new StringSegment("/zero/one/two", 10, 3),
+ }
+ },
+ {
+ "/zero/one/two/",
+ new StringSegment[]
+ {
+ new StringSegment("/zero/one/two/", 1, 4),
+ new StringSegment("/zero/one/two/", 6, 3),
+ new StringSegment("/zero/one/two/", 10, 3),
+ }
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TokenizationData))]
+ public void PathTokenizer_Count(string path, StringSegment[] expectedSegments)
+ {
+ // Arrange
+ var tokenizer = new PathTokenizer(new PathString(path));
+
+ // Act
+ var count = tokenizer.Count;
+
+ // Assert
+ Assert.Equal(expectedSegments.Length, count);
+ }
+
+ [Theory]
+ [MemberData(nameof(TokenizationData))]
+ public void PathTokenizer_Indexer(string path, StringSegment[] expectedSegments)
+ {
+ // Arrange
+ var tokenizer = new PathTokenizer(new PathString(path));
+
+ // Act & Assert
+ for (var i = 0; i < expectedSegments.Length; i++)
+ {
+ Assert.Equal(expectedSegments[i], tokenizer[i]);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TokenizationData))]
+ public void PathTokenizer_Enumerator(string path, StringSegment[] expectedSegments)
+ {
+ // Arrange
+ var tokenizer = new PathTokenizer(new PathString(path));
+
+ // Act & Assert
+ Assert.Equal<StringSegment>(expectedSegments, tokenizer);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Logging/WriteContext.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Logging/WriteContext.cs
new file mode 100644
index 0000000000..4a5fa51e04
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Logging/WriteContext.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;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class WriteContext
+ {
+ public LogLevel LogLevel { get; set; }
+
+ public int EventId { get; set; }
+
+ public object State { get; set; }
+
+ public Exception Exception { get; set; }
+
+ public Func<object, Exception, string> Formatter { get; set; }
+
+ public object Scope { get; set; }
+
+ public string LoggerName { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Microsoft.AspNetCore.Routing.Tests.csproj b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Microsoft.AspNetCore.Routing.Tests.csproj
new file mode 100644
index 0000000000..ee389c233d
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Microsoft.AspNetCore.Routing.Tests.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.WebEncoders" Version="$(MicrosoftExtensionsWebEncodersPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RequestDelegateRouteBuilderExtensionsTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RequestDelegateRouteBuilderExtensionsTest.cs
new file mode 100644
index 0000000000..534c193a93
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RequestDelegateRouteBuilderExtensionsTest.cs
@@ -0,0 +1,159 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.ObjectPool;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ // These are really more like integration tests. They verify that these extensions
+ // add routes that behave as advertised.
+ public class RequestDelegateRouteBuilderExtensionsTest
+ {
+ private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0);
+
+ public static TheoryData<Action<IRouteBuilder>, Action<HttpContext>> MatchingActions
+ {
+ get
+ {
+ return new TheoryData<Action<IRouteBuilder>, Action<HttpContext>>()
+ {
+ { b => { b.MapRoute("api/{id}", NullHandler); }, null },
+ { b => { b.MapMiddlewareRoute("api/{id}", app => { }); }, null },
+
+ { b => { b.MapDelete("api/{id}", NullHandler); }, c => { c.Request.Method = "DELETE"; } },
+ { b => { b.MapMiddlewareDelete("api/{id}", app => { }); }, c => { c.Request.Method = "DELETE"; } },
+ { b => { b.MapGet("api/{id}", NullHandler); }, c => { c.Request.Method = "GET"; } },
+ { b => { b.MapMiddlewareGet("api/{id}", app => { }); }, c => { c.Request.Method = "GET"; } },
+ { b => { b.MapPost("api/{id}", NullHandler); }, c => { c.Request.Method = "POST"; } },
+ { b => { b.MapMiddlewarePost("api/{id}", app => { }); }, c => { c.Request.Method = "POST"; } },
+ { b => { b.MapPut("api/{id}", NullHandler); }, c => { c.Request.Method = "PUT"; } },
+ { b => { b.MapMiddlewarePut("api/{id}", app => { }); }, c => { c.Request.Method = "PUT"; } },
+
+ { b => { b.MapVerb("PUT", "api/{id}", NullHandler); }, c => { c.Request.Method = "PUT"; } },
+ { b => { b.MapMiddlewareVerb("PUT", "api/{id}", app => { }); }, c => { c.Request.Method = "PUT"; } },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(MatchingActions))]
+ public async Task Map_MatchesRequest(
+ Action<IRouteBuilder> routeSetup,
+ Action<HttpContext> requestSetup)
+ {
+ // Arrange
+ var services = CreateServices();
+
+ var context = CreateRouteContext(services);
+ context.HttpContext.Request.Path = new PathString("/api/5");
+ requestSetup?.Invoke(context.HttpContext);
+
+ var builder = CreateRouteBuilder(services);
+ routeSetup(builder);
+ var route = builder.Build();
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Same(NullHandler, context.Handler);
+ }
+
+ public static TheoryData<Action<IRouteBuilder>, Action<HttpContext>> NonmatchingActions
+ {
+ get
+ {
+ return new TheoryData<Action<IRouteBuilder>, Action<HttpContext>>()
+ {
+ { b => { b.MapRoute("api/{id}/extra", NullHandler); }, null },
+ { b => { b.MapMiddlewareRoute("api/{id}/extra", app => { }); }, null },
+
+ { b => { b.MapDelete("api/{id}", NullHandler); }, c => { c.Request.Method = "GET"; } },
+ { b => { b.MapMiddlewareDelete("api/{id}", app => { }); }, c => { c.Request.Method = "PUT"; } },
+ { b => { b.MapDelete("api/{id}/extra", NullHandler); }, c => { c.Request.Method = "DELETE"; } },
+ { b => { b.MapMiddlewareDelete("api/{id}/extra", app => { }); }, c => { c.Request.Method = "DELETE"; } },
+ { b => { b.MapGet("api/{id}", NullHandler); }, c => { c.Request.Method = "PUT"; } },
+ { b => { b.MapMiddlewareGet("api/{id}", app => { }); }, c => { c.Request.Method = "POST"; } },
+ { b => { b.MapGet("api/{id}/extra", NullHandler); }, c => { c.Request.Method = "GET"; } },
+ { b => { b.MapMiddlewareGet("api/{id}/extra", app => { }); }, c => { c.Request.Method = "GET"; } },
+ { b => { b.MapPost("api/{id}", NullHandler); }, c => { c.Request.Method = "MEH"; } },
+ { b => { b.MapMiddlewarePost("api/{id}", app => { }); }, c => { c.Request.Method = "DELETE"; } },
+ { b => { b.MapPost("api/{id}/extra", NullHandler); }, c => { c.Request.Method = "POST"; } },
+ { b => { b.MapMiddlewarePost("api/{id}/extra", app => { }); }, c => { c.Request.Method = "POST"; } },
+ { b => { b.MapPut("api/{id}", NullHandler); }, c => { c.Request.Method = "BLEH"; } },
+ { b => { b.MapMiddlewarePut("api/{id}", app => { }); }, c => { c.Request.Method = "HEAD"; } },
+ { b => { b.MapPut("api/{id}/extra", NullHandler); }, c => { c.Request.Method = "PUT"; } },
+ { b => { b.MapMiddlewarePut("api/{id}/extra", app => { }); }, c => { c.Request.Method = "PUT"; } },
+
+ { b => { b.MapVerb("PUT", "api/{id}", NullHandler); }, c => { c.Request.Method = "POST"; } },
+ { b => { b.MapMiddlewareVerb("PUT", "api/{id}", app => { }); }, c => { c.Request.Method = "HEAD"; } },
+ { b => { b.MapVerb("PUT", "api/{id}/extra", NullHandler); }, c => { c.Request.Method = "PUT"; } },
+ { b => { b.MapMiddlewareVerb("PUT", "api/{id}/extra", app => { }); }, c => { c.Request.Method = "PUT"; } },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(NonmatchingActions))]
+ public async Task Map_DoesNotMatchRequest(
+ Action<IRouteBuilder> routeSetup,
+ Action<HttpContext> requestSetup)
+ {
+ // Arrange
+ var services = CreateServices();
+
+ var context = CreateRouteContext(services);
+ context.HttpContext.Request.Path = new PathString("/api/5");
+ requestSetup?.Invoke(context.HttpContext);
+
+ var builder = CreateRouteBuilder(services);
+ routeSetup(builder);
+ var route = builder.Build();
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+ }
+
+ private static IServiceProvider CreateServices()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
+ services.AddOptions();
+ services.AddRouting();
+ services.AddLogging();
+ return services.BuildServiceProvider();
+ }
+
+ private static RouteContext CreateRouteContext(IServiceProvider services)
+ {
+ var httpContext = new DefaultHttpContext();
+ httpContext.RequestServices = services;
+ return new RouteContext(httpContext);
+ }
+
+ private static IRouteBuilder CreateRouteBuilder(IServiceProvider services)
+ {
+ var applicationBuilder = new Mock<IApplicationBuilder>();
+ applicationBuilder.SetupAllProperties();
+
+ applicationBuilder
+ .Setup(b => b.New().Build())
+ .Returns(NullHandler);
+
+ applicationBuilder.Object.ApplicationServices = services;
+
+ var routeBuilder = new RouteBuilder(applicationBuilder.Object);
+ return routeBuilder;
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteBuilderTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteBuilderTest.cs
new file mode 100644
index 0000000000..edd3e09cba
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteBuilderTest.cs
@@ -0,0 +1,55 @@
+// 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.Builder;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class RouteBuilderTest
+ {
+ [Fact]
+ public void Ctor_SetsPropertyValues()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ services.AddSingleton(typeof(RoutingMarkerService));
+ var applicationServices = services.BuildServiceProvider();
+ var applicationBuilderMock = new Mock<IApplicationBuilder>();
+ applicationBuilderMock.Setup(a => a.ApplicationServices).Returns(applicationServices);
+ var applicationBuilder = applicationBuilderMock.Object;
+ var defaultHandler = Mock.Of<IRouter>();
+
+ // Act
+ var builder = new RouteBuilder(applicationBuilder, defaultHandler);
+
+ // Assert
+ Assert.Same(applicationBuilder, builder.ApplicationBuilder);
+ Assert.Same(defaultHandler, builder.DefaultHandler);
+ Assert.Same(applicationServices, builder.ServiceProvider);
+ }
+
+ [Fact]
+ public void Ctor_ThrowsInvalidOperationException_IfRoutingMarkerServiceIsNotRegistered()
+ {
+ // Arrange
+ var applicationBuilderMock = new Mock<IApplicationBuilder>();
+ applicationBuilderMock
+ .Setup(s => s.ApplicationServices)
+ .Returns(Mock.Of<IServiceProvider>());
+
+ // Act & Assert
+ var exception = Assert.Throws<InvalidOperationException>(() => new RouteBuilder(applicationBuilderMock.Object));
+
+ Assert.Equal(
+ "Unable to find the required services. Please add all the required services by calling " +
+ "'IServiceCollection.AddRouting' inside the call to 'ConfigureServices(...)'" +
+ " in the application startup code.",
+ exception.Message);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteCollectionTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteCollectionTest.cs
new file mode 100644
index 0000000000..c0d5a0187a
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteCollectionTest.cs
@@ -0,0 +1,675 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class RouteCollectionTest
+ {
+ private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0);
+
+ [Theory]
+ [InlineData(@"Home/Index/23", "/home/index/23", true, false)]
+ [InlineData(@"Home/Index/23", "/Home/Index/23", false, false)]
+ [InlineData(@"Home/Index/23", "/home/index/23/", true, true)]
+ [InlineData(@"Home/Index/23", "/Home/Index/23/", false, true)]
+ [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/Home/Index/23/?Param1=ABC&Param2=Xyz", false, true)]
+ [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/Home/Index/23?Param1=ABC&Param2=Xyz", false, false)]
+ [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/home/index/23/?Param1=ABC&Param2=Xyz", true, true)]
+ [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/Home/Index/23/#Param1=ABC&Param2=Xyz", false, true)]
+ [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/home/index/23#Param1=ABC&Param2=Xyz", true, false)]
+ [InlineData(@"Home/Index/23/?Param1=ABC&Param2=Xyz", "/home/index/23/?Param1=ABC&Param2=Xyz", true, true)]
+ [InlineData(@"Home/Index/23/#Param1=ABC&Param2=Xyz", "/home/index/23/#Param1=ABC&Param2=Xyz", true, false)]
+ public void GetVirtualPath_CanLowerCaseUrls_And_AppendTrailingSlash_BasedOnOptions(
+ string returnUrl,
+ string expectedUrl,
+ bool lowercaseUrls,
+ bool appendTrailingSlash)
+ {
+ // Arrange
+ var target = new Mock<IRouter>(MockBehavior.Strict);
+ target
+ .Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
+ .Returns(new VirtualPathData(target.Object, returnUrl));
+
+ var routeCollection = new RouteCollection();
+ routeCollection.Add(target.Object);
+ var virtualPathContext = CreateVirtualPathContext(
+ options: GetRouteOptions(
+ lowerCaseUrls: lowercaseUrls,
+ appendTrailingSlash: appendTrailingSlash));
+
+ // Act
+ var pathData = routeCollection.GetVirtualPath(virtualPathContext);
+
+ // Assert
+ Assert.Equal(expectedUrl, pathData.VirtualPath);
+ Assert.Same(target.Object, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Theory]
+ [InlineData(@"\u0130", @"/\u0130", true)]
+ [InlineData(@"\u0049", @"/\u0049", true)]
+ [InlineData(@"�ino", @"/�ino", true)]
+ public void GetVirtualPath_DoesntLowerCaseUrls_Invariant(
+ string returnUrl,
+ string lowercaseUrl,
+ bool lowercaseUrls)
+ {
+ // Arrange
+ var target = new Mock<IRouter>(MockBehavior.Strict);
+ target
+ .Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
+ .Returns(new VirtualPathData(target.Object, returnUrl));
+
+ var routeCollection = new RouteCollection();
+ routeCollection.Add(target.Object);
+ var virtualPathContext = CreateVirtualPathContext(options: GetRouteOptions(lowercaseUrls));
+
+ // Act
+ var pathData = routeCollection.GetVirtualPath(virtualPathContext);
+
+ // Assert
+ Assert.Equal(lowercaseUrl, pathData.VirtualPath);
+ Assert.Same(target.Object, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Theory]
+ [MemberData(nameof(DataTokensTestData))]
+ public void GetVirtualPath_ReturnsDataTokens(RouteValueDictionary dataTokens, string routerName)
+ {
+ // Arrange
+ var virtualPath = "/TestVirtualPath";
+
+ var pathContextValues = new RouteValueDictionary { { "controller", virtualPath } };
+
+ var pathContext = CreateVirtualPathContext(
+ pathContextValues,
+ GetRouteOptions(),
+ routerName);
+
+ var route = CreateTemplateRoute("{controller}", routerName, dataTokens);
+ var routeCollection = new RouteCollection();
+ routeCollection.Add(route);
+
+ var expectedDataTokens = dataTokens ?? new RouteValueDictionary();
+
+ // Act
+ var pathData = routeCollection.GetVirtualPath(pathContext);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Same(route, pathData.Router);
+
+ Assert.Equal(virtualPath, pathData.VirtualPath);
+
+ Assert.Equal(expectedDataTokens.Count, pathData.DataTokens.Count);
+ foreach (var dataToken in expectedDataTokens)
+ {
+ Assert.True(pathData.DataTokens.ContainsKey(dataToken.Key));
+ Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]);
+ }
+ }
+
+ [Fact]
+ public async Task RouteAsync_FirstMatches()
+ {
+ // Arrange
+ var routes = new RouteCollection();
+
+ var route1 = CreateRoute(accept: true);
+ routes.Add(route1.Object);
+
+ var route2 = CreateRoute(accept: false);
+ routes.Add(route2.Object);
+
+ var context = CreateRouteContext("/Cool");
+
+ // Act
+ await routes.RouteAsync(context);
+
+ // Assert
+ route1.Verify(e => e.RouteAsync(It.IsAny<RouteContext>()), Times.Exactly(1));
+ route2.Verify(e => e.RouteAsync(It.IsAny<RouteContext>()), Times.Exactly(0));
+ Assert.NotNull(context.Handler);
+
+ Assert.Equal(1, context.RouteData.Routers.Count);
+ Assert.Same(route1.Object, context.RouteData.Routers[0]);
+ }
+
+ [Fact]
+ public async Task RouteAsync_SecondMatches()
+ {
+ // Arrange
+
+ var routes = new RouteCollection();
+ var route1 = CreateRoute(accept: false);
+ routes.Add(route1.Object);
+
+ var route2 = CreateRoute(accept: true);
+ routes.Add(route2.Object);
+
+ var context = CreateRouteContext("/Cool");
+
+ // Act
+ await routes.RouteAsync(context);
+
+ // Assert
+ route1.Verify(e => e.RouteAsync(It.IsAny<RouteContext>()), Times.Exactly(1));
+ route2.Verify(e => e.RouteAsync(It.IsAny<RouteContext>()), Times.Exactly(1));
+ Assert.NotNull(context.Handler);
+
+ Assert.Equal(1, context.RouteData.Routers.Count);
+ Assert.Same(route2.Object, context.RouteData.Routers[0]);
+ }
+
+ [Fact]
+ public async Task RouteAsync_NoMatch()
+ {
+ // Arrange
+ var routes = new RouteCollection();
+ var route1 = CreateRoute(accept: false);
+ routes.Add(route1.Object);
+
+ var route2 = CreateRoute(accept: false);
+ routes.Add(route2.Object);
+
+ var context = CreateRouteContext("/Cool");
+
+ // Act
+ await routes.RouteAsync(context);
+
+ // Assert
+ route1.Verify(e => e.RouteAsync(It.IsAny<RouteContext>()), Times.Exactly(1));
+ route2.Verify(e => e.RouteAsync(It.IsAny<RouteContext>()), Times.Exactly(1));
+ Assert.Null(context.Handler);
+
+ Assert.Empty(context.RouteData.Routers);
+ }
+
+ [Theory]
+ [InlineData(false, "/RouteName")]
+ [InlineData(true, "/routename")]
+ public void NamedRouteTests_GetNamedRoute_ReturnsValue(bool lowercaseUrls, string expectedUrl)
+ {
+ // Arrange
+ var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "RouteName", "Route3" });
+ var virtualPathContext = CreateVirtualPathContext(
+ routeName: "RouteName",
+ options: GetRouteOptions(lowercaseUrls));
+
+ // Act
+ var pathData = routeCollection.GetVirtualPath(virtualPathContext);
+
+ // Assert
+ Assert.Equal(expectedUrl, pathData.VirtualPath);
+ var namedRouter = Assert.IsAssignableFrom<INamedRouter>(pathData.Router);
+ Assert.Equal(virtualPathContext.RouteName, namedRouter.Name);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void NamedRouteTests_GetNamedRoute_RouteNotFound()
+ {
+ // Arrange
+ var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "Route3" });
+ var virtualPathContext = CreateVirtualPathContext("NonExistantRoute");
+
+ // Act
+ var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext);
+
+ // Assert
+ Assert.Null(stringVirtualPath);
+ }
+
+ [Fact]
+ public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_DoesNotThrowForUnambiguousRoute()
+ {
+ // Arrange
+ var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "Route3", "Route4" });
+
+ // Add Duplicate route.
+ routeCollection.Add(CreateNamedRoute("Route3"));
+ var virtualPathContext = CreateVirtualPathContext(routeName: "Route1", options: GetRouteOptions(true));
+
+ // Act
+ var pathData = routeCollection.GetVirtualPath(virtualPathContext);
+
+ // Assert
+ Assert.Equal("/route1", pathData.VirtualPath);
+ var namedRouter = Assert.IsAssignableFrom<INamedRouter>(pathData.Router);
+ Assert.Equal("Route1", namedRouter.Name);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_ThrowsForAmbiguousRoute()
+ {
+ // Arrange
+ var ambiguousRoute = "ambiguousRoute";
+ var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", ambiguousRoute, "Route4" });
+
+ // Add Duplicate route.
+ routeCollection.Add(CreateNamedRoute(ambiguousRoute));
+ var virtualPathContext = CreateVirtualPathContext(routeName: ambiguousRoute, options: GetRouteOptions());
+
+ // Act & Assert
+ var ex = Assert.Throws<InvalidOperationException>(() => routeCollection.GetVirtualPath(virtualPathContext));
+ Assert.Equal(
+ "The supplied route name 'ambiguousRoute' is ambiguous and matched more than one route.",
+ ex.Message);
+ }
+
+ [Fact]
+ public void GetVirtualPath_AmbiguousRoutes_RequiresRouteValueValidation_Error()
+ {
+ // Arrange
+ var namedRoute = CreateNamedRoute("Ambiguous", accept: false);
+
+ var routeCollection = new RouteCollection();
+ routeCollection.Add(namedRoute);
+
+ var innerRouteCollection = new RouteCollection();
+ innerRouteCollection.Add(namedRoute);
+ routeCollection.Add(innerRouteCollection);
+
+ var virtualPathContext = CreateVirtualPathContext("Ambiguous");
+
+ // Act & Assert
+ var ex = Assert.Throws<InvalidOperationException>(() => routeCollection.GetVirtualPath(virtualPathContext));
+ Assert.Equal("The supplied route name 'Ambiguous' is ambiguous and matched more than one route.", ex.Message);
+ }
+
+ // "Integration" tests for RouteCollection
+
+ public static IEnumerable<object[]> IntegrationTestData
+ {
+ get
+ {
+ yield return new object[] {
+ "{controller}/{action}",
+ new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" } },
+ "/home/index",
+ true };
+
+ yield return new object[] {
+ "{controller}/{action}/",
+ new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" } },
+ "/Home/Index",
+ false };
+
+ yield return new object[] {
+ "api/{action}/",
+ new RouteValueDictionary { { "action", "Create" } },
+ "/api/create",
+ true };
+
+ yield return new object[] {
+ "api/{action}/{id}",
+ new RouteValueDictionary {
+ { "action", "Create" },
+ { "id", "23" },
+ { "Param1", "Value1" },
+ { "Param2", "Value2" } },
+ "/api/create/23?Param1=Value1&Param2=Value2",
+ true };
+
+ yield return new object[] {
+ "api/{action}/{id}",
+ new RouteValueDictionary {
+ { "action", "Create" },
+ { "id", "23" },
+ { "Param1", "Value1" },
+ { "Param2", "Value2" } },
+ "/api/Create/23?Param1=Value1&Param2=Value2",
+ false };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(IntegrationTestData))]
+ public void GetVirtualPath_Success(
+ string template,
+ RouteValueDictionary values,
+ string expectedUrl,
+ bool lowercaseUrls)
+ {
+ // Arrange
+ var routeCollection = new RouteCollection();
+ var route = CreateTemplateRoute(template);
+ routeCollection.Add(route);
+ var context = CreateVirtualPathContext(values, options: GetRouteOptions(lowercaseUrls));
+
+ // Act
+ var pathData = routeCollection.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal(expectedUrl, pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ public static IEnumerable<object[]> RestoresRouteDataForEachRouterData
+ {
+ get
+ {
+ // Here 'area' segment doesn't have a value but the later segments have values. This is an invalid
+ // route match and the url generation should look into the next available route in the collection.
+ yield return new object[] {
+ new Route[]
+ {
+ CreateTemplateRoute("{area?}/{controller=Home}/{action=Index}/{id?}", "1"),
+ CreateTemplateRoute("{controller=Home}/{action=Index}/{id?}", "2")
+ },
+ new RouteValueDictionary(new { controller = "Test", action = "Index" }),
+ "/Test",
+ "2" };
+
+ // Here the segment 'a' is valid but 'b' is not as it would be empty. This would be an invalid route match, but
+ // the route value of 'a' should still be present to be evaluated for the next available route.
+ yield return new object[] {
+ new[]
+ {
+ CreateTemplateRoute("{a}/{b?}/{c}", "1"),
+ CreateTemplateRoute("{a=Home}/{b=Index}", "2")
+ },
+ new RouteValueDictionary(new { a = "Test", c = "Foo" }),
+ "/Test?c=Foo",
+ "2" };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(RestoresRouteDataForEachRouterData))]
+ public void GetVirtualPath_RestoresRouteData_ForEachRouter(
+ Route[] routes,
+ RouteValueDictionary routeValues,
+ string expectedUrl,
+ string expectedRouteToMatch)
+ {
+ // Arrange
+ var routeCollection = new RouteCollection();
+ foreach (var route in routes)
+ {
+ routeCollection.Add(route);
+ }
+ var context = CreateVirtualPathContext(routeValues);
+
+ // Act
+ var pathData = routeCollection.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal(expectedUrl, pathData.VirtualPath);
+ Assert.Same(expectedRouteToMatch, ((INamedRouter)pathData.Router).Name);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_NoBestEffort_NoMatch()
+ {
+ // Arrange
+ var route1 = CreateRoute(accept: false, match: false, matchValue: "bad");
+ var route2 = CreateRoute(accept: false, match: false, matchValue: "bad");
+ var route3 = CreateRoute(accept: false, match: false, matchValue: "bad");
+
+ var routeCollection = new RouteCollection();
+ routeCollection.Add(route1.Object);
+ routeCollection.Add(route2.Object);
+ routeCollection.Add(route3.Object);
+
+ var virtualPathContext = CreateVirtualPathContext();
+
+ // Act
+ var path = routeCollection.GetVirtualPath(virtualPathContext);
+
+ Assert.Null(path);
+
+ // All of these should be called
+ route1.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
+ route2.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
+ route3.Verify(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()), Times.Once());
+ }
+
+ // DataTokens test data for RouterCollection.GetVirtualPath
+ public static IEnumerable<object[]> DataTokensTestData
+ {
+ get
+ {
+ yield return new object[] { null, null };
+ yield return new object[] { new RouteValueDictionary(), null };
+ yield return new object[] { new RouteValueDictionary() { { "tokenKey", "tokenValue" } }, null };
+
+ yield return new object[] { null, "routerA" };
+ yield return new object[] { new RouteValueDictionary(), "routerA" };
+ yield return new object[] { new RouteValueDictionary() { { "tokenKey", "tokenValue" } }, "routerA" };
+ }
+ }
+
+ private static RouteCollection GetRouteCollectionWithNamedRoutes(IEnumerable<string> routeNames)
+ {
+ var routes = new RouteCollection();
+ foreach (var routeName in routeNames)
+ {
+ var route1 = CreateNamedRoute(routeName, accept: true);
+ routes.Add(route1);
+ }
+
+ return routes;
+ }
+
+ private static RouteCollection GetNestedRouteCollection(string[] routeNames)
+ {
+ var random = new Random();
+ int index = random.Next(0, routeNames.Length - 1);
+ var first = routeNames.Take(index).ToArray();
+ var second = routeNames.Skip(index).ToArray();
+
+ var rc1 = GetRouteCollectionWithNamedRoutes(first);
+ var rc2 = GetRouteCollectionWithNamedRoutes(second);
+ var rc3 = new RouteCollection();
+ var rc4 = new RouteCollection();
+
+ rc1.Add(rc3);
+ rc4.Add(rc2);
+
+ // Add a few unnamedRoutes.
+ rc1.Add(CreateRoute(accept: false).Object);
+ rc2.Add(CreateRoute(accept: false).Object);
+ rc3.Add(CreateRoute(accept: false).Object);
+ rc3.Add(CreateRoute(accept: false).Object);
+ rc4.Add(CreateRoute(accept: false).Object);
+ rc4.Add(CreateRoute(accept: false).Object);
+
+ var routeCollection = new RouteCollection();
+ routeCollection.Add(rc1);
+ routeCollection.Add(rc4);
+
+ return routeCollection;
+ }
+
+ private static INamedRouter CreateNamedRoute(string name, bool accept = false, string matchValue = null)
+ {
+ if (matchValue == null)
+ {
+ matchValue = name;
+ }
+
+ var target = new Mock<INamedRouter>(MockBehavior.Strict);
+ target
+ .Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
+ .Returns<VirtualPathContext>(c =>
+ c.RouteName == name ? new VirtualPathData(target.Object, matchValue) : null)
+ .Verifiable();
+
+ target
+ .SetupGet(e => e.Name)
+ .Returns(name);
+
+ target
+ .Setup(e => e.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>((c) => c.Handler = accept ? NullHandler : null)
+ .Returns(Task.FromResult<object>(null))
+ .Verifiable();
+
+ return target.Object;
+ }
+
+ private static Route CreateTemplateRoute(
+ string template,
+ string routerName = null,
+ RouteValueDictionary dataTokens = null,
+ IInlineConstraintResolver constraintResolver = null)
+ {
+ var target = new Mock<IRouter>(MockBehavior.Strict);
+ target
+ .Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
+ .Returns<VirtualPathContext>(rc => null);
+
+ if (constraintResolver == null)
+ {
+ constraintResolver = new Mock<IInlineConstraintResolver>().Object;
+ }
+
+ return new Route(
+ target.Object,
+ routerName,
+ template,
+ defaults: null,
+ constraints: null,
+ dataTokens: dataTokens,
+ inlineConstraintResolver: constraintResolver);
+ }
+
+ private static VirtualPathContext CreateVirtualPathContext(
+ string routeName = null,
+ ILoggerFactory loggerFactory = null,
+ Action<RouteOptions> options = null)
+ {
+ if (loggerFactory == null)
+ {
+ loggerFactory = NullLoggerFactory.Instance;
+ }
+
+ var request = new Mock<HttpRequest>(MockBehavior.Strict);
+
+ var services = new ServiceCollection();
+ services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
+ services.AddOptions();
+ services.AddRouting();
+ if (options != null)
+ {
+ services.Configure(options);
+ }
+
+ var context = new Mock<HttpContext>(MockBehavior.Strict);
+ context.SetupGet(m => m.RequestServices).Returns(services.BuildServiceProvider());
+ context.SetupGet(c => c.Request).Returns(request.Object);
+
+ return new VirtualPathContext(context.Object, null, null, routeName);
+ }
+
+ private static VirtualPathContext CreateVirtualPathContext(
+ RouteValueDictionary values,
+ Action<RouteOptions> options = null,
+ string routeName = null)
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
+ services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
+ services.AddOptions();
+ services.AddRouting();
+ if (options != null)
+ {
+ services.Configure<RouteOptions>(options);
+ }
+
+ var context = new DefaultHttpContext
+ {
+ RequestServices = services.BuildServiceProvider(),
+ };
+
+ return new VirtualPathContext(
+ context,
+ ambientValues: null,
+ values: values,
+ routeName: routeName);
+ }
+
+ private static RouteContext CreateRouteContext(
+ string requestPath,
+ ILoggerFactory loggerFactory = null,
+ RouteOptions options = null)
+ {
+ if (loggerFactory == null)
+ {
+ loggerFactory = NullLoggerFactory.Instance;
+ }
+
+ if (options == null)
+ {
+ options = new RouteOptions();
+ }
+
+ var request = new Mock<HttpRequest>(MockBehavior.Strict);
+ request.SetupGet(r => r.Path).Returns(requestPath);
+
+ var optionsAccessor = new Mock<IOptions<RouteOptions>>(MockBehavior.Strict);
+ optionsAccessor.SetupGet(o => o.Value).Returns(options);
+
+ var context = new Mock<HttpContext>(MockBehavior.Strict);
+ context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory)))
+ .Returns(loggerFactory);
+ context.Setup(m => m.RequestServices.GetService(typeof(IOptions<RouteOptions>)))
+ .Returns(optionsAccessor.Object);
+ context.SetupGet(c => c.Request).Returns(request.Object);
+
+ return new RouteContext(context.Object);
+ }
+
+ private static Mock<IRouter> CreateRoute(
+ bool accept = true,
+ bool match = false,
+ string matchValue = "value")
+ {
+ var target = new Mock<IRouter>(MockBehavior.Strict);
+ target
+ .Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
+ .Returns(accept || match ? new VirtualPathData(target.Object, matchValue) : null)
+ .Verifiable();
+
+ target
+ .Setup(e => e.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>((c) => c.Handler = accept ? NullHandler : null)
+ .Returns(Task.FromResult<object>(null))
+ .Verifiable();
+
+ return target;
+ }
+
+ private static Action<RouteOptions> GetRouteOptions(
+ bool lowerCaseUrls = false,
+ bool appendTrailingSlash = false)
+ {
+ return (options) =>
+ {
+ options.LowercaseUrls = lowerCaseUrls;
+ options.AppendTrailingSlash = appendTrailingSlash;
+ };
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteConstraintBuilderTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteConstraintBuilderTest.cs
new file mode 100644
index 0000000000..9eca4d7759
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteConstraintBuilderTest.cs
@@ -0,0 +1,190 @@
+// 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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Constraints;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class RouteConstraintBuilderTest
+ {
+ [Fact]
+ public void AddConstraint_String_CreatesARegex()
+ {
+ // Arrange
+ var builder = CreateBuilder("{controller}/{action}");
+ builder.AddConstraint("controller", "abc");
+
+ // Act
+ var result = builder.Build();
+
+ // Assert
+ Assert.Equal(1, result.Count);
+ Assert.Equal("controller", result.First().Key);
+
+ Assert.IsType<RegexRouteConstraint>(Assert.Single(result).Value);
+ }
+
+ [Fact]
+ public void AddConstraint_IRouteConstraint()
+ {
+ // Arrange
+ var originalConstraint = Mock.Of<IRouteConstraint>();
+
+ var builder = CreateBuilder("{controller}/{action}");
+ builder.AddConstraint("controller", originalConstraint);
+
+ // Act
+ var result = builder.Build();
+
+ // Assert
+ Assert.Equal(1, result.Count);
+
+ var kvp = Assert.Single(result);
+ Assert.Equal("controller", kvp.Key);
+
+ Assert.Same(originalConstraint, kvp.Value);
+ }
+
+ [Fact]
+ public void AddResolvedConstraint_IRouteConstraint()
+ {
+ // Arrange
+ var builder = CreateBuilder("{controller}/{action}");
+ builder.AddResolvedConstraint("controller", "int");
+
+ // Act
+ var result = builder.Build();
+
+ // Assert
+ Assert.Equal(1, result.Count);
+
+ var kvp = Assert.Single(result);
+ Assert.Equal("controller", kvp.Key);
+
+ Assert.IsType<IntRouteConstraint>(kvp.Value);
+ }
+
+ [Fact]
+ public void AddConstraint_InvalidType_Throws()
+ {
+ // Arrange
+ var builder = CreateBuilder("{controller}/{action}");
+
+ // Act & Assert
+ ExceptionAssert.Throws<RouteCreationException>(
+ () => builder.AddConstraint("controller", 5),
+ "The constraint entry 'controller' - '5' on the route " +
+ "'{controller}/{action}' must have a string value or be of a type which implements '" +
+ typeof(IRouteConstraint) + "'.");
+ }
+
+ [Fact]
+ public void AddResolvedConstraint_NotFound_Throws()
+ {
+ // Arrange
+ var unresolvedConstraint = @"test";
+
+ var builder = CreateBuilder("{controller}/{action}");
+
+ // Act & Assert
+ ExceptionAssert.Throws<InvalidOperationException>(
+ () => builder.AddResolvedConstraint("controller", unresolvedConstraint),
+ @"The constraint entry 'controller' - '" + unresolvedConstraint + "' on the route " +
+ "'{controller}/{action}' could not be resolved by the constraint resolver " +
+ "of type 'DefaultInlineConstraintResolver'.");
+ }
+
+ [Fact]
+ public void AddResolvedConstraint_ForOptionalParameter()
+ {
+ var builder = CreateBuilder("{controller}/{action}/{id}");
+ builder.SetOptional("id");
+ builder.AddResolvedConstraint("id", "int");
+
+ var result = builder.Build();
+ Assert.Equal(1, result.Count);
+ Assert.Equal("id", result.First().Key);
+ Assert.IsType<OptionalRouteConstraint>(Assert.Single(result).Value);
+ }
+
+ [Fact]
+ public void AddResolvedConstraint_SetOptionalParameter_AfterAddingTheParameter()
+ {
+ var builder = CreateBuilder("{controller}/{action}/{id}");
+ builder.AddResolvedConstraint("id", "int");
+ builder.SetOptional("id");
+
+ var result = builder.Build();
+ Assert.Equal(1, result.Count);
+ Assert.Equal("id", result.First().Key);
+ Assert.IsType<OptionalRouteConstraint>(Assert.Single(result).Value);
+ }
+
+ [Fact]
+ public void AddResolvedConstraint_And_AddConstraint_ForOptionalParameter()
+ {
+ var builder = CreateBuilder("{controller}/{action}/{name}");
+ builder.SetOptional("name");
+ builder.AddResolvedConstraint("name", "alpha");
+ var minLenConstraint = new MinLengthRouteConstraint(10);
+ builder.AddConstraint("name", minLenConstraint);
+
+ var result = builder.Build();
+ Assert.Equal(1, result.Count);
+ Assert.Equal("name", result.First().Key);
+ Assert.IsType<OptionalRouteConstraint>(Assert.Single(result).Value);
+ var optionalConstraint = (OptionalRouteConstraint)result.First().Value;
+ var compositeConstraint = Assert.IsType<CompositeRouteConstraint>(optionalConstraint.InnerConstraint); ;
+ Assert.Equal(2, compositeConstraint.Constraints.Count());
+
+ Assert.Single(compositeConstraint.Constraints, c => c is MinLengthRouteConstraint);
+ Assert.Single(compositeConstraint.Constraints, c => c is AlphaRouteConstraint);
+ }
+
+ [Theory]
+ [InlineData("abc", "abc", true)] // simple case
+ [InlineData("abc", "bbb|abc", true)] // Regex or
+ [InlineData("Abc", "abc", true)] // Case insensitive
+ [InlineData("Abc ", "abc", false)] // Matches whole (but no trimming)
+ [InlineData("Abcd", "abc", false)] // Matches whole (additional non whitespace char)
+ [InlineData("Abc", " abc", false)] // Matches whole (less one char)
+ public void StringConstraintsMatchingScenarios(string routeValue,
+ string constraintValue,
+ bool shouldMatch)
+ {
+ // Arrange
+ var routeValues = new RouteValueDictionary(new { controller = routeValue });
+
+ var builder = CreateBuilder("{controller}/{action}");
+ builder.AddConstraint("controller", constraintValue);
+
+ var constraint = Assert.Single(builder.Build()).Value;
+
+ Assert.Equal(shouldMatch,
+ constraint.Match(
+ httpContext: new Mock<HttpContext>().Object,
+ route: new Mock<IRouter>().Object,
+ routeKey: "controller",
+ values: routeValues,
+ routeDirection: RouteDirection.IncomingRequest));
+ }
+
+ private static RouteConstraintBuilder CreateBuilder(string template)
+ {
+ var options = new Mock<IOptions<RouteOptions>>(MockBehavior.Strict);
+ options
+ .SetupGet(o => o.Value)
+ .Returns(new RouteOptions());
+
+ var inlineConstraintResolver = new DefaultInlineConstraintResolver(options.Object);
+ return new RouteConstraintBuilder(inlineConstraintResolver, template);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteOptionsTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteOptionsTests.cs
new file mode 100644
index 0000000000..6dc6ceb2ad
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteOptionsTests.cs
@@ -0,0 +1,49 @@
+// 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;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class RouteOptionsTests
+ {
+ [Fact]
+ public void ConfigureRouting_ConfiguresOptionsProperly()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ services.AddOptions();
+
+ // Act
+ services.AddRouting(options => options.ConstraintMap.Add("foo", typeof(TestRouteConstraint)));
+ var serviceProvider = services.BuildServiceProvider();
+
+ // Assert
+ var accessor = serviceProvider.GetRequiredService<IOptions<RouteOptions>>();
+ Assert.Equal("TestRouteConstraint", accessor.Value.ConstraintMap["foo"].Name);
+ }
+
+ private class TestRouteConstraint : IRouteConstraint
+ {
+ public TestRouteConstraint(string pattern)
+ {
+ Pattern = pattern;
+ }
+
+ public string Pattern { get; private set; }
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs
new file mode 100644
index 0000000000..a09935fcd5
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouteTest.cs
@@ -0,0 +1,1863 @@
+// 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.Linq;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Constraints;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.WebEncoders.Testing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class RouteTest
+ {
+ private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0);
+ private static IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver();
+
+ [Fact]
+ public void CreateTemplate_InlineConstraint_Regex_Malformed()
+ {
+ // Arrange
+ var template = @"{controller}/{action}/ {p1:regex(abc} ";
+ var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
+
+ var exception = Assert.Throws<RouteCreationException>(
+ () => new Route(
+ mockTarget.Object,
+ template,
+ defaults: null,
+ constraints: null,
+ dataTokens: null,
+ inlineConstraintResolver: _inlineConstraintResolver));
+
+ var expected = "An error occurred while creating the route with name '' and template" +
+ $" '{template}'.";
+ Assert.Equal(expected, exception.Message);
+
+ Assert.NotNull(exception.InnerException);
+ expected = "The constraint entry 'p1' - 'regex(abc' on the route " +
+ "'{controller}/{action}/ {p1:regex(abc} ' could not be resolved by the constraint resolver of type " +
+ $"'{nameof(DefaultInlineConstraintResolver)}'.";
+ Assert.Equal(expected, exception.InnerException.Message);
+ }
+
+ [Fact]
+ public async Task RouteAsync_MergesExistingRouteData_IfRouteMatches()
+ {
+ // Arrange
+ var template = "{controller}/{action}/{id:int}";
+
+ var context = CreateRouteContext("/Home/Index/5");
+
+ var originalRouteDataValues = context.RouteData.Values;
+ originalRouteDataValues.Add("country", "USA");
+
+ var originalDataTokens = context.RouteData.DataTokens;
+ originalDataTokens.Add("company", "Contoso");
+
+ IDictionary<string, object> routeValues = null;
+ var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
+ mockTarget
+ .Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>(ctx =>
+ {
+ routeValues = ctx.RouteData.Values;
+ ctx.Handler = NullHandler;
+ })
+ .Returns(Task.FromResult(true));
+
+ var route = new Route(
+ mockTarget.Object,
+ template,
+ defaults: null,
+ constraints: null,
+ dataTokens: new RouteValueDictionary(new { today = "Friday" }),
+ inlineConstraintResolver: _inlineConstraintResolver);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(routeValues);
+
+ Assert.True(routeValues.ContainsKey("country"));
+ Assert.Equal("USA", routeValues["country"]);
+ Assert.True(routeValues.ContainsKey("id"));
+ Assert.Equal("5", routeValues["id"]);
+
+ Assert.True(context.RouteData.Values.ContainsKey("country"));
+ Assert.Equal("USA", context.RouteData.Values["country"]);
+ Assert.True(context.RouteData.Values.ContainsKey("id"));
+ Assert.Equal("5", context.RouteData.Values["id"]);
+ Assert.Same(originalRouteDataValues, context.RouteData.Values);
+
+ Assert.Equal("Contoso", context.RouteData.DataTokens["company"]);
+ Assert.Equal("Friday", context.RouteData.DataTokens["today"]);
+ Assert.Same(originalDataTokens, context.RouteData.DataTokens);
+ }
+
+ [Fact]
+ public async Task RouteAsync_MergesExistingRouteData_PassedToConstraint()
+ {
+ // Arrange
+ var template = "{controller}/{action}/{id:int}";
+
+ var context = CreateRouteContext("/Home/Index/5");
+ var originalRouteDataValues = context.RouteData.Values;
+ originalRouteDataValues.Add("country", "USA");
+
+ var originalDataTokens = context.RouteData.DataTokens;
+ originalDataTokens.Add("company", "Contoso");
+
+ IDictionary<string, object> routeValues = null;
+ var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
+ mockTarget
+ .Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>(ctx =>
+ {
+ routeValues = ctx.RouteData.Values;
+ ctx.Handler = NullHandler;
+ })
+ .Returns(Task.FromResult(true));
+
+ var constraint = new CapturingConstraint();
+
+ var route = new Route(
+ mockTarget.Object,
+ template,
+ defaults: null,
+ constraints: new RouteValueDictionary(new { action = constraint }),
+ dataTokens: new RouteValueDictionary(new { today = "Friday" }),
+ inlineConstraintResolver: _inlineConstraintResolver);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(routeValues);
+
+ Assert.True(routeValues.ContainsKey("country"));
+ Assert.Equal("USA", routeValues["country"]);
+ Assert.True(routeValues.ContainsKey("id"));
+ Assert.Equal("5", routeValues["id"]);
+
+ Assert.True(constraint.Values.ContainsKey("country"));
+ Assert.Equal("USA", constraint.Values["country"]);
+ Assert.True(constraint.Values.ContainsKey("id"));
+ Assert.Equal("5", constraint.Values["id"]);
+
+ Assert.True(context.RouteData.Values.ContainsKey("country"));
+ Assert.Equal("USA", context.RouteData.Values["country"]);
+ Assert.True(context.RouteData.Values.ContainsKey("id"));
+ Assert.Equal("5", context.RouteData.Values["id"]);
+
+ Assert.Equal("Contoso", context.RouteData.DataTokens["company"]);
+ Assert.Equal("Friday", context.RouteData.DataTokens["today"]);
+ }
+
+ [Fact]
+ public async Task RouteAsync_InlineConstraint_OptionalParameter()
+ {
+ // Arrange
+ var template = "{controller}/{action}/{id:int?}";
+
+ var context = CreateRouteContext("/Home/Index/5");
+
+ IDictionary<string, object> routeValues = null;
+ var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
+ mockTarget
+ .Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>(ctx =>
+ {
+ routeValues = ctx.RouteData.Values;
+ ctx.Handler = NullHandler;
+ })
+ .Returns(Task.FromResult(true));
+
+ var route = new Route(
+ mockTarget.Object,
+ template,
+ defaults: null,
+ constraints: null,
+ dataTokens: null,
+ inlineConstraintResolver: _inlineConstraintResolver);
+
+ Assert.NotEmpty(route.Constraints);
+ Assert.IsType<OptionalRouteConstraint>(route.Constraints["id"]);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.True(routeValues.ContainsKey("id"));
+ Assert.Equal("5", routeValues["id"]);
+
+ Assert.True(context.RouteData.Values.ContainsKey("id"));
+ Assert.Equal("5", context.RouteData.Values["id"]);
+ }
+
+ [Fact]
+ public async Task RouteAsync_InlineConstraint_Regex()
+ {
+ // Arrange
+ var template = @"{controller}/{action}/{ssn:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}";
+
+ var context = CreateRouteContext("/Home/Index/123-456-7890");
+
+ IDictionary<string, object> routeValues = null;
+ var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
+ mockTarget
+ .Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>(ctx =>
+ {
+ routeValues = ctx.RouteData.Values;
+ ctx.Handler = NullHandler;
+ })
+ .Returns(Task.FromResult(true));
+
+ var route = new Route(
+ mockTarget.Object,
+ template,
+ defaults: null,
+ constraints: null,
+ dataTokens: null,
+ inlineConstraintResolver: _inlineConstraintResolver);
+
+ Assert.NotEmpty(route.Constraints);
+ Assert.IsType<RegexInlineRouteConstraint>(route.Constraints["ssn"]);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.True(routeValues.ContainsKey("ssn"));
+ Assert.Equal("123-456-7890", routeValues["ssn"]);
+
+ Assert.True(context.RouteData.Values.ContainsKey("ssn"));
+ Assert.Equal("123-456-7890", context.RouteData.Values["ssn"]);
+ }
+
+ [Fact]
+ public async Task RouteAsync_InlineConstraint_OptionalParameter_NotPresent()
+ {
+ // Arrange
+ var template = "{controller}/{action}/{id:int?}";
+
+ var context = CreateRouteContext("/Home/Index");
+
+ IDictionary<string, object> routeValues = null;
+ var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
+ mockTarget
+ .Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>(ctx =>
+ {
+ routeValues = ctx.RouteData.Values;
+ ctx.Handler = NullHandler;
+ })
+ .Returns(Task.FromResult(true));
+
+ var route = new Route(
+ mockTarget.Object,
+ template,
+ defaults: null,
+ constraints: null,
+ dataTokens: null,
+ inlineConstraintResolver: _inlineConstraintResolver);
+
+ Assert.NotEmpty(route.Constraints);
+ Assert.IsType<OptionalRouteConstraint>(route.Constraints["id"]);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.NotNull(routeValues);
+ Assert.False(routeValues.ContainsKey("id"));
+ Assert.False(context.RouteData.Values.ContainsKey("id"));
+ }
+
+ [Fact]
+ public async Task RouteAsync_InlineConstraint_OptionalParameter_WithInConstructorConstraint()
+ {
+ // Arrange
+ var template = "{controller}/{action}/{id:int?}";
+
+ var context = CreateRouteContext("/Home/Index/5");
+
+ IDictionary<string, object> routeValues = null;
+ var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
+ mockTarget
+ .Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>(ctx =>
+ {
+ routeValues = ctx.RouteData.Values;
+ ctx.Handler = NullHandler;
+ })
+ .Returns(Task.FromResult(true));
+
+ var constraints = new Dictionary<string, object>();
+ constraints.Add("id", new RangeRouteConstraint(1, 20));
+
+ var route = new Route(
+ mockTarget.Object,
+ template,
+ defaults: null,
+ constraints: constraints,
+ dataTokens: null,
+ inlineConstraintResolver: _inlineConstraintResolver);
+
+ Assert.NotEmpty(route.Constraints);
+ Assert.IsType<OptionalRouteConstraint>(route.Constraints["id"]);
+ var innerConstraint = ((OptionalRouteConstraint)route.Constraints["id"]).InnerConstraint;
+ Assert.IsType<CompositeRouteConstraint>(innerConstraint);
+ var compositeConstraint = (CompositeRouteConstraint)innerConstraint;
+ Assert.Equal(2, compositeConstraint.Constraints.Count<IRouteConstraint>());
+
+ Assert.Single(compositeConstraint.Constraints, c => c is IntRouteConstraint);
+ Assert.Single(compositeConstraint.Constraints, c => c is RangeRouteConstraint);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.True(routeValues.ContainsKey("id"));
+ Assert.Equal("5", routeValues["id"]);
+
+ Assert.True(context.RouteData.Values.ContainsKey("id"));
+ Assert.Equal("5", context.RouteData.Values["id"]);
+ }
+
+ [Fact]
+ public async Task RouteAsync_InlineConstraint_OptionalParameter_ConstraintFails()
+ {
+ // Arrange
+ var template = "{controller}/{action}/{id:range(1,20)?}";
+
+ var context = CreateRouteContext("/Home/Index/100");
+
+ IDictionary<string, object> routeValues = null;
+ var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
+ mockTarget
+ .Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>(ctx =>
+ {
+ routeValues = ctx.RouteData.Values;
+ ctx.Handler = NullHandler;
+ })
+ .Returns(Task.FromResult(true));
+
+ var route = new Route(
+ mockTarget.Object,
+ template,
+ defaults: null,
+ constraints: null,
+ dataTokens: null,
+ inlineConstraintResolver: _inlineConstraintResolver);
+
+ Assert.NotEmpty(route.Constraints);
+ Assert.IsType<OptionalRouteConstraint>(route.Constraints["id"]);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+ }
+
+ // PathString in HttpAbstractions guarantees a leading slash - so no value in testing other cases.
+ [Fact]
+ public async Task Match_Success_LeadingSlash()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}");
+ var context = CreateRouteContext("/Home/Index");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.Equal(2, context.RouteData.Values.Count);
+ Assert.Equal("Home", context.RouteData.Values["controller"]);
+ Assert.Equal("Index", context.RouteData.Values["action"]);
+ }
+
+ [Fact]
+ public async Task Match_Success_RootUrl()
+ {
+ // Arrange
+ var route = CreateRoute("");
+ var context = CreateRouteContext("/");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.Empty(context.RouteData.Values);
+ }
+
+ [Fact]
+ public async Task Match_Success_Defaults()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}", new { action = "Index" });
+ var context = CreateRouteContext("/Home");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.Equal(2, context.RouteData.Values.Count);
+ Assert.Equal("Home", context.RouteData.Values["controller"]);
+ Assert.Equal("Index", context.RouteData.Values["action"]);
+ }
+
+ [Fact]
+ public async Task Match_Success_CopiesDataTokens()
+ {
+ // Arrange
+ var route = CreateRoute(
+ "{controller}/{action}",
+ defaults: new { action = "Index" },
+ dataTokens: new { culture = "en-CA" });
+
+ var context = CreateRouteContext("/Home");
+
+ // Act
+ await route.RouteAsync(context);
+ Assert.NotNull(context.Handler);
+
+ // This should not affect the route - RouteData.DataTokens is a copy
+ context.RouteData.DataTokens.Add("company", "contoso");
+
+ // Assert
+ Assert.Single(route.DataTokens);
+ Assert.Single(route.DataTokens, kvp => kvp.Key == "culture" && ((string)kvp.Value) == "en-CA");
+ }
+
+ [Fact]
+ public async Task Match_Fails()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}");
+ var context = CreateRouteContext("/Home");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+ }
+
+ [Fact]
+ public async Task Match_RejectedByHandler()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}", handleRequest: false);
+ var context = CreateRouteContext("/Home");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+
+ var value = Assert.Single(context.RouteData.Values);
+ Assert.Equal("controller", value.Key);
+ Assert.Equal("Home", Assert.IsType<string>(value.Value));
+ }
+
+ [Fact]
+ public async Task Match_SetsRouters()
+ {
+ // Arrange
+ var target = CreateTarget(handleRequest: true);
+ var route = CreateRoute(target, "{controller}");
+ var context = CreateRouteContext("/Home");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.Equal(1, context.RouteData.Routers.Count);
+ Assert.Same(target, context.RouteData.Routers[0]);
+ }
+
+ [Fact]
+ public async Task Match_RouteValuesDoesntThrowOnKeyNotFound()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}");
+ var context = CreateRouteContext("/Home/Index");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.RouteData.Values["1controller"]);
+ }
+
+ [Fact]
+ public async Task Match_Success_OptionalParameter_ValueProvided()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" });
+ var context = CreateRouteContext("/Home/Create.xml");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.Equal(3, context.RouteData.Values.Count);
+ Assert.Equal("Home", context.RouteData.Values["controller"]);
+ Assert.Equal("Create", context.RouteData.Values["action"]);
+ Assert.Equal("xml", context.RouteData.Values["format"]);
+ }
+
+ [Fact]
+ public async Task Match_Success_OptionalParameter_ValueNotProvided()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" });
+ var context = CreateRouteContext("/Home/Create");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.Equal(2, context.RouteData.Values.Count);
+ Assert.Equal("Home", context.RouteData.Values["controller"]);
+ Assert.Equal("Create", context.RouteData.Values["action"]);
+ }
+
+ [Fact]
+ public async Task Match_Success_OptionalParameter_DefaultValue()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index", format = "xml" });
+ var context = CreateRouteContext("/Home/Create");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.Equal(3, context.RouteData.Values.Count);
+ Assert.Equal("Home", context.RouteData.Values["controller"]);
+ Assert.Equal("Create", context.RouteData.Values["action"]);
+ Assert.Equal("xml", context.RouteData.Values["format"]);
+ }
+
+ [Fact]
+ public async Task Match_Success_OptionalParameter_EndsWithDot()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" });
+ var context = CreateRouteContext("/Home/Create.");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+ }
+
+ private static RouteContext CreateRouteContext(string requestPath, ILoggerFactory factory = null)
+ {
+ if (factory == null)
+ {
+ factory = NullLoggerFactory.Instance;
+ }
+
+ var request = new Mock<HttpRequest>(MockBehavior.Strict);
+ request.SetupGet(r => r.Path).Returns(requestPath);
+
+ var context = new Mock<HttpContext>(MockBehavior.Strict);
+ context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory)))
+ .Returns(factory);
+ context.SetupGet(c => c.Request).Returns(request.Object);
+
+ return new RouteContext(context.Object);
+ }
+
+ [Fact]
+ public void GetVirtualPath_Success()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}");
+ var context = CreateVirtualPathContext(new { controller = "Home" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_Fail()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}");
+ var context = CreateVirtualPathContext(new { controller = "Home" });
+
+ // Act
+ var path = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Null(path);
+ }
+
+ [Fact]
+ public void GetVirtualPath_EncodesValues()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}");
+ var context = CreateVirtualPathContext(
+ new { name = "name with %special #characters" },
+ new { controller = "Home", action = "Index" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index?name=name%20with%20%25special%20%23characters", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_AlwaysUsesDefaultUrlEncoder()
+ {
+ // Arrange
+ var nameRouteValue = "name with %special #characters Jrn";
+ var expected = "/Home/Index?name=" + UrlEncoder.Default.Encode(nameRouteValue);
+ var services = new ServiceCollection();
+ services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
+ services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
+ services.AddRouting();
+ // This test encoder should not be used by Routing and should always use the default one.
+ services.AddSingleton<UrlEncoder>(new UrlTestEncoder());
+ var httpContext = new DefaultHttpContext
+ {
+ RequestServices = services.BuildServiceProvider(),
+ };
+
+ var context = new VirtualPathContext(
+ httpContext,
+ values: new RouteValueDictionary(new { name = nameRouteValue }),
+ ambientValues: new RouteValueDictionary(new { controller = "Home", action = "Index" }));
+
+ var route = CreateRoute("{controller}/{action}");
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal(expected, pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_ForListOfStrings()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}");
+ var context = CreateVirtualPathContext(
+ new { color = new List<string> { "red", "green", "blue" } },
+ new { controller = "Home", action = "Index" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index?color=red&color=green&color=blue", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_ForListOfInts()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}");
+ var context = CreateVirtualPathContext(
+ new { items = new List<int> { 10, 20, 30 } },
+ new { controller = "Home", action = "Index" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index?items=10&items=20&items=30", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_ForList_Empty()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}");
+ var context = CreateVirtualPathContext(
+ new { color = new List<string> { } },
+ new { controller = "Home", action = "Index" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_ForList_StringWorkaround()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}");
+ var context = CreateVirtualPathContext(
+ new { page = 1, color = new List<string> { "red", "green", "blue" }, message = "textfortest" },
+ new { controller = "Home", action = "Index" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index?page=1&color=red&color=green&color=blue&message=textfortest", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Theory]
+ [MemberData(nameof(DataTokensTestData))]
+ public void GetVirtualPath_ReturnsDataTokens_WhenTargetReturnsVirtualPathData(
+ RouteValueDictionary dataTokens)
+ {
+ // Arrange
+ var path = "/TestPath";
+
+ var target = new Mock<IRouter>(MockBehavior.Strict);
+ target
+ .Setup(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()))
+ .Returns(() => new VirtualPathData(target.Object, path, dataTokens));
+
+ var routeDataTokens =
+ new RouteValueDictionary() { { "ThisShouldBeIgnored", "" } };
+
+ var route = CreateRoute(
+ target.Object,
+ "{controller}",
+ defaults: null,
+ dataTokens: routeDataTokens);
+ var context = CreateVirtualPathContext(new { controller = path });
+
+ var expectedDataTokens = dataTokens ?? new RouteValueDictionary();
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Same(target.Object, pathData.Router);
+ Assert.Equal(path, pathData.VirtualPath);
+ Assert.NotNull(pathData.DataTokens);
+
+ Assert.DoesNotContain(routeDataTokens.First().Key, pathData.DataTokens.Keys);
+
+ Assert.Equal(expectedDataTokens.Count, pathData.DataTokens.Count);
+ foreach (var dataToken in expectedDataTokens)
+ {
+ Assert.True(pathData.DataTokens.ContainsKey(dataToken.Key));
+ Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(DataTokensTestData))]
+ public void GetVirtualPath_ReturnsDataTokens_WhenTargetReturnsNullVirtualPathData(
+ RouteValueDictionary dataTokens)
+ {
+ // Arrange
+ var path = "/TestPath";
+
+ var target = new Mock<IRouter>(MockBehavior.Strict);
+ target
+ .Setup(r => r.GetVirtualPath(It.IsAny<VirtualPathContext>()))
+ .Returns(() => null);
+
+ var route = CreateRoute(
+ target.Object,
+ "{controller}",
+ defaults: null,
+ dataTokens: dataTokens);
+ var context = CreateVirtualPathContext(new { controller = path });
+
+ var expectedDataTokens = dataTokens ?? new RouteValueDictionary();
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Same(route, pathData.Router);
+ Assert.Equal(path, pathData.VirtualPath);
+ Assert.NotNull(pathData.DataTokens);
+
+ Assert.Equal(expectedDataTokens.Count, pathData.DataTokens.Count);
+ foreach (var dataToken in expectedDataTokens)
+ {
+ Assert.True(pathData.DataTokens.ContainsKey(dataToken.Key));
+ Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]);
+ }
+ }
+
+ [Fact]
+ public void GetVirtualPath_ValuesRejectedByHandler_StillGeneratesPath()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}", handleRequest: false);
+ var context = CreateVirtualPathContext(new { controller = "Home" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_Success_AmbientValues()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}");
+ var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Home" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void RouteGenerationRejectsConstraints()
+ {
+ // Arrange
+ var context = CreateVirtualPathContext(new { p1 = "abcd" });
+
+ var route = CreateRoute(
+ "{p1}/{p2}",
+ new { p2 = "catchall" },
+ true,
+ new RouteValueDictionary(new { p2 = "\\d{4}" }));
+
+ // Act
+ var virtualPath = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Null(virtualPath);
+ }
+
+ [Fact]
+ public void RouteGenerationAcceptsConstraints()
+ {
+ // Arrange
+ var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" });
+
+ var route = CreateRoute(
+ "{p1}/{p2}",
+ new { p2 = "catchall" },
+ true,
+ new RouteValueDictionary(new { p2 = "\\d{4}" }));
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/hello/1234", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void RouteWithCatchAllRejectsConstraints()
+ {
+ // Arrange
+ var context = CreateVirtualPathContext(new { p1 = "abcd" });
+
+ var route = CreateRoute(
+ "{p1}/{*p2}",
+ new { p2 = "catchall" },
+ true,
+ new RouteValueDictionary(new { p2 = "\\d{4}" }));
+
+ // Act
+ var virtualPath = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Null(virtualPath);
+ }
+
+ [Fact]
+ public void RouteWithCatchAllAcceptsConstraints()
+ {
+ // Arrange
+ var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" });
+
+ var route = CreateRoute(
+ "{p1}/{*p2}",
+ new { p2 = "catchall" },
+ true,
+ new RouteValueDictionary(new { p2 = "\\d{4}" }));
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/hello/1234", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPathWithNonParameterConstraintReturnsUrlWithoutQueryString()
+ {
+ // Arrange
+ var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" });
+
+ var target = new Mock<IRouteConstraint>();
+ target
+ .Setup(
+ e => e.Match(
+ It.IsAny<HttpContext>(),
+ It.IsAny<IRouter>(),
+ It.IsAny<string>(),
+ It.IsAny<RouteValueDictionary>(),
+ It.IsAny<RouteDirection>()))
+ .Returns(true)
+ .Verifiable();
+
+ var route = CreateRoute(
+ "{p1}/{p2}",
+ new { p2 = "catchall" },
+ true,
+ new RouteValueDictionary(new { p2 = target.Object }));
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/hello/1234", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+
+ target.VerifyAll();
+ }
+
+ // Any ambient values from the current request should be visible to constraint, even
+ // if they have nothing to do with the route generating a link
+ [Fact]
+ public void GetVirtualPath_ConstraintsSeeAmbientValues()
+ {
+ // Arrange
+ var constraint = new CapturingConstraint();
+ var route = CreateRoute(
+ template: "slug/{controller}/{action}",
+ defaults: null,
+ handleRequest: true,
+ constraints: new { c = constraint });
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Store" },
+ ambientValues: new { Controller = "Home", action = "Blog", extra = "42" });
+
+ var expectedValues = new RouteValueDictionary(
+ new { controller = "Home", action = "Store", extra = "42" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/slug/Home/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+
+ Assert.Equal(expectedValues, constraint.Values);
+ }
+
+ // Non-parameter default values from the routing generating a link are not in the 'values'
+ // collection when constraints are processed.
+ [Fact]
+ public void GetVirtualPath_ConstraintsDontSeeDefaults_WhenTheyArentParameters()
+ {
+ // Arrange
+ var constraint = new CapturingConstraint();
+ var route = CreateRoute(
+ template: "slug/{controller}/{action}",
+ defaults: new { otherthing = "17" },
+ handleRequest: true,
+ constraints: new { c = constraint });
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Store" },
+ ambientValues: new { Controller = "Home", action = "Blog" });
+
+ var expectedValues = new RouteValueDictionary(
+ new { controller = "Home", action = "Store" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/slug/Home/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+
+ Assert.Equal(expectedValues, constraint.Values);
+ }
+
+ // Default values are visible to the constraint when they are used to fill a parameter.
+ [Fact]
+ public void GetVirtualPath_ConstraintsSeesDefault_WhenThereItsAParamter()
+ {
+ // Arrange
+ var constraint = new CapturingConstraint();
+ var route = CreateRoute(
+ template: "slug/{controller}/{action}",
+ defaults: new { action = "Index" },
+ handleRequest: true,
+ constraints: new { c = constraint });
+
+ var context = CreateVirtualPathContext(
+ values: new { controller = "Shopping" },
+ ambientValues: new { Controller = "Home", action = "Blog" });
+
+ var expectedValues = new RouteValueDictionary(
+ new { controller = "Shopping", action = "Index" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/slug/Shopping", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+
+ Assert.Equal(expectedValues, constraint.Values);
+ }
+
+ // Default values from the routing generating a link are in the 'values' collection when
+ // constraints are processed - IFF they are specified as values or ambient values.
+ [Fact]
+ public void GetVirtualPath_ConstraintsSeeDefaults_IfTheyAreSpecifiedOrAmbient()
+ {
+ // Arrange
+ var constraint = new CapturingConstraint();
+ var route = CreateRoute(
+ template: "slug/{controller}/{action}",
+ defaults: new { otherthing = "17", thirdthing = "13" },
+ handleRequest: true,
+ constraints: new { c = constraint });
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Store", thirdthing = "13" },
+ ambientValues: new { Controller = "Home", action = "Blog", otherthing = "17" });
+
+ var expectedValues = new RouteValueDictionary(
+ new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/slug/Home/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+
+ Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key));
+ }
+
+ [Fact]
+ public void GetVirtualPath_InlineConstraints_Success()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}/{id:int}");
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home", id = 4 });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index/4", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_InlineConstraints_NonMatchingvalue()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}/{id:int}");
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home", id = "asf" });
+
+ // Act
+ var path = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Null(path);
+ }
+
+ [Fact]
+ public void GetVirtualPath_InlineConstraints_OptionalParameter_ValuePresent()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}/{id:int?}");
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home", id = 98 });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index/98", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_InlineConstraints_OptionalParameter_ValueNotPresent()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}/{id:int?}");
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_InlineConstraints_OptionalParameter_ValuePresent_ConstraintFails()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}/{id:int?}");
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home", id = "sdfd" });
+
+ // Act
+ var path = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Null(path);
+ }
+
+ [Fact]
+ public void GetVirtualPath_InlineConstraints_CompositeInlineConstraint()
+ {
+ // Arrange
+ var route = CreateRoute("{controller}/{action}/{id:int:range(1,20)}");
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home", id = 14 });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index/14", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_InlineConstraints_CompositeConstraint_FromConstructor()
+ {
+ // Arrange
+ var constraint = new MaxLengthRouteConstraint(20);
+ var route = CreateRoute(
+ template: "{controller}/{action}/{name:alpha}",
+ defaults: null,
+ handleRequest: true,
+ constraints: new { name = constraint });
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home", name = "products" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index/products", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_OptionalParameter_ParameterPresentInValues()
+ {
+ // Arrange
+ var route = CreateRoute(
+ template: "{controller}/{action}/{name}.{format?}",
+ defaults: null,
+ handleRequest: true,
+ constraints: null);
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home", name = "products", format = "xml" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index/products.xml", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_OptionalParameter_ParameterNotPresentInValues()
+ {
+ // Arrange
+ var route = CreateRoute(
+ template: "{controller}/{action}/{name}.{format?}",
+ defaults: null,
+ handleRequest: true,
+ constraints: null);
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home", name = "products" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index/products", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_OptionalParameter_ParameterPresentInValuesAndDefaults()
+ {
+ // Arrange
+ var route = CreateRoute(
+ template: "{controller}/{action}/{name}.{format?}",
+ defaults: new { format = "json" },
+ handleRequest: true,
+ constraints: null);
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home", name = "products", format = "xml" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index/products.xml", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_OptionalParameter_ParameterNotPresentInValues_PresentInDefaults()
+ {
+ // Arrange
+ var route = CreateRoute(
+ template: "{controller}/{action}/{name}.{format?}",
+ defaults: new { format = "json" },
+ handleRequest: true,
+ constraints: null);
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home", name = "products" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index/products", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_OptionalParameter_ParameterNotPresentInTemplate_PresentInValues()
+ {
+ // Arrange
+ var route = CreateRoute(
+ template: "{controller}/{action}/{name}",
+ defaults: null,
+ handleRequest: true,
+ constraints: null);
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home", name = "products", format = "json" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index/products?format=json", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_OptionalParameter_FollowedByDotAfterSlash_ParameterPresent()
+ {
+ // Arrange
+ var route = CreateRoute(
+ template: "{controller}/{action}/.{name?}",
+ defaults: null,
+ handleRequest: true,
+ constraints: null);
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home", name = "products" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index/.products", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_OptionalParameter_FollowedByDotAfterSlash_ParameterNotPresent()
+ {
+ // Arrange
+ var route = CreateRoute(
+ template: "{controller}/{action}/.{name?}",
+ defaults: null,
+ handleRequest: true,
+ constraints: null);
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index/", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_OptionalParameter_InSimpleSegment()
+ {
+ // Arrange
+ var route = CreateRoute(
+ template: "{controller}/{action}/{name?}",
+ defaults: null,
+ handleRequest: true,
+ constraints: null);
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Index", controller = "Home" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Equal("/Home/Index", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_TwoOptionalParameters_OneValueFromAmbientValues()
+ {
+ // Arrange
+ var route = CreateRoute(
+ template: "a/{b=15}/{c?}/{d?}",
+ defaults: null,
+ handleRequest: true,
+ constraints: null);
+
+ var context = CreateVirtualPathContext(
+ values: new { },
+ ambientValues: new { c = "17" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/a/15/17", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+
+ [Fact]
+ public void GetVirtualPath_OptionalParameterAfterDefault_OneValueFromAmbientValues()
+ {
+ // Arrange
+ var route = CreateRoute(
+ template: "a/{b=15}/{c?}",
+ defaults: null,
+ handleRequest: true,
+ constraints: null);
+
+ var context = CreateVirtualPathContext(
+ values: new { },
+ ambientValues: new { c = "17" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/a/15/17", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_TwoOptionalParametersAfterDefault_OneValueFromAmbientValues()
+ {
+ // Arrange
+ var route = CreateRoute(
+ template: "a/{b=15}/{c?}/{d?}",
+ defaults: null,
+ handleRequest: true,
+ constraints: null);
+
+ var context = CreateVirtualPathContext(
+ values: new { },
+ ambientValues: new { c = "17" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/a/15/17", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void GetVirtualPath_TwoOptionalParametersAfterDefault_LastValueFromAmbientValues()
+ {
+ // Arrange
+ var route = CreateRoute(
+ template: "a/{b=15}/{c?}/{d?}",
+ defaults: null,
+ handleRequest: true,
+ constraints: null);
+
+ var context = CreateVirtualPathContext(
+ values: new { },
+ ambientValues: new { d = "17" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/a", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ private static VirtualPathContext CreateVirtualPathContext(object values)
+ {
+ return CreateVirtualPathContext(new RouteValueDictionary(values), null);
+ }
+
+ private static VirtualPathContext CreateVirtualPathContext(object values, object ambientValues)
+ {
+ return CreateVirtualPathContext(new RouteValueDictionary(values), new RouteValueDictionary(ambientValues));
+ }
+
+ private static VirtualPathContext CreateVirtualPathContext(
+ RouteValueDictionary values,
+ RouteValueDictionary ambientValues)
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
+ services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
+ services.AddRouting();
+
+ var context = new DefaultHttpContext
+ {
+ RequestServices = services.BuildServiceProvider(),
+ };
+
+ return new VirtualPathContext(context, ambientValues, values);
+ }
+
+ private static VirtualPathContext CreateVirtualPathContext(string routeName)
+ {
+ return new VirtualPathContext(null, null, null, routeName);
+ }
+
+ public static IEnumerable<object[]> DataTokens
+ {
+ get
+ {
+ yield return new object[] {
+ new Dictionary<string, object> { { "key1", "data1" }, { "key2", 13 } },
+ new Dictionary<string, object> { { "key1", "data1" }, { "key2", 13 } },
+ };
+ yield return new object[] {
+ new RouteValueDictionary { { "key1", "data1" }, { "key2", 13 } },
+ new Dictionary<string, object> { { "key1", "data1" }, { "key2", 13 } },
+ };
+ yield return new object[] {
+ new object(),
+ new Dictionary<string,object>(),
+ };
+ yield return new object[] {
+ null,
+ new Dictionary<string, object>()
+ };
+ yield return new object[] {
+ new { key1 = "data1", key2 = 13 },
+ new Dictionary<string, object> { { "key1", "data1" }, { "key2", 13 } },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(DataTokens))]
+ public void RegisteringRoute_WithDataTokens_AbleToAddTheRoute(object dataToken,
+ IDictionary<string, object> expectedDictionary)
+ {
+ // Arrange
+ var routeBuilder = CreateRouteBuilder();
+
+ // Act
+ routeBuilder.MapRoute("mockName",
+ "{controller}/{action}",
+ defaults: null,
+ constraints: null,
+ dataTokens: dataToken);
+
+ // Assert
+ var templateRoute = (Route)routeBuilder.Routes[0];
+
+ // Assert
+ Assert.Equal(expectedDictionary.Count, templateRoute.DataTokens.Count);
+ foreach (var expectedKey in expectedDictionary.Keys)
+ {
+ Assert.True(templateRoute.DataTokens.ContainsKey(expectedKey));
+ Assert.Equal(expectedDictionary[expectedKey], templateRoute.DataTokens[expectedKey]);
+ }
+ }
+
+ [Fact]
+ public void RegisteringRouteWithInvalidConstraints_Throws()
+ {
+ // Arrange
+ var routeBuilder = CreateRouteBuilder();
+
+ // Assert
+ var expectedMessage = "An error occurred while creating the route with name 'mockName' and template" +
+ " '{controller}/{action}'.";
+
+ var exception = ExceptionAssert.Throws<RouteCreationException>(
+ () => routeBuilder.MapRoute("mockName",
+ "{controller}/{action}",
+ defaults: null,
+ constraints: new { controller = "a.*", action = 17 }),
+ expectedMessage);
+
+ expectedMessage = "The constraint entry 'action' - '17' on the route '{controller}/{action}' " +
+ "must have a string value or be of a type which implements '" +
+ typeof(IRouteConstraint) + "'.";
+ Assert.NotNull(exception.InnerException);
+ Assert.Equal(expectedMessage, exception.InnerException.Message);
+ }
+
+ [Fact]
+ public void RegisteringRouteWithTwoConstraints()
+ {
+ // Arrange
+ var routeBuilder = CreateRouteBuilder();
+
+ var mockConstraint = new Mock<IRouteConstraint>().Object;
+
+ routeBuilder.MapRoute("mockName",
+ "{controller}/{action}",
+ defaults: null,
+ constraints: new { controller = "a.*", action = mockConstraint });
+
+ var constraints = ((Route)routeBuilder.Routes[0]).Constraints;
+
+ // Assert
+ Assert.Equal(2, constraints.Count);
+ Assert.IsType<RegexRouteConstraint>(constraints["controller"]);
+ Assert.Equal(mockConstraint, constraints["action"]);
+ }
+
+ [Fact]
+ public void RegisteringRouteWithOneInlineConstraintAndOneUsingConstraintArgument()
+ {
+ // Arrange
+ var routeBuilder = CreateRouteBuilder();
+
+ // Act
+ routeBuilder.MapRoute("mockName",
+ "{controller}/{action}/{id:int}",
+ defaults: null,
+ constraints: new { id = "1*" });
+
+ // Assert
+ var constraints = ((Route)routeBuilder.Routes[0]).Constraints;
+ Assert.Equal(1, constraints.Count);
+ var constraint = (CompositeRouteConstraint)constraints["id"];
+ Assert.IsType<CompositeRouteConstraint>(constraint);
+ Assert.IsType<RegexRouteConstraint>(constraint.Constraints.ElementAt(0));
+ Assert.IsType<IntRouteConstraint>(constraint.Constraints.ElementAt(1));
+ }
+
+ [Fact]
+ public void RegisteringRoute_WithOneInlineConstraint_AddsItToConstraintCollection()
+ {
+ // Arrange
+ var routeBuilder = CreateRouteBuilder();
+
+ // Act
+ routeBuilder.MapRoute("mockName",
+ "{controller}/{action}/{id:int}",
+ defaults: null,
+ constraints: null);
+
+ // Assert
+ var constraints = ((Route)routeBuilder.Routes[0]).Constraints;
+ Assert.Equal(1, constraints.Count);
+ Assert.IsType<IntRouteConstraint>(constraints["id"]);
+ }
+
+ [Fact]
+ public void RegisteringRouteWithRouteName_WithNullDefaults_AddsTheRoute()
+ {
+ // Arrange
+ var routeBuilder = CreateRouteBuilder();
+
+ routeBuilder.MapRoute(name: "RouteName", template: "{controller}/{action}", defaults: null);
+
+ // Act
+ var name = ((Route)routeBuilder.Routes[0]).Name;
+
+ // Assert
+ Assert.Equal("RouteName", name);
+ }
+
+ [Fact]
+ public void RegisteringRouteWithRouteName_WithNullDefaultsAndConstraints_AddsTheRoute()
+ {
+ // Arrange
+ var routeBuilder = CreateRouteBuilder();
+
+ routeBuilder.MapRoute(name: "RouteName",
+ template: "{controller}/{action}",
+ defaults: null,
+ constraints: null);
+
+ // Act
+ var name = ((Route)routeBuilder.Routes[0]).Name;
+
+ // Assert
+ Assert.Equal("RouteName", name);
+ }
+
+ [Theory]
+ [InlineData("///")]
+ [InlineData("/a//")]
+ [InlineData("/a/b//")]
+ [InlineData("//b//")]
+ [InlineData("///c")]
+ [InlineData("///c/")]
+ public async Task RouteAsync_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url)
+ {
+ // Arrange
+ var builder = CreateRouteBuilder();
+
+ builder.MapRoute(name: null,
+ template: "{controller?}/{action?}/{id?}",
+ defaults: null,
+ constraints: null);
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+ }
+
+ // DataTokens test data for TemplateRoute.GetVirtualPath
+ public static IEnumerable<object[]> DataTokensTestData
+ {
+ get
+ {
+ yield return new object[] { null };
+ yield return new object[] { new RouteValueDictionary() };
+ yield return new object[] { new RouteValueDictionary() { { "tokenKeyA", "tokenValueA" } } };
+ }
+ }
+
+ private static IRouteBuilder CreateRouteBuilder()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton<IInlineConstraintResolver>(_inlineConstraintResolver);
+ services.AddSingleton<RoutingMarkerService>();
+
+ var applicationBuilder = Mock.Of<IApplicationBuilder>();
+ applicationBuilder.ApplicationServices = services.BuildServiceProvider();
+
+ var routeBuilder = new RouteBuilder(applicationBuilder);
+ routeBuilder.DefaultHandler = new RouteHandler(NullHandler);
+ return routeBuilder;
+ }
+
+ private static Route CreateRoute(string routeName, string template, bool handleRequest = true)
+ {
+ return new Route(
+ CreateTarget(handleRequest),
+ routeName,
+ template,
+ defaults: null,
+ constraints: null,
+ dataTokens: null,
+ inlineConstraintResolver: _inlineConstraintResolver);
+ }
+
+ private static Route CreateRoute(string template, bool handleRequest = true)
+ {
+ return new Route(CreateTarget(handleRequest), template, _inlineConstraintResolver);
+ }
+
+ private static Route CreateRoute(
+ string template,
+ object defaults,
+ bool handleRequest = true,
+ object constraints = null,
+ object dataTokens = null)
+ {
+ return new Route(
+ CreateTarget(handleRequest),
+ template,
+ new RouteValueDictionary(defaults),
+ new RouteValueDictionary(constraints),
+ new RouteValueDictionary(dataTokens),
+ _inlineConstraintResolver);
+ }
+
+ private static Route CreateRoute(IRouter target, string template)
+ {
+ return new Route(
+ target,
+ template,
+ new RouteValueDictionary(),
+ constraints: null,
+ dataTokens: null,
+ inlineConstraintResolver: _inlineConstraintResolver);
+ }
+
+ private static Route CreateRoute(
+ IRouter target,
+ string template,
+ object defaults,
+ RouteValueDictionary dataTokens = null)
+ {
+ return new Route(
+ target,
+ template,
+ new RouteValueDictionary(defaults),
+ constraints: null,
+ dataTokens: dataTokens,
+ inlineConstraintResolver: _inlineConstraintResolver);
+ }
+
+ private static IRouter CreateTarget(bool handleRequest = true)
+ {
+ var target = new Mock<IRouter>(MockBehavior.Strict);
+ target
+ .Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
+ .Returns<VirtualPathContext>(rc => null);
+
+ target
+ .Setup(e => e.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>((c) => c.Handler = handleRequest ? NullHandler : null)
+ .Returns(Task.FromResult<object>(null));
+
+ return target.Object;
+ }
+
+ private static IInlineConstraintResolver GetInlineConstraintResolver()
+ {
+ var routeOptions = new Mock<IOptions<RouteOptions>>();
+ routeOptions
+ .SetupGet(o => o.Value)
+ .Returns(new RouteOptions());
+
+ return new DefaultInlineConstraintResolver(routeOptions.Object);
+ }
+
+ private class CapturingConstraint : IRouteConstraint
+ {
+ public IDictionary<string, object> Values { get; private set; }
+
+ public bool Match(
+ HttpContext httpContext,
+ IRouter route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+ {
+ Values = new RouteValueDictionary(values);
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouterMiddlewareTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouterMiddlewareTest.cs
new file mode 100644
index 0000000000..464a389b44
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RouterMiddlewareTest.cs
@@ -0,0 +1,106 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging.Testing;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class RouterMiddlewareTest
+ {
+ [Fact]
+ public async void Invoke_LogsCorrectValues_WhenNotHandled()
+ {
+ // Arrange
+ var expectedMessage = "Request did not match any routes.";
+ var isHandled = false;
+
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName<RouterMiddleware>,
+ TestSink.EnableWithTypeName<RouterMiddleware>);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.RequestServices = new ServiceProvider();
+
+ RequestDelegate next = (c) =>
+ {
+ return Task.FromResult<object>(null);
+ };
+
+ var router = new TestRouter(isHandled);
+ var middleware = new RouterMiddleware(next, loggerFactory, router);
+
+ // Act
+ await middleware.Invoke(httpContext);
+
+ // Assert
+ Assert.Empty(sink.Scopes);
+ var write = Assert.Single(sink.Writes);
+ Assert.Equal(expectedMessage, write.State?.ToString());
+ }
+
+ [Fact]
+ public async void Invoke_DoesNotLog_WhenHandled()
+ {
+ // Arrange
+ var isHandled = true;
+
+ var sink = new TestSink(
+ TestSink.EnableWithTypeName<RouterMiddleware>,
+ TestSink.EnableWithTypeName<RouterMiddleware>);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.RequestServices = new ServiceProvider();
+
+ RequestDelegate next = (c) =>
+ {
+ return Task.FromResult<object>(null);
+ };
+
+ var router = new TestRouter(isHandled);
+ var middleware = new RouterMiddleware(next, loggerFactory, router);
+
+ // Act
+ await middleware.Invoke(httpContext);
+
+ // Assert
+ Assert.Empty(sink.Scopes);
+ Assert.Empty(sink.Writes);
+ }
+
+ private class TestRouter : IRouter
+ {
+ private bool _isHandled;
+
+ public TestRouter(bool isHandled)
+ {
+ _isHandled = isHandled;
+ }
+
+ public VirtualPathData GetVirtualPath(VirtualPathContext context)
+ {
+ return new VirtualPathData(this, "");
+ }
+
+ public Task RouteAsync(RouteContext context)
+ {
+ context.Handler = _isHandled ? (RequestDelegate)((c) => Task.FromResult(0)) : null;
+ return Task.FromResult<object>(null);
+ }
+ }
+
+ private class ServiceProvider : IServiceProvider
+ {
+ public object GetService(Type serviceType)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RoutingBuilderExtensionsTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RoutingBuilderExtensionsTest.cs
new file mode 100644
index 0000000000..9499a6cdeb
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/RoutingBuilderExtensionsTest.cs
@@ -0,0 +1,115 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Builder.Internal;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ public class RoutingBuilderExtensionsTest
+ {
+ [Fact]
+ public void UseRouter_IRouter_ThrowsWithoutCallingAddRouting()
+ {
+ // Arrange
+ var app = new ApplicationBuilder(Mock.Of<IServiceProvider>());
+
+ // Act
+ var ex = Assert.Throws<InvalidOperationException>(() => app.UseRouter(Mock.Of<IRouter>()));
+
+ // Assert
+ Assert.Equal(
+ "Unable to find the required services. " +
+ "Please add all the required services by calling 'IServiceCollection.AddRouting' " +
+ "inside the call to 'ConfigureServices(...)' in the application startup code.",
+ ex.Message);
+ }
+
+ [Fact]
+ public void UseRouter_Action_ThrowsWithoutCallingAddRouting()
+ {
+ // Arrange
+ var app = new ApplicationBuilder(Mock.Of<IServiceProvider>());
+
+ // Act
+ var ex = Assert.Throws<InvalidOperationException>(() => app.UseRouter(b => { }));
+
+ // Assert
+ Assert.Equal(
+ "Unable to find the required services. " +
+ "Please add all the required services by calling 'IServiceCollection.AddRouting' " +
+ "inside the call to 'ConfigureServices(...)' in the application startup code.",
+ ex.Message);
+ }
+
+ [Fact]
+ public async Task UseRouter_IRouter_CallsRoute()
+ {
+ // Arrange
+ var services = CreateServices();
+
+ var app = new ApplicationBuilder(services);
+
+ var router = new Mock<IRouter>(MockBehavior.Strict);
+ router
+ .Setup(r => r.RouteAsync(It.IsAny<RouteContext>()))
+ .Returns(Task.FromResult(0))
+ .Verifiable();
+
+ app.UseRouter(router.Object);
+
+ var appFunc = app.Build();
+
+ // Act
+ await appFunc(new DefaultHttpContext());
+
+ // Assert
+ router.Verify();
+ }
+
+ [Fact]
+ public async Task UseRouter_Action_CallsRoute()
+ {
+ // Arrange
+ var services = CreateServices();
+
+ var app = new ApplicationBuilder(services);
+
+ var router = new Mock<IRouter>(MockBehavior.Strict);
+ router
+ .Setup(r => r.RouteAsync(It.IsAny<RouteContext>()))
+ .Returns(Task.FromResult(0))
+ .Verifiable();
+
+ app.UseRouter(b =>
+ {
+ b.Routes.Add(router.Object);
+ });
+
+ var appFunc = app.Build();
+
+ // Act
+ await appFunc(new DefaultHttpContext());
+
+ // Assert
+ router.Verify();
+ }
+
+ private IServiceProvider CreateServices()
+ {
+ var services = new ServiceCollection();
+
+ services.AddLogging();
+ services.AddOptions();
+ services.AddRouting();
+
+ return services.BuildServiceProvider();
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs
new file mode 100644
index 0000000000..7dd04454e2
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/RoutePrecedenceTests.cs
@@ -0,0 +1,121 @@
+// 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.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Template
+{
+ public class RoutePrecedenceTests
+ {
+ [Theory]
+ [InlineData("Employees/{id}", "Employees/{employeeId}")]
+ [InlineData("abc", "def")]
+ [InlineData("{x:alpha}", "{x:int}")]
+ public void ComputeMatched_IsEqual(string xTemplate, string yTemplate)
+ {
+ // Arrange & Act
+ var xPrededence = ComputeMatched(xTemplate);
+ var yPrededence = ComputeMatched(yTemplate);
+
+ // Assert
+ Assert.Equal(xPrededence, yPrededence);
+ }
+
+ [Theory]
+ [InlineData("Employees/{id}", "Employees/{employeeId}")]
+ [InlineData("abc", "def")]
+ [InlineData("{x:alpha}", "{x:int}")]
+ public void ComputeGenerated_IsEqual(string xTemplate, string yTemplate)
+ {
+ // Arrange & Act
+ var xPrededence = ComputeGenerated(xTemplate);
+ var yPrededence = ComputeGenerated(yTemplate);
+
+ // Assert
+ Assert.Equal(xPrededence, yPrededence);
+ }
+
+ [Theory]
+ [InlineData("abc", "a{x}")]
+ [InlineData("abc", "{x}c")]
+ [InlineData("abc", "{x:int}")]
+ [InlineData("abc", "{x}")]
+ [InlineData("abc", "{*x}")]
+ [InlineData("{x:int}", "{x}")]
+ [InlineData("{x:int}", "{*x}")]
+ [InlineData("a{x}", "{x}")]
+ [InlineData("{x}c", "{x}")]
+ [InlineData("a{x}", "{*x}")]
+ [InlineData("{x}c", "{*x}")]
+ [InlineData("{x}", "{*x}")]
+ [InlineData("{*x:maxlength(10)}", "{*x}")]
+ [InlineData("abc/def", "abc/{x:int}")]
+ [InlineData("abc/def", "abc/{x}")]
+ [InlineData("abc/def", "abc/{*x}")]
+ [InlineData("abc/{x:int}", "abc/{x}")]
+ [InlineData("abc/{x:int}", "abc/{*x}")]
+ [InlineData("abc/{x}", "abc/{*x}")]
+ [InlineData("{x}/{y:int}", "{x}/{y}")]
+ public void ComputeMatched_IsLessThan(string xTemplate, string yTemplate)
+ {
+ // Arrange & Act
+ var xPrededence = ComputeMatched(xTemplate);
+ var yPrededence = ComputeMatched(yTemplate);
+
+ // Assert
+ Assert.True(xPrededence < yPrededence);
+ }
+
+ [Theory]
+ [InlineData("abc", "a{x}")]
+ [InlineData("abc", "{x}c")]
+ [InlineData("abc", "{x:int}")]
+ [InlineData("abc", "{x}")]
+ [InlineData("abc", "{*x}")]
+ [InlineData("{x:int}", "{x}")]
+ [InlineData("{x:int}", "{*x}")]
+ [InlineData("a{x}", "{x}")]
+ [InlineData("{x}c", "{x}")]
+ [InlineData("a{x}", "{*x}")]
+ [InlineData("{x}c", "{*x}")]
+ [InlineData("{x}", "{*x}")]
+ [InlineData("{*x:maxlength(10)}", "{*x}")]
+ [InlineData("abc/def", "abc/{x:int}")]
+ [InlineData("abc/def", "abc/{x}")]
+ [InlineData("abc/def", "abc/{*x}")]
+ [InlineData("abc/{x:int}", "abc/{x}")]
+ [InlineData("abc/{x:int}", "abc/{*x}")]
+ [InlineData("abc/{x}", "abc/{*x}")]
+ [InlineData("{x}/{y:int}", "{x}/{y}")]
+ public void ComputeGenerated_IsGreaterThan(string xTemplate, string yTemplate)
+ {
+ // Arrange & Act
+ var xPrecedence = ComputeGenerated(xTemplate);
+ var yPrecedence = ComputeGenerated(yTemplate);
+
+ // Assert
+ Assert.True(xPrecedence > yPrecedence);
+ }
+
+ private static decimal ComputeMatched(string template)
+ {
+ return Compute(template, RoutePrecedence.ComputeInbound);
+ }
+ private static decimal ComputeGenerated(string template)
+ {
+ return Compute(template, RoutePrecedence.ComputeOutbound);
+ }
+
+ private static decimal Compute(string template, Func<RouteTemplate, decimal> func)
+ {
+ var options = new Mock<IOptions<RouteOptions>>();
+ options.SetupGet(o => o.Value).Returns(new RouteOptions());
+
+ var parsed = TemplateParser.Parse(template);
+ return func(parsed);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs
new file mode 100644
index 0000000000..d4dacaaa6b
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateBinderTests.cs
@@ -0,0 +1,1266 @@
+// 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.Text.Encodings.Web;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Template.Tests
+{
+ public class TemplateBinderTests
+ {
+ private readonly IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver();
+
+ public static TheoryData EmptyAndNullDefaultValues =>
+ new TheoryData<string, RouteValueDictionary, RouteValueDictionary, string>
+ {
+ {
+ "Test/{val1}/{val2}",
+ new RouteValueDictionary(new {val1 = "", val2 = ""}),
+ new RouteValueDictionary(new {val2 = "SomeVal2"}),
+ null
+ },
+ {
+ "Test/{val1}/{val2}",
+ new RouteValueDictionary(new {val1 = "", val2 = ""}),
+ new RouteValueDictionary(new {val1 = "a"}),
+ "/Test/a"
+ },
+ {
+ "Test/{val1}/{val2}/{val3}",
+ new RouteValueDictionary(new {val1 = "", val3 = ""}),
+ new RouteValueDictionary(new {val2 = "a"}),
+ null
+ },
+ {
+ "Test/{val1}/{val2}",
+ new RouteValueDictionary(new {val1 = "", val2 = ""}),
+ new RouteValueDictionary(new {val1 = "a", val2 = "b"}),
+ "/Test/a/b"
+ },
+ {
+ "Test/{val1}/{val2}/{val3}",
+ new RouteValueDictionary(new {val1 = "", val2 = "", val3 = ""}),
+ new RouteValueDictionary(new {val1 = "a", val2 = "b", val3 = "c"}),
+ "/Test/a/b/c"
+ },
+ {
+ "Test/{val1}/{val2}/{val3}",
+ new RouteValueDictionary(new {val1 = "", val2 = "", val3 = ""}),
+ new RouteValueDictionary(new {val1 = "a", val2 = "b"}),
+ "/Test/a/b"
+ },
+ {
+ "Test/{val1}/{val2}/{val3}",
+ new RouteValueDictionary(new {val1 = "", val2 = "", val3 = ""}),
+ new RouteValueDictionary(new {val1 = "a"}),
+ "/Test/a"
+ },
+ {
+ "Test/{val1}",
+ new RouteValueDictionary(new {val1 = "42", val2 = "", val3 = ""}),
+ new RouteValueDictionary(),
+ "/Test"
+ },
+ {
+ "Test/{val1}/{val2}/{val3}",
+ new RouteValueDictionary(new {val1 = "42", val2 = (string)null, val3 = (string)null}),
+ new RouteValueDictionary(),
+ "/Test"
+ },
+ {
+ "Test/{val1}/{val2}/{val3}/{val4}",
+ new RouteValueDictionary(new {val1 = "21", val2 = "", val3 = "", val4 = ""}),
+ new RouteValueDictionary(new {val1 = "42", val2 = "11", val3 = "", val4 = ""}),
+ "/Test/42/11"
+ },
+ {
+ "Test/{val1}/{val2}/{val3}",
+ new RouteValueDictionary(new {val1 = "21", val2 = "", val3 = ""}),
+ new RouteValueDictionary(new {val1 = "42"}),
+ "/Test/42"
+ },
+ {
+ "Test/{val1}/{val2}/{val3}/{val4}",
+ new RouteValueDictionary(new {val1 = "21", val2 = "", val3 = "", val4 = ""}),
+ new RouteValueDictionary(new {val1 = "42", val2 = "11"}),
+ "/Test/42/11"
+ },
+ {
+ "Test/{val1}/{val2}/{val3}",
+ new RouteValueDictionary(new {val1 = "21", val2 = (string)null, val3 = (string)null}),
+ new RouteValueDictionary(new {val1 = "42"}),
+ "/Test/42"
+ },
+ {
+ "Test/{val1}/{val2}/{val3}/{val4}",
+ new RouteValueDictionary(new {val1 = "21", val2 = (string)null, val3 = (string)null, val4 = (string)null}),
+ new RouteValueDictionary(new {val1 = "42", val2 = "11"}),
+ "/Test/42/11"
+ },
+ };
+
+ [Theory]
+ [MemberData(nameof(EmptyAndNullDefaultValues))]
+ public void Binding_WithEmptyAndNull_DefaultValues(
+ string template,
+ RouteValueDictionary defaults,
+ RouteValueDictionary values,
+ string expected)
+ {
+ // Arrange
+ var binder = new TemplateBinder(
+ UrlEncoder.Default,
+ new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
+ TemplateParser.Parse(template),
+ defaults);
+
+ // Act & Assert
+ var result = binder.GetValues(ambientValues: null, values: values);
+ if (result == null)
+ {
+ if (expected == null)
+ {
+ return;
+ }
+ else
+ {
+ Assert.NotNull(result);
+ }
+ }
+
+ var boundTemplate = binder.BindValues(result.AcceptedValues);
+ if (expected == null)
+ {
+ Assert.Null(boundTemplate);
+ }
+ else
+ {
+ Assert.NotNull(boundTemplate);
+ Assert.Equal(expected, boundTemplate);
+ }
+ }
+
+ [Fact]
+ public void GetVirtualPathWithMultiSegmentParamsOnBothEndsMatches()
+ {
+ RunTest(
+ "language/{lang}-{region}",
+ null,
+ new RouteValueDictionary(new { lang = "en", region = "US" }),
+ new RouteValueDictionary(new { lang = "xx", region = "yy" }),
+ "/language/xx-yy");
+ }
+
+ [Fact]
+ public void GetVirtualPathWithMultiSegmentParamsOnLeftEndMatches()
+ {
+ RunTest(
+ "language/{lang}-{region}a",
+ null,
+ new RouteValueDictionary(new { lang = "en", region = "US" }),
+ new RouteValueDictionary(new { lang = "xx", region = "yy" }),
+ "/language/xx-yya");
+ }
+
+ [Fact]
+ public void GetVirtualPathWithMultiSegmentParamsOnRightEndMatches()
+ {
+ RunTest(
+ "language/a{lang}-{region}",
+ null,
+ new RouteValueDictionary(new { lang = "en", region = "US" }),
+ new RouteValueDictionary(new { lang = "xx", region = "yy" }),
+ "/language/axx-yy");
+ }
+
+ public static TheoryData OptionalParamValues =>
+ new TheoryData<string, RouteValueDictionary, RouteValueDictionary, RouteValueDictionary, string>
+ {
+ // defaults
+ // ambient values
+ // values
+ {
+ "Test/{val1}/{val2}.{val3?}",
+ new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}),
+ new RouteValueDictionary(new {val3 = "someval3"}),
+ new RouteValueDictionary(new {val3 = "someval3"}),
+ "/Test/someval1/someval2.someval3"
+ },
+ {
+ "Test/{val1}/{val2}.{val3?}",
+ new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}),
+ new RouteValueDictionary(new {val3 = "someval3a"}),
+ new RouteValueDictionary(new {val3 = "someval3v"}),
+ "/Test/someval1/someval2.someval3v"
+ },
+ {
+ "Test/{val1}/{val2}.{val3?}",
+ new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}),
+ new RouteValueDictionary(new {val3 = "someval3a"}),
+ new RouteValueDictionary(),
+ "/Test/someval1/someval2.someval3a"
+ },
+ {
+ "Test/{val1}/{val2}.{val3?}",
+ new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}),
+ new RouteValueDictionary(),
+ new RouteValueDictionary(new {val3 = "someval3v"}),
+ "/Test/someval1/someval2.someval3v"
+ },
+ {
+ "Test/{val1}/{val2}.{val3?}",
+ new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2"}),
+ new RouteValueDictionary(),
+ new RouteValueDictionary(),
+ "/Test/someval1/someval2"
+ },
+ {
+ "Test/{val1}.{val2}.{val3}.{val4?}",
+ new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2" }),
+ new RouteValueDictionary(),
+ new RouteValueDictionary(new {val4 = "someval4", val3 = "someval3" }),
+ "/Test/someval1.someval2."
+ + "someval3.someval4"
+ },
+ {
+ "Test/{val1}.{val2}.{val3}.{val4?}",
+ new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2" }),
+ new RouteValueDictionary(),
+ new RouteValueDictionary(new {val3 = "someval3" }),
+ "/Test/someval1.someval2."
+ + "someval3"
+ },
+ {
+ "Test/.{val2?}",
+ new RouteValueDictionary(new { }),
+ new RouteValueDictionary(),
+ new RouteValueDictionary(new {val2 = "someval2" }),
+ "/Test/.someval2"
+ },
+ {
+ "Test/{val1}.{val2}",
+ new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2" }),
+ new RouteValueDictionary(),
+ new RouteValueDictionary(new {val3 = "someval3" }),
+ "/Test/someval1.someval2?" +
+ "val3=someval3"
+ },
+ };
+
+ [Theory]
+ [MemberData(nameof(OptionalParamValues))]
+ public void GetVirtualPathWithMultiSegmentWithOptionalParam(
+ string template,
+ RouteValueDictionary defaults,
+ RouteValueDictionary ambientValues,
+ RouteValueDictionary values,
+ string expected)
+ {
+ // Arrange
+ var binder = new TemplateBinder(
+ UrlEncoder.Default,
+ new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
+ TemplateParser.Parse(template),
+ defaults);
+
+ // Act & Assert
+ var result = binder.GetValues(ambientValues: ambientValues, values: values);
+ if (result == null)
+ {
+ if (expected == null)
+ {
+ return;
+ }
+ else
+ {
+ Assert.NotNull(result);
+ }
+ }
+
+ var boundTemplate = binder.BindValues(result.AcceptedValues);
+ if (expected == null)
+ {
+ Assert.Null(boundTemplate);
+ }
+ else
+ {
+ Assert.NotNull(boundTemplate);
+ Assert.Equal(expected, boundTemplate);
+ }
+ }
+
+ [Fact]
+ public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndMatches()
+ {
+ RunTest(
+ "language/a{lang}-{region}a",
+ null,
+ new RouteValueDictionary(new { lang = "en", region = "US" }),
+ new RouteValueDictionary(new { lang = "xx", region = "yy" }),
+ "/language/axx-yya");
+ }
+
+ [Fact]
+ public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch()
+ {
+ RunTest(
+ "language/a{lang}-{region}a",
+ null,
+ new RouteValueDictionary(new { lang = "en", region = "US" }),
+ new RouteValueDictionary(new { lang = "", region = "yy" }),
+ null);
+ }
+
+ [Fact]
+ public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch2()
+ {
+ RunTest(
+ "language/a{lang}-{region}a",
+ null,
+ new RouteValueDictionary(new { lang = "en", region = "US" }),
+ new RouteValueDictionary(new { lang = "xx", region = "" }),
+ null);
+ }
+
+ [Fact]
+ public void GetVirtualPathWithSimpleMultiSegmentParamsOnBothEndsMatches()
+ {
+ RunTest(
+ "language/{lang}",
+ null,
+ new RouteValueDictionary(new { lang = "en" }),
+ new RouteValueDictionary(new { lang = "xx" }),
+ "/language/xx");
+ }
+
+ [Fact]
+ public void GetVirtualPathWithSimpleMultiSegmentParamsOnLeftEndMatches()
+ {
+ RunTest(
+ "language/{lang}-",
+ null,
+ new RouteValueDictionary(new { lang = "en" }),
+ new RouteValueDictionary(new { lang = "xx" }),
+ "/language/xx-");
+ }
+
+ [Fact]
+ public void GetVirtualPathWithSimpleMultiSegmentParamsOnRightEndMatches()
+ {
+ RunTest(
+ "language/a{lang}",
+ null,
+ new RouteValueDictionary(new { lang = "en" }),
+ new RouteValueDictionary(new { lang = "xx" }),
+ "/language/axx");
+ }
+
+ [Fact]
+ public void GetVirtualPathWithSimpleMultiSegmentParamsOnNeitherEndMatches()
+ {
+ RunTest(
+ "language/a{lang}a",
+ null,
+ new RouteValueDictionary(new { lang = "en" }),
+ new RouteValueDictionary(new { lang = "xx" }),
+ "/language/axxa");
+ }
+
+ [Fact]
+ public void GetVirtualPathWithMultiSegmentStandardMvcRouteMatches()
+ {
+ RunTest(
+ "{controller}.mvc/{action}/{id}",
+ new RouteValueDictionary(new { action = "Index", id = (string)null }),
+ new RouteValueDictionary(new { controller = "home", action = "list", id = (string)null }),
+ new RouteValueDictionary(new { controller = "products" }),
+ "/products.mvc");
+ }
+
+ [Fact]
+ public void GetVirtualPathWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches()
+ {
+ RunTest(
+ "language/{lang}-{region}",
+ new RouteValueDictionary(new { lang = "xx", region = "yy" }),
+ new RouteValueDictionary(new { lang = "en", region = "US" }),
+ new RouteValueDictionary(new { lang = "zz" }),
+ "/language/zz-yy");
+ }
+
+ [Fact]
+ public void GetUrlWithDefaultValue()
+ {
+ // URL should be found but excluding the 'id' parameter, which has only a default value.
+ RunTest(
+ "{controller}/{action}/{id}",
+ new RouteValueDictionary(new { id = "defaultid" }),
+ new RouteValueDictionary(new { controller = "home", action = "oldaction" }),
+ new RouteValueDictionary(new { action = "newaction" }),
+ "/home/newaction");
+ }
+
+ [Fact]
+ public void GetVirtualPathWithEmptyStringRequiredValueReturnsNull()
+ {
+ RunTest(
+ "foo/{controller}",
+ null,
+ new RouteValueDictionary(new { }),
+ new RouteValueDictionary(new { controller = "" }),
+ null);
+ }
+
+ [Fact]
+ public void GetVirtualPathWithNullRequiredValueReturnsNull()
+ {
+ RunTest(
+ "foo/{controller}",
+ null,
+ new RouteValueDictionary(new { }),
+ new RouteValueDictionary(new { controller = (string)null }),
+ null);
+ }
+
+ [Fact]
+ public void GetVirtualPathWithRequiredValueReturnsPath()
+ {
+ RunTest(
+ "foo/{controller}",
+ null,
+ new RouteValueDictionary(new { }),
+ new RouteValueDictionary(new { controller = "home" }),
+ "/foo/home");
+ }
+
+ [Fact]
+ public void GetUrlWithNullDefaultValue()
+ {
+ // URL should be found but excluding the 'id' parameter, which has only a default value.
+ RunTest(
+ "{controller}/{action}/{id}",
+ new RouteValueDictionary(new { id = (string)null }),
+ new RouteValueDictionary(new { controller = "home", action = "oldaction", id = (string)null }),
+ new RouteValueDictionary(new { action = "newaction" }),
+ "/home/newaction");
+ }
+
+ [Fact]
+ public void GetVirtualPathCanFillInSeparatedParametersWithDefaultValues()
+ {
+ RunTest(
+ "{controller}/{language}-{locale}",
+ new RouteValueDictionary(new { language = "en", locale = "US" }),
+ new RouteValueDictionary(),
+ new RouteValueDictionary(new { controller = "Orders" }),
+ "/Orders/en-US");
+ }
+
+ [Fact]
+ public void GetVirtualPathWithUnusedNullValueShouldGenerateUrlAndIgnoreNullValue()
+ {
+ RunTest(
+ "{controller}.mvc/{action}/{id}",
+ new RouteValueDictionary(new { action = "Index", id = "" }),
+ new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
+ new RouteValueDictionary(new { controller = "Home", action = "TestAction", id = "1", format = (string)null }),
+ "/Home.mvc/TestAction/1");
+ }
+
+ [Fact]
+ public void GetUrlWithMissingValuesDoesntMatch()
+ {
+ RunTest(
+ "{controller}/{action}/{id}",
+ null,
+ new { controller = "home", action = "oldaction" },
+ new { action = "newaction" },
+ null);
+ }
+
+ [Fact]
+ public void GetUrlWithEmptyRequiredValuesReturnsNull()
+ {
+ RunTest(
+ "{p1}/{p2}/{p3}",
+ null,
+ new { p1 = "v1", },
+ new { p2 = "", p3 = "" },
+ null);
+ }
+
+ [Fact]
+ public void GetUrlWithEmptyOptionalValuesReturnsShortUrl()
+ {
+ RunTest(
+ "{p1}/{p2}/{p3}",
+ new { p2 = "d2", p3 = "d3" },
+ new { p1 = "v1", },
+ new { p2 = "", p3 = "" },
+ "/v1");
+ }
+
+ [Fact]
+ public void GetUrlShouldIgnoreValuesAfterChangedParameter()
+ {
+ RunTest(
+ "{controller}/{action}/{id}",
+ new { action = "Index", id = (string)null },
+ new { controller = "orig", action = "init", id = "123" },
+ new { action = "new", },
+ "/orig/new");
+ }
+
+ [Fact]
+ public void GetUrlWithNullForMiddleParameterIgnoresRemainingParameters()
+ {
+ RunTest(
+ "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}",
+ new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" },
+ new { controller = "UrlRouting", action = "Play", category = "Photos", year = "2008", occasion = "Easter", SafeParam = "SafeParamValue" },
+ new { year = (string)null, occasion = "Hola" },
+ "/UrlGeneration1/UrlRouting.mvc/Play/"
+ + "Photos/1995/Hola");
+ }
+
+ [Fact]
+ public void GetUrlWithEmptyStringForMiddleParameterIgnoresRemainingParameters()
+ {
+ var ambientValues = new RouteValueDictionary();
+ ambientValues.Add("controller", "UrlRouting");
+ ambientValues.Add("action", "Play");
+ ambientValues.Add("category", "Photos");
+ ambientValues.Add("year", "2008");
+ ambientValues.Add("occasion", "Easter");
+ ambientValues.Add("SafeParam", "SafeParamValue");
+
+ var values = new RouteValueDictionary();
+ values.Add("year", String.Empty);
+ values.Add("occasion", "Hola");
+
+ RunTest(
+ "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}",
+ new RouteValueDictionary(new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" }),
+ ambientValues,
+ values,
+ "/UrlGeneration1/UrlRouting.mvc/"
+ + "Play/Photos/1995/Hola");
+ }
+
+ [Fact]
+ public void GetUrlWithEmptyStringForMiddleParameterShouldUseDefaultValue()
+ {
+ var ambientValues = new RouteValueDictionary();
+ ambientValues.Add("Controller", "Test");
+ ambientValues.Add("Action", "Fallback");
+ ambientValues.Add("param1", "fallback1");
+ ambientValues.Add("param2", "fallback2");
+ ambientValues.Add("param3", "fallback3");
+
+ var values = new RouteValueDictionary();
+ values.Add("controller", "subtest");
+ values.Add("param1", "b");
+
+ RunTest(
+ "{controller}.mvc/{action}/{param1}",
+ new RouteValueDictionary(new { action = "Default" }),
+ ambientValues,
+ values,
+ "/subtest.mvc/Default/b");
+ }
+
+ [Fact]
+ public void GetUrlVerifyEncoding()
+ {
+ var values = new RouteValueDictionary();
+ values.Add("controller", "#;?:@&=+$,");
+ values.Add("action", "showcategory");
+ values.Add("id", 123);
+ values.Add("so?rt", "de?sc");
+ values.Add("maxPrice", 100);
+
+ RunTest(
+ "{controller}.mvc/{action}/{id}",
+ new RouteValueDictionary(new { controller = "Home" }),
+ new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }),
+ values,
+ "/%23;%3F%3A@%26%3D%2B$,.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100");
+ }
+
+ [Fact]
+ public void GetUrlGeneratesQueryStringForNewValuesAndEscapesQueryString()
+ {
+ var values = new RouteValueDictionary(new { controller = "products", action = "showcategory", id = 123, maxPrice = 100 });
+ values.Add("so?rt", "de?sc");
+
+ RunTest(
+ "{controller}.mvc/{action}/{id}",
+ new RouteValueDictionary(new { controller = "Home" }),
+ new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }),
+ values,
+ "/products.mvc/showcategory/123" +
+ "?so%3Frt=de%3Fsc&maxPrice=100");
+ }
+
+ [Fact]
+ public void GetUrlGeneratesQueryStringForNewValuesButIgnoresNewValuesThatMatchDefaults()
+ {
+ RunTest(
+ "{controller}.mvc/{action}/{id}",
+ new RouteValueDictionary(new { controller = "Home", Custom = "customValue" }),
+ new RouteValueDictionary(new { controller = "Home", action = "Index", id = (string)null }),
+ new RouteValueDictionary(
+ new
+ {
+ controller = "products",
+ action = "showcategory",
+ id = 123,
+ sort = "desc",
+ maxPrice = 100,
+ custom = "customValue"
+ }),
+ "/products.mvc/showcategory/123" +
+ "?sort=desc&maxPrice=100");
+ }
+
+ [Fact]
+ public void GetVirtualPathEncodesParametersAndLiterals()
+ {
+ RunTest(
+ "bl%og/{controller}/he llo/{action}",
+ null,
+ new RouteValueDictionary(new { controller = "ho%me", action = "li st" }),
+ new RouteValueDictionary(),
+ "/bl%25og/ho%25me/he%20llo/li%20st");
+ }
+
+ [Fact]
+ public void GetVirtualDoesNotEncodeLeadingSlashes()
+ {
+ RunTest(
+ "{controller}/{action}",
+ null,
+ new RouteValueDictionary(new { controller = "/home", action = "/my/index" }),
+ new RouteValueDictionary(),
+ "/home/%2Fmy%2Findex");
+ }
+
+ [Fact]
+ public void GetUrlWithCatchAllWithValue()
+ {
+ RunTest(
+ "{p1}/{*p2}",
+ new RouteValueDictionary(new { id = "defaultid" }),
+ new RouteValueDictionary(new { p1 = "v1" }),
+ new RouteValueDictionary(new { p2 = "v2a/v2b" }),
+ "/v1/v2a%2Fv2b");
+ }
+
+ [Fact]
+ public void GetUrlWithCatchAllWithEmptyValue()
+ {
+ RunTest(
+ "{p1}/{*p2}",
+ new RouteValueDictionary(new { id = "defaultid" }),
+ new RouteValueDictionary(new { p1 = "v1" }),
+ new RouteValueDictionary(new { p2 = "" }),
+ "/v1");
+ }
+
+ [Fact]
+ public void GetUrlWithCatchAllWithNullValue()
+ {
+ RunTest(
+ "{p1}/{*p2}",
+ new RouteValueDictionary(new { id = "defaultid" }),
+ new RouteValueDictionary(new { p1 = "v1" }),
+ new RouteValueDictionary(new { p2 = (string)null }),
+ "/v1");
+ }
+
+ [Fact]
+ public void GetUrlWithLeadingTildeSlash()
+ {
+ RunTest(
+ "~/foo",
+ null,
+ null,
+ new RouteValueDictionary(new { }),
+ "/foo");
+ }
+
+ [Fact]
+ public void GetUrlWithLeadingSlash()
+ {
+ RunTest(
+ "/foo",
+ null,
+ null,
+ new RouteValueDictionary(new { }),
+ "/foo");
+ }
+
+ [Fact]
+ public void TemplateBinder_KeepsExplicitlySuppliedRouteValues_OnFailedRouetMatch()
+ {
+ // Arrange
+ var template = "{area?}/{controller=Home}/{action=Index}/{id?}";
+ var binder = new TemplateBinder(
+ UrlEncoder.Default,
+ new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
+ TemplateParser.Parse(template),
+ defaults: null);
+ var ambientValues = new RouteValueDictionary();
+ var routeValues = new RouteValueDictionary(new { controller = "Test", action = "Index" });
+
+ // Act
+ var templateValuesResult = binder.GetValues(ambientValues, routeValues);
+ var boundTemplate = binder.BindValues(templateValuesResult.AcceptedValues);
+
+ // Assert
+ Assert.Null(boundTemplate);
+ Assert.Equal(2, templateValuesResult.CombinedValues.Count);
+ object routeValue;
+ Assert.True(templateValuesResult.CombinedValues.TryGetValue("controller", out routeValue));
+ Assert.Equal("Test", routeValue?.ToString());
+ Assert.True(templateValuesResult.CombinedValues.TryGetValue("action", out routeValue));
+ Assert.Equal("Index", routeValue?.ToString());
+ }
+
+#if ROUTE_COLLECTION
+
+ [Fact]
+ public void GetUrlShouldValidateOnlyAcceptedParametersAndUserDefaultValuesForInvalidatedParameters()
+ {
+ // Arrange
+ var rd = CreateRouteData();
+ rd.Values.Add("Controller", "UrlRouting");
+ rd.Values.Add("Name", "MissmatchedValidateParams");
+ rd.Values.Add("action", "MissmatchedValidateParameters2");
+ rd.Values.Add("ValidateParam1", "special1");
+ rd.Values.Add("ValidateParam2", "special2");
+
+ IRouteCollection rc = new DefaultRouteCollection();
+ rc.Add(CreateRoute(
+ "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}",
+ new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams", ValidateParam2 = "valid" }),
+ new RouteValueDictionary(new { ValidateParam1 = "valid.*", ValidateParam2 = "valid.*" })));
+
+ rc.Add(CreateRoute(
+ "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}",
+ new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams" }),
+ new RouteValueDictionary(new { ValidateParam1 = "special.*", ValidateParam2 = "special.*" })));
+
+ var values = CreateRouteValueDictionary();
+ values.Add("Name", "MissmatchedValidateParams");
+ values.Add("ValidateParam1", "valid1");
+
+ // Act
+ var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal<string>("/app1/UrlConstraints/Validation.mvc/Input5/MissmatchedValidateParameters2/valid1", vpd.VirtualPath);
+ }
+
+ [Fact]
+ public void GetUrlWithRouteThatHasExtensionWithSubsequentDefaultValueIncludesExtensionButNotDefaultValue()
+ {
+ // Arrange
+ var rd = CreateRouteData();
+ rd.Values.Add("controller", "Bank");
+ rd.Values.Add("action", "MakeDeposit");
+ rd.Values.Add("accountId", "7770");
+
+ IRouteCollection rc = new DefaultRouteCollection();
+ rc.Add(CreateRoute(
+ "{controller}.mvc/Deposit/{accountId}",
+ new RouteValueDictionary(new { Action = "DepositView" })));
+
+ // Note: This route was in the original bug, but it turns out that this behavior is incorrect. With the
+ // recent fix to Route (in this changelist) this route would have been selected since we have values for
+ // all three required parameters.
+ //rc.Add(new Route {
+ // Url = "{controller}.mvc/{action}/{accountId}",
+ // RouteHandler = new DummyRouteHandler()
+ //});
+
+ // This route should be chosen because the requested action is List. Since the default value of the action
+ // is List then the Action should not be in the URL. However, the file extension should be included since
+ // it is considered "safe."
+ rc.Add(CreateRoute(
+ "{controller}.mvc/{action}",
+ new RouteValueDictionary(new { Action = "List" })));
+
+ var values = CreateRouteValueDictionary();
+ values.Add("Action", "List");
+
+ // Act
+ var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal<string>("/app1/Bank.mvc", vpd.VirtualPath);
+ }
+
+ [Fact]
+ public void GetUrlWithRouteThatHasDifferentControllerCaseShouldStillMatch()
+ {
+ // Arrange
+ var rd = CreateRouteData();
+ rd.Values.Add("controller", "Bar");
+ rd.Values.Add("action", "bbb");
+ rd.Values.Add("id", null);
+
+ IRouteCollection rc = new DefaultRouteCollection();
+ rc.Add(CreateRoute("PrettyFooUrl", new RouteValueDictionary(new { controller = "Foo", action = "aaa", id = (string)null })));
+
+ rc.Add(CreateRoute("PrettyBarUrl", new RouteValueDictionary(new { controller = "Bar", action = "bbb", id = (string)null })));
+
+ rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null })));
+
+ var values = CreateRouteValueDictionary();
+ values.Add("Action", "aaa");
+ values.Add("Controller", "foo");
+
+ // Act
+ var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal<string>("/app1/PrettyFooUrl", vpd.VirtualPath);
+ }
+
+ [Fact]
+ public void GetUrlWithNoChangedValuesShouldProduceSameUrl()
+ {
+ // Arrange
+ var rd = CreateRouteData();
+ rd.Values.Add("controller", "Home");
+ rd.Values.Add("action", "Index");
+ rd.Values.Add("id", null);
+
+ IRouteCollection rc = new DefaultRouteCollection();
+ rc.Add(CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null })));
+
+ rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null })));
+
+ var values = CreateRouteValueDictionary();
+ values.Add("Action", "Index");
+
+ // Act
+ var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal<string>("/app1/Home.mvc", vpd.VirtualPath);
+ }
+
+ [Fact]
+ public void GetUrlAppliesConstraintsRulesToChooseRoute()
+ {
+ // Arrange
+ var rd = CreateRouteData();
+ rd.Values.Add("controller", "Home");
+ rd.Values.Add("action", "Index");
+ rd.Values.Add("id", null);
+
+ IRouteCollection rc = new DefaultRouteCollection();
+ rc.Add(CreateRoute(
+ "foo.mvc/{action}",
+ new RouteValueDictionary(new { controller = "Home" }),
+ new RouteValueDictionary(new { controller = "Home", action = "Contact", httpMethod = CreateHttpMethodConstraint("get") })));
+
+ rc.Add(CreateRoute(
+ "{controller}.mvc/{action}",
+ new RouteValueDictionary(new { action = "Index" }),
+ new RouteValueDictionary(new { controller = "Home", action = "(Index|About)", httpMethod = CreateHttpMethodConstraint("post") })));
+
+ var values = CreateRouteValueDictionary();
+ values.Add("Action", "Index");
+
+ // Act
+ var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal<string>("/app1/Home.mvc", vpd.VirtualPath);
+ }
+
+ [Fact]
+ public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrentRoute()
+ {
+ // Arrange
+ HttpContext context = GetHttpContext("/app", null, null);
+ IRouteCollection rt = new DefaultRouteCollection();
+ rt.Add(CreateRoute("date/{y}/{m}/{d}", null));
+ rt.Add(CreateRoute("{controller}/{action}/{id}", null));
+
+ var rd = CreateRouteData();
+ rd.Values.Add("controller", "home");
+ rd.Values.Add("action", "dostuff");
+
+ var values = CreateRouteValueDictionary();
+ values.Add("y", "2007");
+ values.Add("m", "08");
+ values.Add("d", "12");
+
+ // Act
+ var vpd = rt.GetVirtualPath(context, values);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal<string>("/app/date/2007/08/12", vpd.VirtualPath);
+ }
+
+ [Fact]
+ public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrentRouteAsSecondRoute()
+ {
+ // Arrange
+ HttpContext context = GetHttpContext("/app", null, null);
+
+ IRouteCollection rt = new DefaultRouteCollection();
+ rt.Add(CreateRoute("{controller}/{action}/{id}"));
+ rt.Add(CreateRoute("date/{y}/{m}/{d}"));
+
+ var rd = CreateRouteData();
+ rd.Values.Add("controller", "home");
+ rd.Values.Add("action", "dostuff");
+
+ var values = CreateRouteValueDictionary();
+ values.Add("y", "2007");
+ values.Add("m", "08");
+ values.Add("d", "12");
+
+ // Act
+ var vpd = rt.GetVirtualPath(context, values);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal<string>("/app/date/2007/08/12", vpd.VirtualPath);
+ }
+
+ [Fact]
+ public void GetVirtualPathUsesCurrentValuesNotInRouteToMatch()
+ {
+ // Arrange
+ HttpContext context = GetHttpContext("/app", null, null);
+ TemplateRoute r1 = CreateRoute(
+ "ParameterMatching.mvc/{Action}/{product}",
+ new RouteValueDictionary(new { Controller = "ParameterMatching", product = (string)null }),
+ null);
+
+ TemplateRoute r2 = CreateRoute(
+ "{controller}.mvc/{action}",
+ new RouteValueDictionary(new { Action = "List" }),
+ new RouteValueDictionary(new { Controller = "Action|Bank|Overridden|DerivedFromAction|OverrideInvokeActionAndExecute|InvalidControllerName|Store|HtmlHelpers|(T|t)est|UrlHelpers|Custom|Parent|Child|TempData|ViewFactory|LocatingViews|AccessingDataInViews|ViewOverrides|ViewMasterPage|InlineCompileError|CustomView" }),
+ null);
+
+ var rd = CreateRouteData();
+ rd.Values.Add("controller", "Bank");
+ rd.Values.Add("Action", "List");
+ var valuesDictionary = CreateRouteValueDictionary();
+ valuesDictionary.Add("action", "AttemptLogin");
+
+ // Act for first route
+ var vpd = r1.GetVirtualPath(context, valuesDictionary);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal<string>("ParameterMatching.mvc/AttemptLogin", vpd.VirtualPath);
+
+ // Act for second route
+ vpd = r2.GetVirtualPath(context, valuesDictionary);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal<string>("Bank.mvc/AttemptLogin", vpd.VirtualPath);
+ }
+
+#endif
+
+#if DATA_TOKENS
+ [Fact]
+ public void GetVirtualPathWithDataTokensCopiesThemFromRouteToVirtualPathData()
+ {
+ // Arrange
+ HttpContext context = GetHttpContext("/app", null, null);
+ TemplateRoute r = CreateRoute("{controller}/{action}", null, null, new RouteValueDictionary(new { foo = "bar", qux = "quux" }));
+
+ var rd = CreateRouteData();
+ rd.Values.Add("controller", "home");
+ rd.Values.Add("action", "index");
+ var valuesDictionary = CreateRouteValueDictionary();
+
+ // Act
+ var vpd = r.GetVirtualPath(context, valuesDictionary);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal<string>("home/index", vpd.VirtualPath);
+ Assert.Equal(r, vpd.Route);
+ Assert.Equal<int>(2, vpd.DataTokens.Count);
+ Assert.Equal("bar", vpd.DataTokens["foo"]);
+ Assert.Equal("quux", vpd.DataTokens["qux"]);
+ }
+#endif
+
+#if ROUTE_FORMAT_HELPER
+
+ [Fact]
+ public void UrlWithEscapedOpenCloseBraces()
+ {
+ RouteFormatHelper("foo/{{p1}}", "foo/{p1}");
+ }
+
+ [Fact]
+ public void UrlWithEscapedOpenBraceAtTheEnd()
+ {
+ RouteFormatHelper("bar{{", "bar{");
+ }
+
+ [Fact]
+ public void UrlWithEscapedOpenBraceAtTheBeginning()
+ {
+ RouteFormatHelper("{{bar", "{bar");
+ }
+
+ [Fact]
+ public void UrlWithRepeatedEscapedOpenBrace()
+ {
+ RouteFormatHelper("foo{{{{bar", "foo{{bar");
+ }
+
+ [Fact]
+ public void UrlWithEscapedCloseBraceAtTheEnd()
+ {
+ RouteFormatHelper("bar}}", "bar}");
+ }
+
+ [Fact]
+ public void UrlWithEscapedCloseBraceAtTheBeginning()
+ {
+ RouteFormatHelper("}}bar", "}bar");
+ }
+
+ [Fact]
+ public void UrlWithRepeatedEscapedCloseBrace()
+ {
+ RouteFormatHelper("foo}}}}bar", "foo}}bar");
+ }
+
+ private static void RouteFormatHelper(string routeUrl, string requestUrl)
+ {
+ var defaults = new RouteValueDictionary(new { route = "matched" });
+ var r = CreateRoute(routeUrl, defaults, null);
+
+ GetRouteDataHelper(r, requestUrl, defaults);
+ GetVirtualPathHelper(r, new RouteValueDictionary(), null, Uri.EscapeUriString(requestUrl));
+ }
+
+#endif
+
+#if CONSTRAINTS
+ [Fact]
+ public void GetVirtualPathWithNonParameterConstraintReturnsUrlWithoutQueryString()
+ {
+ // DevDiv Bugs 199612: UrlRouting: UrlGeneration should not append parameter to query string if it is a Constraint parameter and not a Url parameter
+ RunTest(
+ "{Controller}.mvc/{action}/{end}",
+ null,
+ new RouteValueDictionary(new { foo = CreateHttpMethodConstraint("GET") }),
+ new RouteValueDictionary(),
+ new RouteValueDictionary(new { controller = "Orders", action = "Index", end = "end", foo = "GET" }),
+ "Orders.mvc/Index/end");
+ }
+
+ [Fact]
+ public void GetVirtualPathWithValidCustomConstraints()
+ {
+ // Arrange
+ HttpContext context = GetHttpContext("/app", null, null);
+ CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 }));
+
+ var rd = CreateRouteData();
+ rd.Values.Add("controller", "home");
+ rd.Values.Add("action", "index");
+
+ var valuesDictionary = CreateRouteValueDictionary();
+
+ // Act
+ var vpd = r.GetVirtualPath(context, valuesDictionary);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal<string>("home/index", vpd.VirtualPath);
+ Assert.Equal(r, vpd.Route);
+ Assert.NotNull(r.ConstraintData);
+ Assert.Equal(5, r.ConstraintData.Constraint);
+ Assert.Equal("action", r.ConstraintData.ParameterName);
+ Assert.Equal("index", r.ConstraintData.ParameterValue);
+ }
+
+ [Fact]
+ public void GetVirtualPathWithInvalidCustomConstraints()
+ {
+ // Arrange
+ HttpContext context = GetHttpContext("/app", null, null);
+ CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 }));
+
+ var rd = CreateRouteData();
+ rd.Values.Add("controller", "home");
+ rd.Values.Add("action", "list");
+
+ var valuesDictionary = CreateRouteValueDictionary();
+
+ // Act
+ var vpd = r.GetVirtualPath(context, valuesDictionary);
+
+ // Assert
+ Assert.Null(vpd);
+ Assert.NotNull(r.ConstraintData);
+ Assert.Equal(5, r.ConstraintData.Constraint);
+ Assert.Equal("action", r.ConstraintData.ParameterName);
+ Assert.Equal("list", r.ConstraintData.ParameterValue);
+ }
+
+#endif
+
+ private static void RunTest(
+ string template,
+ RouteValueDictionary defaults,
+ RouteValueDictionary ambientValues,
+ RouteValueDictionary values,
+ string expected)
+ {
+ // Arrange
+ var binder = new TemplateBinder(
+ UrlEncoder.Default,
+ new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
+ TemplateParser.Parse(template),
+ defaults);
+
+ // Act & Assert
+ var result = binder.GetValues(ambientValues, values);
+ if (result == null)
+ {
+ if (expected == null)
+ {
+ return;
+ }
+ else
+ {
+ Assert.NotNull(result);
+ }
+ }
+
+ var boundTemplate = binder.BindValues(result.AcceptedValues);
+ if (expected == null)
+ {
+ Assert.Null(boundTemplate);
+ }
+ else
+ {
+ Assert.NotNull(boundTemplate);
+
+ // We want to chop off the query string and compare that using an unordered comparison
+ var expectedParts = new PathAndQuery(expected);
+ var actualParts = new PathAndQuery(boundTemplate);
+
+ Assert.Equal(expectedParts.Path, actualParts.Path);
+
+ if (expectedParts.Parameters == null)
+ {
+ Assert.Null(actualParts.Parameters);
+ }
+ else
+ {
+ Assert.Equal(expectedParts.Parameters.Count, actualParts.Parameters.Count);
+
+ foreach (var kvp in expectedParts.Parameters)
+ {
+ string value;
+ Assert.True(actualParts.Parameters.TryGetValue(kvp.Key, out value));
+ Assert.Equal(kvp.Value, value);
+ }
+ }
+ }
+ }
+
+ private static void RunTest(
+ string template,
+ object defaults,
+ object ambientValues,
+ object values,
+ string expected)
+ {
+ RunTest(
+ template,
+ new RouteValueDictionary(defaults),
+ new RouteValueDictionary(ambientValues),
+ new RouteValueDictionary(values),
+ expected);
+ }
+
+ [Theory]
+ [InlineData(null, null, true)]
+ [InlineData("blog", null, false)]
+ [InlineData(null, "store", false)]
+ [InlineData("Cool", "cool", true)]
+ [InlineData("Co0l", "cool", false)]
+ public void RoutePartsEqualTest(object left, object right, bool expected)
+ {
+ // Arrange & Act & Assert
+ if (expected)
+ {
+ Assert.True(TemplateBinder.RoutePartsEqual(left, right));
+ }
+ else
+ {
+ Assert.False(TemplateBinder.RoutePartsEqual(left, right));
+ }
+ }
+
+ private static IInlineConstraintResolver GetInlineConstraintResolver()
+ {
+ var services = new ServiceCollection().AddOptions();
+ var serviceProvider = services.BuildServiceProvider();
+ var accessor = serviceProvider.GetRequiredService<IOptions<RouteOptions>>();
+ return new DefaultInlineConstraintResolver(accessor);
+ }
+
+ private class PathAndQuery
+ {
+ public PathAndQuery(string uri)
+ {
+ var queryIndex = uri.IndexOf("?", StringComparison.Ordinal);
+ if (queryIndex == -1)
+ {
+ Path = uri;
+ }
+ else
+ {
+ Path = uri.Substring(0, queryIndex);
+
+ var query = uri.Substring(queryIndex + 1);
+ Parameters =
+ query
+ .Split(new char[] { '&' }, StringSplitOptions.None)
+ .Select(s => s.Split(new char[] { '=' }, StringSplitOptions.None))
+ .ToDictionary(pair => pair[0], pair => pair[1]);
+ }
+ }
+
+ public string Path { get; private set; }
+
+ public Dictionary<string, string> Parameters { get; private set; }
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateMatcherTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateMatcherTests.cs
new file mode 100644
index 0000000000..1e5eb7c39e
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateMatcherTests.cs
@@ -0,0 +1,1142 @@
+// 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 Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Template.Tests
+{
+ public class TemplateMatcherTests
+ {
+ private static IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver();
+
+ [Fact]
+ public void TryMatch_Success()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{controller}/{action}/{id}");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/Bank/DoAction/123", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal("Bank", values["controller"]);
+ Assert.Equal("DoAction", values["action"]);
+ Assert.Equal("123", values["id"]);
+ }
+
+ [Fact]
+ public void TryMatch_Fails()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{controller}/{action}/{id}");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/Bank/DoAction", values);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Fact]
+ public void TryMatch_WithDefaults_Success()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" });
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/Bank/DoAction", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal("Bank", values["controller"]);
+ Assert.Equal("DoAction", values["action"]);
+ Assert.Equal("default id", values["id"]);
+ }
+
+ [Fact]
+ public void TryMatch_WithDefaults_Fails()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" });
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/Bank", values);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Fact]
+ public void TryMatch_WithLiterals_Success()
+ {
+ // Arrange
+ var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" });
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/moo/111/bar/222", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal("111", values["p1"]);
+ Assert.Equal("222", values["p2"]);
+ }
+
+ [Fact]
+ public void TryMatch_RouteWithLiteralsAndDefaults_Success()
+ {
+ // Arrange
+ var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" });
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/moo/111/bar/", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal("111", values["p1"]);
+ Assert.Equal("default p2", values["p2"]);
+ }
+
+ [Theory]
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", "/123-456-7890")] // ssn
+ [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", "/asd@assds.com")] // email
+ [InlineData(@"{p1:regex(([}}])\w+)}", "/}sda")] // Not balanced }
+ [InlineData(@"{p1:regex(([{{)])\w+)}", "/})sda")] // Not balanced {
+ public void TryMatch_RegularExpressionConstraint_Valid(
+ string template,
+ string path)
+ {
+ // Arrange
+ var matcher = CreateMatcher(template);
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch(path, values);
+
+ // Assert
+ Assert.True(match);
+ }
+
+ [Theory]
+ [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", true, "foo", "bar")]
+ [InlineData("moo/{p1?}", "/moo/foo", true, "foo", null)]
+ [InlineData("moo/{p1?}", "/moo", true, null, null)]
+ [InlineData("moo/{p1}.{p2?}", "/moo/foo", true, "foo", null)]
+ [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", true, "foo.", "bar")]
+ [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", true, "foo.moo", "bar")]
+ [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", true, "foo", "bar")]
+ [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", true, "moo", "bar")]
+ [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", true, "moo", null)]
+ [InlineData("moo/.{p2?}", "/moo/.foo", true, null, "foo")]
+ [InlineData("moo/.{p2?}", "/moo", false, null, null)]
+ [InlineData("moo/{p1}.{p2?}", "/moo/....", true, "..", ".")]
+ [InlineData("moo/{p1}.{p2?}", "/moo/.bar", true, ".bar", null)]
+ public void TryMatch_OptionalParameter_FollowedByPeriod_Valid(
+ string template,
+ string path,
+ bool expectedMatch,
+ string p1,
+ string p2)
+ {
+ // Arrange
+ var matcher = CreateMatcher(template);
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch(path, values);
+
+ // Assert
+ Assert.Equal(expectedMatch, match);
+ if (p1 != null)
+ {
+ Assert.Equal(p1, values["p1"]);
+ }
+ if (p2 != null)
+ {
+ Assert.Equal(p2, values["p2"]);
+ }
+ }
+
+ [Theory]
+ [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")]
+ [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)]
+ [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")]
+ [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")]
+ [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")]
+ [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")]
+ [InlineData("{p1}/{p2}/{p3?}", "/foo/bar/baz", "foo", "bar", "baz")]
+ public void TryMatch_OptionalParameter_FollowedByPeriod_3Parameters_Valid(
+ string template,
+ string path,
+ string p1,
+ string p2,
+ string p3)
+ {
+ // Arrange
+ var matcher = CreateMatcher(template);
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch(path, values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal(p1, values["p1"]);
+
+ if (p2 != null)
+ {
+ Assert.Equal(p2, values["p2"]);
+ }
+
+ if (p3 != null)
+ {
+ Assert.Equal(p3, values["p3"]);
+ }
+ }
+
+ [Theory]
+ [InlineData("moo/{p1}.{p2?}", "/moo/foo.")]
+ [InlineData("moo/{p1}.{p2?}", "/moo/.")]
+ [InlineData("moo/{p1}.{p2}", "/foo.")]
+ [InlineData("moo/{p1}.{p2}", "/foo")]
+ [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")]
+ [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")]
+ [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")]
+ [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")]
+ [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")]
+ [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")]
+ [InlineData("moo/.{p2?}", "/moo/.")]
+ [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")]
+ public void TryMatch_OptionalParameter_FollowedByPeriod_Invalid(string template, string path)
+ {
+ // Arrange
+ var matcher = CreateMatcher(template);
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch(path, values);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Fact]
+ public void TryMatch_RouteWithOnlyLiterals_Success()
+ {
+ // Arrange
+ var matcher = CreateMatcher("moo/bar");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/moo/bar", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Empty(values);
+ }
+
+ [Fact]
+ public void TryMatch_RouteWithOnlyLiterals_Fails()
+ {
+ // Arrange
+ var matcher = CreateMatcher("moo/bars");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/moo/bar", values);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Fact]
+ public void TryMatch_RouteWithExtraSeparators_Success()
+ {
+ // Arrange
+ var matcher = CreateMatcher("moo/bar");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/moo/bar/", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Empty(values);
+ }
+
+ [Fact]
+ public void TryMatch_UrlWithExtraSeparators_Success()
+ {
+ // Arrange
+ var matcher = CreateMatcher("moo/bar/");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/moo/bar", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Empty(values);
+ }
+
+ [Fact]
+ public void TryMatch_RouteWithParametersAndExtraSeparators_Success()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{p1}/{p2}/");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/moo/bar", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal("moo", values["p1"]);
+ Assert.Equal("bar", values["p2"]);
+ }
+
+ [Fact]
+ public void TryMatch_RouteWithDifferentLiterals_Fails()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{p1}/{p2}/baz");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/moo/bar/boo", values);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Fact]
+ public void TryMatch_LongerUrl_Fails()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{p1}");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/moo/bar", values);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Fact]
+ public void TryMatch_SimpleFilename_Success()
+ {
+ // Arrange
+ var matcher = CreateMatcher("DEFAULT.ASPX");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/default.aspx", values);
+
+ // Assert
+ Assert.True(match);
+ }
+
+ [Theory]
+ [InlineData("{prefix}x{suffix}", "/xxxxxxxxxx")]
+ [InlineData("{prefix}xyz{suffix}", "/xxxxyzxyzxxxxxxyz")]
+ [InlineData("{prefix}xyz{suffix}", "/abcxxxxyzxyzxxxxxxyzxx")]
+ [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz")]
+ [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz1")]
+ [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyz")]
+ [InlineData("{prefix}aa{suffix}", "/aaaaa")]
+ [InlineData("{prefix}aaa{suffix}", "/aaaaa")]
+ public void TryMatch_RouteWithComplexSegment_Success(string template, string path)
+ {
+ var matcher = CreateMatcher(template);
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch(path, values);
+
+ // Assert
+ Assert.True(match);
+ }
+
+ [Fact]
+ public void TryMatch_RouteWithExtraDefaultValues_Success()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{p1}/{p2}", new { p2 = (string)null, foo = "bar" });
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/v1", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal<int>(3, values.Count);
+ Assert.Equal("v1", values["p1"]);
+ Assert.Null(values["p2"]);
+ Assert.Equal("bar", values["foo"]);
+ }
+
+ [Fact]
+ public void TryMatch_PrettyRouteWithExtraDefaultValues_Success()
+ {
+ // Arrange
+ var matcher = CreateMatcher(
+ "date/{y}/{m}/{d}",
+ new { controller = "blog", action = "showpost", m = (string)null, d = (string)null });
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/date/2007/08", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal<int>(5, values.Count);
+ Assert.Equal("blog", values["controller"]);
+ Assert.Equal("showpost", values["action"]);
+ Assert.Equal("2007", values["y"]);
+ Assert.Equal("08", values["m"]);
+ Assert.Null(values["d"]);
+ }
+
+ [Fact]
+ public void TryMatch_WithMultiSegmentParamsOnBothEndsMatches()
+ {
+ RunTest(
+ "language/{lang}-{region}",
+ "/language/en-US",
+ null,
+ new RouteValueDictionary(new { lang = "en", region = "US" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithMultiSegmentParamsOnLeftEndMatches()
+ {
+ RunTest(
+ "language/{lang}-{region}a",
+ "/language/en-USa",
+ null,
+ new RouteValueDictionary(new { lang = "en", region = "US" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithMultiSegmentParamsOnRightEndMatches()
+ {
+ RunTest(
+ "language/a{lang}-{region}",
+ "/language/aen-US",
+ null,
+ new RouteValueDictionary(new { lang = "en", region = "US" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithMultiSegmentParamsOnNeitherEndMatches()
+ {
+ RunTest(
+ "language/a{lang}-{region}a",
+ "/language/aen-USa",
+ null,
+ new RouteValueDictionary(new { lang = "en", region = "US" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithMultiSegmentParamsOnNeitherEndDoesNotMatch()
+ {
+ RunTest(
+ "language/a{lang}-{region}a",
+ "/language/a-USa",
+ null,
+ null);
+ }
+
+ [Fact]
+ public void TryMatch_WithMultiSegmentParamsOnNeitherEndDoesNotMatch2()
+ {
+ RunTest(
+ "language/a{lang}-{region}a",
+ "/language/aen-a",
+ null,
+ null);
+ }
+
+ [Fact]
+ public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsMatches()
+ {
+ RunTest(
+ "language/{lang}",
+ "/language/en",
+ null,
+ new RouteValueDictionary(new { lang = "en" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsTrailingSlashDoesNotMatch()
+ {
+ RunTest(
+ "language/{lang}",
+ "/language/",
+ null,
+ null);
+ }
+
+ [Fact]
+ public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsDoesNotMatch()
+ {
+ RunTest(
+ "language/{lang}",
+ "/language",
+ null,
+ null);
+ }
+
+ [Fact]
+ public void TryMatch_WithSimpleMultiSegmentParamsOnLeftEndMatches()
+ {
+ RunTest(
+ "language/{lang}-",
+ "/language/en-",
+ null,
+ new RouteValueDictionary(new { lang = "en" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithSimpleMultiSegmentParamsOnRightEndMatches()
+ {
+ RunTest(
+ "language/a{lang}",
+ "/language/aen",
+ null,
+ new RouteValueDictionary(new { lang = "en" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithSimpleMultiSegmentParamsOnNeitherEndMatches()
+ {
+ RunTest(
+ "language/a{lang}a",
+ "/language/aena",
+ null,
+ new RouteValueDictionary(new { lang = "en" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithMultiSegmentStandamatchMvcRouteMatches()
+ {
+ RunTest(
+ "{controller}.mvc/{action}/{id}",
+ "/home.mvc/index",
+ new RouteValueDictionary(new { action = "Index", id = (string)null }),
+ new RouteValueDictionary(new { controller = "home", action = "index", id = (string)null }));
+ }
+
+ [Fact]
+ public void TryMatch_WithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches()
+ {
+ RunTest(
+ "language/{lang}-{region}",
+ "/language/-",
+ new RouteValueDictionary(new { lang = "xx", region = "yy" }),
+ null);
+ }
+
+ [Fact]
+ public void TryMatch_WithUrlWithMultiSegmentWithRepeatedDots()
+ {
+ RunTest(
+ "{Controller}..mvc/{id}/{Param1}",
+ "/Home..mvc/123/p1",
+ null,
+ new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithUrlWithTwoRepeatedDots()
+ {
+ RunTest(
+ "{Controller}.mvc/../{action}",
+ "/Home.mvc/../index",
+ null,
+ new RouteValueDictionary(new { Controller = "Home", action = "index" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithUrlWithThreeRepeatedDots()
+ {
+ RunTest(
+ "{Controller}.mvc/.../{action}",
+ "/Home.mvc/.../index",
+ null,
+ new RouteValueDictionary(new { Controller = "Home", action = "index" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithUrlWithManyRepeatedDots()
+ {
+ RunTest(
+ "{Controller}.mvc/../../../{action}",
+ "/Home.mvc/../../../index",
+ null,
+ new RouteValueDictionary(new { Controller = "Home", action = "index" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithUrlWithExclamationPoint()
+ {
+ RunTest(
+ "{Controller}.mvc!/{action}",
+ "/Home.mvc!/index",
+ null,
+ new RouteValueDictionary(new { Controller = "Home", action = "index" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithUrlWithStartingDotDotSlash()
+ {
+ RunTest(
+ "../{Controller}.mvc",
+ "/../Home.mvc",
+ null,
+ new RouteValueDictionary(new { Controller = "Home" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithUrlWithStartingBackslash()
+ {
+ RunTest(
+ @"\{Controller}.mvc",
+ @"/\Home.mvc",
+ null,
+ new RouteValueDictionary(new { Controller = "Home" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithUrlWithBackslashSeparators()
+ {
+ RunTest(
+ @"{Controller}.mvc\{id}\{Param1}",
+ @"/Home.mvc\123\p1",
+ null,
+ new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" }));
+ }
+
+ [Fact]
+ public void TryMatch_WithUrlWithParenthesesLiterals()
+ {
+ RunTest(
+ @"(Controller).mvc",
+ @"/(Controller).mvc",
+ null,
+ new RouteValueDictionary());
+ }
+
+ [Fact]
+ public void TryMatch_WithUrlWithTrailingSlashSpace()
+ {
+ RunTest(
+ @"Controller.mvc/ ",
+ @"/Controller.mvc/ ",
+ null,
+ new RouteValueDictionary());
+ }
+
+ [Fact]
+ public void TryMatch_WithUrlWithTrailingSpace()
+ {
+ RunTest(
+ @"Controller.mvc ",
+ @"/Controller.mvc ",
+ null,
+ new RouteValueDictionary());
+ }
+
+ [Fact]
+ public void TryMatch_WithCatchAllCapturesDots()
+ {
+ // DevDiv Bugs 189892: UrlRouting: Catch all parameter cannot capture url segments that contain the "."
+ RunTest(
+ "Home/ShowPilot/{missionId}/{*name}",
+ "/Home/ShowPilot/777/12345./foobar",
+ new RouteValueDictionary(new
+ {
+ controller = "Home",
+ action = "ShowPilot",
+ missionId = (string)null,
+ name = (string)null
+ }),
+ new RouteValueDictionary(new { controller = "Home", action = "ShowPilot", missionId = "777", name = "12345./foobar" }));
+ }
+
+ [Fact]
+ public void TryMatch_RouteWithCatchAll_MatchesMultiplePathSegments()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{p1}/{*p2}");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/v1/v2/v3", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal<int>(2, values.Count);
+ Assert.Equal("v1", values["p1"]);
+ Assert.Equal("v2/v3", values["p2"]);
+ }
+
+ [Fact]
+ public void TryMatch_RouteWithCatchAll_MatchesTrailingSlash()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{p1}/{*p2}");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/v1/", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal<int>(2, values.Count);
+ Assert.Equal("v1", values["p1"]);
+ Assert.Null(values["p2"]);
+ }
+
+ [Fact]
+ public void TryMatch_RouteWithCatchAll_MatchesEmptyContent()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{p1}/{*p2}");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch("/v1", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal<int>(2, values.Count);
+ Assert.Equal("v1", values["p1"]);
+ Assert.Null(values["p2"]);
+ }
+
+ [Fact]
+ public void TryMatch_RouteWithCatchAll_MatchesEmptyContent_DoesNotReplaceExistingRouteValue()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{p1}/{*p2}");
+
+ var values = new RouteValueDictionary(new { p2 = "hello" });
+
+ // Act
+ var match = matcher.TryMatch("/v1", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal<int>(2, values.Count);
+ Assert.Equal("v1", values["p1"]);
+ Assert.Equal("hello", values["p2"]);
+ }
+
+ [Fact]
+ public void TryMatch_RouteWithCatchAll_UsesDefaultValueForEmptyContent()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" });
+
+ var values = new RouteValueDictionary(new { p2 = "overridden" });
+
+ // Act
+ var match = matcher.TryMatch("/v1", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal<int>(2, values.Count);
+ Assert.Equal("v1", values["p1"]);
+ Assert.Equal("catchall", values["p2"]);
+ }
+
+ [Fact]
+ public void TryMatch_RouteWithCatchAll_IgnoresDefaultValueForNonEmptyContent()
+ {
+ // Arrange
+ var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" });
+
+ var values = new RouteValueDictionary(new { p2 = "overridden" });
+
+ // Act
+ var match = matcher.TryMatch("/v1/hello/whatever", values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal<int>(2, values.Count);
+ Assert.Equal("v1", values["p1"]);
+ Assert.Equal("hello/whatever", values["p2"]);
+ }
+
+ [Fact]
+ public void TryMatch_DoesNotMatchOnlyLeftLiteralMatch()
+ {
+ // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url
+ RunTest(
+ "foo",
+ "/fooBAR",
+ null,
+ null);
+ }
+
+ [Fact]
+ public void TryMatch_DoesNotMatchOnlyRightLiteralMatch()
+ {
+ // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url
+ RunTest(
+ "foo",
+ "/BARfoo",
+ null,
+ null);
+ }
+
+ [Fact]
+ public void TryMatch_DoesNotMatchMiddleLiteralMatch()
+ {
+ // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url
+ RunTest(
+ "foo",
+ "/BARfooBAR",
+ null,
+ null);
+ }
+
+ [Fact]
+ public void TryMatch_DoesMatchesExactLiteralMatch()
+ {
+ // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url
+ RunTest(
+ "foo",
+ "/foo",
+ null,
+ new RouteValueDictionary());
+ }
+
+ [Fact]
+ public void TryMatch_WithWeimatchParameterNames()
+ {
+ RunTest(
+ "foo/{ }/{.!$%}/{dynamic.data}/{op.tional}",
+ "/foo/space/weimatch/omatcherid",
+ new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } },
+ new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weimatch" }, { "dynamic.data", "omatcherid" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } });
+ }
+
+ [Fact]
+ public void TryMatch_DoesNotMatchRouteWithLiteralSeparatomatchefaultsButNoValue()
+ {
+ RunTest(
+ "{controller}/{language}-{locale}",
+ "/foo",
+ new RouteValueDictionary(new { language = "en", locale = "US" }),
+ null);
+ }
+
+ [Fact]
+ public void TryMatch_DoesNotMatchesRouteWithLiteralSeparatomatchefaultsAndLeftValue()
+ {
+ RunTest(
+ "{controller}/{language}-{locale}",
+ "/foo/xx-",
+ new RouteValueDictionary(new { language = "en", locale = "US" }),
+ null);
+ }
+
+ [Fact]
+ public void TryMatch_DoesNotMatchesRouteWithLiteralSeparatomatchefaultsAndRightValue()
+ {
+ RunTest(
+ "{controller}/{language}-{locale}",
+ "/foo/-yy",
+ new RouteValueDictionary(new { language = "en", locale = "US" }),
+ null);
+ }
+
+ [Fact]
+ public void TryMatch_MatchesRouteWithLiteralSeparatomatchefaultsAndValue()
+ {
+ RunTest(
+ "{controller}/{language}-{locale}",
+ "/foo/xx-yy",
+ new RouteValueDictionary(new { language = "en", locale = "US" }),
+ new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } });
+ }
+
+ [Fact]
+ public void TryMatch_SetsOptionalParameter()
+ {
+ // Arrange
+ var route = CreateMatcher("{controller}/{action?}");
+ var url = "/Home/Index";
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = route.TryMatch(url, values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal(2, values.Count);
+ Assert.Equal("Home", values["controller"]);
+ Assert.Equal("Index", values["action"]);
+ }
+
+ [Fact]
+ public void TryMatch_DoesNotSetOptionalParameter()
+ {
+ // Arrange
+ var route = CreateMatcher("{controller}/{action?}");
+ var url = "/Home";
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = route.TryMatch(url, values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Single(values);
+ Assert.Equal("Home", values["controller"]);
+ Assert.False(values.ContainsKey("action"));
+ }
+
+ [Fact]
+ public void TryMatch_DoesNotSetOptionalParameter_EmptyString()
+ {
+ // Arrange
+ var route = CreateMatcher("{controller?}");
+ var url = "";
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = route.TryMatch(url, values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Empty(values);
+ Assert.False(values.ContainsKey("controller"));
+ }
+
+ [Fact]
+ public void TryMatch__EmptyRouteWith_EmptyString()
+ {
+ // Arrange
+ var route = CreateMatcher("");
+ var url = "";
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = route.TryMatch(url, values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Empty(values);
+ }
+
+ [Fact]
+ public void TryMatch_MultipleOptionalParameters()
+ {
+ // Arrange
+ var route = CreateMatcher("{controller}/{action?}/{id?}");
+ var url = "/Home/Index";
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = route.TryMatch(url, values);
+
+ // Assert
+ Assert.True(match);
+ Assert.Equal(2, values.Count);
+ Assert.Equal("Home", values["controller"]);
+ Assert.Equal("Index", values["action"]);
+ Assert.False(values.ContainsKey("id"));
+ }
+
+ [Theory]
+ [InlineData("///")]
+ [InlineData("/a//")]
+ [InlineData("/a/b//")]
+ [InlineData("//b//")]
+ [InlineData("///c")]
+ [InlineData("///c/")]
+ public void TryMatch_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url)
+ {
+ // Arrange
+ var route = CreateMatcher("{controller?}/{action?}/{id?}");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = route.TryMatch(url, values);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("/")]
+ [InlineData("/a")]
+ [InlineData("/a/")]
+ [InlineData("/a/b")]
+ [InlineData("/a/b/")]
+ [InlineData("/a/b/c")]
+ [InlineData("/a/b/c/")]
+ public void TryMatch_MultipleOptionalParameters_WithIncrementalOptionalValues(string url)
+ {
+ // Arrange
+ var route = CreateMatcher("{controller?}/{action?}/{id?}");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = route.TryMatch(url, values);
+
+ // Assert
+ Assert.True(match);
+ }
+
+ [Theory]
+ [InlineData("///")]
+ [InlineData("////")]
+ [InlineData("/a//")]
+ [InlineData("/a///")]
+ [InlineData("//b/")]
+ [InlineData("//b//")]
+ [InlineData("///c")]
+ [InlineData("///c/")]
+ public void TryMatch_MultipleParameters_WithEmptyValues(string url)
+ {
+ // Arrange
+ var route = CreateMatcher("{controller}/{action}/{id}");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = route.TryMatch(url, values);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ [Theory]
+ [InlineData("/a/b/c//")]
+ [InlineData("/a/b/c/////")]
+ public void TryMatch_CatchAllParameters_WithEmptyValuesAtTheEnd(string url)
+ {
+ // Arrange
+ var route = CreateMatcher("{controller}/{action}/{*id}");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = route.TryMatch(url, values);
+
+ // Assert
+ Assert.True(match);
+ }
+
+ [Theory]
+ [InlineData("/a/b//")]
+ [InlineData("/a/b///c")]
+ public void TryMatch_CatchAllParameters_WithEmptyValues(string url)
+ {
+ // Arrange
+ var route = CreateMatcher("{controller}/{action}/{*id}");
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = route.TryMatch(url, values);
+
+ // Assert
+ Assert.False(match);
+ }
+
+ private TemplateMatcher CreateMatcher(string template, object defaults = null)
+ {
+ return new TemplateMatcher(
+ TemplateParser.Parse(template),
+ new RouteValueDictionary(defaults));
+ }
+
+ private static void RunTest(
+ string template,
+ string path,
+ RouteValueDictionary defaults,
+ IDictionary<string, object> expected)
+ {
+ // Arrange
+ var matcher = new TemplateMatcher(
+ TemplateParser.Parse(template),
+ defaults ?? new RouteValueDictionary());
+
+ var values = new RouteValueDictionary();
+
+ // Act
+ var match = matcher.TryMatch(new PathString(path), values);
+
+ // Assert
+ if (expected == null)
+ {
+ Assert.False(match);
+ }
+ else
+ {
+ Assert.True(match);
+ Assert.Equal(expected.Count, values.Count);
+ foreach (string key in values.Keys)
+ {
+ Assert.Equal(expected[key], values[key]);
+ }
+ }
+ }
+
+ private static IInlineConstraintResolver GetInlineConstraintResolver()
+ {
+ var services = new ServiceCollection().AddOptions();
+ var serviceProvider = services.BuildServiceProvider();
+ var accessor = serviceProvider.GetRequiredService<IOptions<RouteOptions>>();
+ return new DefaultInlineConstraintResolver(accessor);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs
new file mode 100644
index 0000000000..7f9f9b8b40
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs
@@ -0,0 +1,922 @@
+// 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.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Template.Tests
+{
+ public class TemplateRouteParserTests
+ {
+ [Fact]
+ public void Parse_SingleLiteral()
+ {
+ // Arrange
+ var template = "cool";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool"));
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_SingleParameter()
+ {
+ // Arrange
+ var template = "{p}";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(
+ TemplatePart.CreateParameter("p", false, false, defaultValue: null, inlineConstraints: null));
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_OptionalParameter()
+ {
+ // Arrange
+ var template = "{p?}";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(
+ TemplatePart.CreateParameter("p", false, true, defaultValue: null, inlineConstraints: null));
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_MultipleLiterals()
+ {
+ // Arrange
+ var template = "cool/awesome/super";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool"));
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("awesome"));
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[2].Parts.Add(TemplatePart.CreateLiteral("super"));
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_MultipleParameters()
+ {
+ // Arrange
+ var template = "{p1}/{p2}/{*p3}";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Parameters.Add(expected.Segments[1].Parts[0]);
+
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3",
+ true,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Parameters.Add(expected.Segments[2].Parts[0]);
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_ComplexSegment_LP()
+ {
+ // Arrange
+ var template = "cool-{p1}";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Parameters.Add(expected.Segments[0].Parts[1]);
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_ComplexSegment_PL()
+ {
+ // Arrange
+ var template = "{p1}-cool";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_ComplexSegment_PLP()
+ {
+ // Arrange
+ var template = "{p1}-cool-{p2}";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Parameters.Add(expected.Segments[0].Parts[2]);
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_ComplexSegment_LPL()
+ {
+ // Arrange
+ var template = "cool-{p1}-awesome";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-"));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Parameters.Add(expected.Segments[0].Parts[1]);
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("-awesome"));
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_ComplexSegment_OptionalParameterFollowingPeriod()
+ {
+ // Arrange
+ var template = "{p1}.{p2?}";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
+ false,
+ true,
+ defaultValue: null,
+ inlineConstraints: null));
+
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+ expected.Parameters.Add(expected.Segments[0].Parts[2]);
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_ComplexSegment_ParametersFollowingPeriod()
+ {
+ // Arrange
+ var template = "{p1}.{p2}";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+ expected.Parameters.Add(expected.Segments[0].Parts[2]);
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_ThreeParameters()
+ {
+ // Arrange
+ var template = "{p1}.{p2}.{p3?}";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3",
+ false,
+ true,
+ defaultValue: null,
+ inlineConstraints: null));
+
+
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+ expected.Parameters.Add(expected.Segments[0].Parts[2]);
+ expected.Parameters.Add(expected.Segments[0].Parts[4]);
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_ComplexSegment_ThreeParametersSeperatedByPeriod()
+ {
+ // Arrange
+ var template = "{p1}.{p2}.{p3}";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+
+
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+ expected.Parameters.Add(expected.Segments[0].Parts[2]);
+ expected.Parameters.Add(expected.Segments[0].Parts[4]);
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_MiddleSegment()
+ {
+ // Arrange
+ var template = "{p1}.{p2?}/{p3}";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("."));
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
+ false,
+ true,
+ defaultValue: null,
+ inlineConstraints: null));
+
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+ expected.Parameters.Add(expected.Segments[0].Parts[2]);
+
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3",
+ false,
+ false,
+ null,
+ null));
+ expected.Parameters.Add(expected.Segments[1].Parts[0]);
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_LastSegment()
+ {
+ // Arrange
+ var template = "{p1}/{p2}.{p3?}";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+ expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("."));
+ expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3",
+ false,
+ true,
+ null,
+ null));
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+ expected.Parameters.Add(expected.Segments[1].Parts[0]);
+ expected.Parameters.Add(expected.Segments[1].Parts[2]);
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Fact]
+ public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_PeriodAfterSlash()
+ {
+ // Arrange
+ var template = "{p2}/.{p3?}";
+
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: null));
+
+ expected.Segments.Add(new TemplateSegment());
+ expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("."));
+ expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3",
+ false,
+ true,
+ null,
+ null));
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+ expected.Parameters.Add(expected.Segments[1].Parts[1]);
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Theory]
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", @"regex(^\d{3}-\d{3}-\d{4}$)")] // ssn
+ [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)}", @"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")] // date
+ [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", @"regex(^\w+\@\w+\.\w+)")] // email
+ [InlineData(@"{p1:regex(([}}])\w+)}", @"regex(([}])\w+)")] // Not balanced }
+ [InlineData(@"{p1:regex(([{{(])\w+)}", @"regex(([{(])\w+)")] // Not balanced {
+ public void Parse_RegularExpressions(string template, string constraint)
+ {
+ // Arrange
+ var expected = new RouteTemplate(template, new List<TemplateSegment>());
+ expected.Segments.Add(new TemplateSegment());
+ var c = new InlineConstraint(constraint);
+ expected.Segments[0].Parts.Add(
+ TemplatePart.CreateParameter("p1",
+ false,
+ false,
+ defaultValue: null,
+ inlineConstraints: new List<InlineConstraint> { c }));
+ expected.Parameters.Add(expected.Segments[0].Parts[0]);
+
+ // Act
+ var actual = TemplateParser.Parse(template);
+
+ // Assert
+ Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
+ }
+
+ [Theory]
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}}$)}")] // extra }
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}}")] // extra } at the end
+ [InlineData(@"{{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}")] // extra { at the begining
+ [InlineData(@"{p1:regex(([}])\w+}")] // Not escaped }
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}$)}")] // Not escaped }
+ [InlineData(@"{p1:regex(abc)")]
+ public void Parse_RegularExpressions_Invalid(string template)
+ {
+ // Act and Assert
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse(template),
+ "There is an incomplete parameter in the route template. Check that each '{' character has a matching " +
+ "'}' character." + Environment.NewLine + "Parameter name: routeTemplate");
+ }
+
+ [Theory]
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{{4}}$)}")] // extra {
+ [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{4}}$)}")] // Not escaped {
+ public void Parse_RegularExpressions_Unescaped(string template)
+ {
+ // Act and Assert
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse(template),
+ "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Theory]
+ [InlineData("{p1}.{p2?}.{p3}", "p2", ".")]
+ [InlineData("{p1?}{p2}", "p1", "p2")]
+ [InlineData("{p1?}{p2?}", "p1", "p2")]
+ [InlineData("{p1}.{p2?})", "p2", ")")]
+ [InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")]
+ public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart(
+ string template,
+ string parameter,
+ string invalid)
+ {
+ // Act and Assert
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse(template),
+ "An optional parameter must be at the end of the segment. In the segment '" + template +
+ "', optional parameter '" + parameter + "' is followed by '" + invalid + "'."
+ + Environment.NewLine + "Parameter name: routeTemplate");
+ }
+
+ [Theory]
+ [InlineData("{p1}-{p2?}", "-")]
+ [InlineData("{p1}..{p2?}", "..")]
+ [InlineData("..{p2?}", "..")]
+ [InlineData("{p1}.abc.{p2?}", ".abc.")]
+ [InlineData("{p1}{p2?}", "{p1}")]
+ public void Parse_ComplexSegment_OptionalParametersSeperatedByPeriod_Invalid(string template, string parameter)
+ {
+ // Act and Assert
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse(template),
+ "In the segment '"+ template +"', the optional parameter 'p2' is preceded by an invalid " +
+ "segment '" + parameter +"'. Only a period (.) can precede an optional parameter." +
+ Environment.NewLine + "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_WithRepeatedParameter()
+ {
+ var ex = ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"),
+ "The route parameter name 'controller' appears more than one time in the route template." +
+ Environment.NewLine + "Parameter name: routeTemplate");
+ }
+
+ [Theory]
+ [InlineData("123{a}abc{")]
+ [InlineData("123{a}abc}")]
+ [InlineData("xyz}123{a}abc}")]
+ [InlineData("{{p1}")]
+ [InlineData("{p1}}")]
+ [InlineData("p1}}p2{")]
+ public void InvalidTemplate_WithMismatchedBraces(string template)
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse(template),
+ @"There is an incomplete parameter in the route template. Check that each '{' character has a " +
+ "matching '}' character." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_CannotHaveCatchAllInMultiSegment()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("123{a}abc{*moo}"),
+ "A path segment that contains more than one section, such as a literal section or a parameter, " +
+ "cannot contain a catch-all parameter." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_CannotHaveMoreThanOneCatchAll()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("{*p1}/{*p2}"),
+ "A catch-all parameter can only appear as the last segment of the route template." +
+ Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("{*p1}abc{*p2}"),
+ "A path segment that contains more than one section, such as a literal section or a parameter, " +
+ "cannot contain a catch-all parameter." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_CannotHaveCatchAllWithNoName()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("foo/{*}"),
+ "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" +
+ " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional," +
+ " and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
+ " and can occur only at the start of the parameter." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Theory]
+ [InlineData("{**}", "*")]
+ [InlineData("{a*}", "a*")]
+ [InlineData("{*a*}", "a*")]
+ [InlineData("{*a*:int}", "a*")]
+ [InlineData("{*a*=5}", "a*")]
+ [InlineData("{*a*b=5}", "a*b")]
+ [InlineData("{p1?}.{p2/}/{p3}", "p2/")]
+ [InlineData("{p{{}", "p{")]
+ [InlineData("{p}}}", "p}")]
+ [InlineData("{p/}", "p/")]
+ public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters(
+ string template,
+ string parameterName)
+ {
+ // Arrange
+ var expectedMessage = "The route parameter name '" + parameterName + "' is invalid. Route parameter " +
+ "names must be non-empty and cannot contain these characters: '{', '}', '/'. The '?' character " +
+ "marks a parameter as optional, and can occur only at the end of the parameter. The '*' character " +
+ "marks a parameter as catch-all, and can occur only at the start of the parameter.";
+
+ // Act & Assert
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse(template), expectedMessage + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Theory]
+ [InlineData("/foo")]
+ [InlineData("~/foo")]
+ public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routeTemplate)
+ {
+ // Arrange & Act
+ var template = TemplateParser.Parse(routeTemplate);
+
+ // Assert
+ Assert.Equal(routeTemplate, template.TemplateText);
+ }
+
+ [Fact]
+ public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("foo/{{p1}"),
+ "There is an incomplete parameter in the route template. Check that each '{' character has a " +
+ "matching '}' character." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_CannotHaveConsecutiveCloseBrace()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("foo/{p1}}"),
+ "There is an incomplete parameter in the route template. Check that each '{' character has a " +
+ "matching '}' character." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_SameParameterTwiceThrows()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("{aaa}/{AAA}"),
+ "The route parameter name 'AAA' appears more than one time in the route template." +
+ Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("{aaa}/{*AAA}"),
+ "The route parameter name 'AAA' appears more than one time in the route template." +
+ Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("{a}/{aa}a}/{z}"),
+ "There is an incomplete parameter in the route template. Check that each '{' character has a " +
+ "matching '}' character." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("{a}/{a{aa}/{z}"),
+ "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("{a}/{}/{z}"),
+ "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" +
+ " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
+ " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
+ " and can occur only at the start of the parameter." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_InvalidParameterNameWithQuestionThrows()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("{Controller}.mvc/{?}"),
+ "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" +
+ " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
+ " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
+ " and can occur only at the start of the parameter." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("{a}//{z}"),
+ "The route template separator character '/' cannot appear consecutively. It must be separated by " +
+ "either a parameter or a literal value." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_WithCatchAllNotAtTheEndThrows()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"),
+ "A catch-all parameter can only appear as the last segment of the route template." +
+ Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_RepeatedParametersThrows()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("foo/aa{p1}{p2}"),
+ "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by " +
+ "a literal string." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_CannotStartWithTilde()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("~foo"),
+ "The route template cannot start with a '~' character unless followed by a '/'." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_CannotContainQuestionMark()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("foor?bar"),
+ "The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." +
+ Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("{foor?b}"),
+ "The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot" +
+ " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
+ " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
+ " and can occur only at the start of the parameter." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ [Fact]
+ public void InvalidTemplate_CatchAllMarkedOptional()
+ {
+ ExceptionAssert.Throws<ArgumentException>(
+ () => TemplateParser.Parse("{a}/{*b?}"),
+ "A catch-all parameter cannot be marked optional." + Environment.NewLine +
+ "Parameter name: routeTemplate");
+ }
+
+ private class TemplateEqualityComparer : IEqualityComparer<RouteTemplate>
+ {
+ public bool Equals(RouteTemplate x, RouteTemplate y)
+ {
+ if (x == null && y == null)
+ {
+ return true;
+ }
+ else if (x == null || y == null)
+ {
+ return false;
+ }
+ else
+ {
+ if (!string.Equals(x.TemplateText, y.TemplateText, StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ if (x.Segments.Count != y.Segments.Count)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < x.Segments.Count; i++)
+ {
+ if (x.Segments[i].Parts.Count != y.Segments[i].Parts.Count)
+ {
+ return false;
+ }
+
+ for (int j = 0; j < x.Segments[i].Parts.Count; j++)
+ {
+ if (!Equals(x.Segments[i].Parts[j], y.Segments[i].Parts[j]))
+ {
+ return false;
+ }
+ }
+ }
+
+ if (x.Parameters.Count != y.Parameters.Count)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < x.Parameters.Count; i++)
+ {
+ if (!Equals(x.Parameters[i], y.Parameters[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ private bool Equals(TemplatePart x, TemplatePart y)
+ {
+ if (x.IsLiteral != y.IsLiteral ||
+ x.IsParameter != y.IsParameter ||
+ x.IsCatchAll != y.IsCatchAll ||
+ x.IsOptional != y.IsOptional ||
+ !String.Equals(x.Name, y.Name, StringComparison.Ordinal) ||
+ !String.Equals(x.Name, y.Name, StringComparison.Ordinal) ||
+ (x.InlineConstraints == null && y.InlineConstraints != null) ||
+ (x.InlineConstraints != null && y.InlineConstraints == null))
+ {
+ return false;
+ }
+
+ if (x.InlineConstraints == null && y.InlineConstraints == null)
+ {
+ return true;
+ }
+
+ if (x.InlineConstraints.Count() != y.InlineConstraints.Count())
+ {
+ return false;
+ }
+
+ foreach (var xconstraint in x.InlineConstraints)
+ {
+ if (!y.InlineConstraints.Any<InlineConstraint>(
+ c => string.Equals(c.Constraint, xconstraint.Constraint)))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public int GetHashCode(RouteTemplate obj)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.cs
new file mode 100644
index 0000000000..96c126fd30
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/TemplateParserDefaultValuesTests.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 Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tests
+{
+ public class TemplateParserDefaultValuesTests
+ {
+ private static IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver();
+
+ [Fact]
+ public void InlineDefaultValueSpecified_InlineValueIsUsed()
+ {
+ // Arrange & Act
+ var routeBuilder = CreateRouteBuilder();
+
+ // Act
+ routeBuilder.MapRoute("mockName",
+ "{controller}/{action}/{id:int=12}",
+ defaults: null,
+ constraints: null);
+
+ // Assert
+ var defaults = ((Route)routeBuilder.Routes[0]).Defaults;
+ Assert.Equal("12", defaults["id"]);
+ }
+
+ [Theory]
+ [InlineData(@"{controller}/{action}/{p1:regex(([}}])\w+)=}}asd}", "}asd")]
+ [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)=12/12/1234}", @"12/12/1234")]
+ public void InlineDefaultValueSpecified_WithSpecialCharacters(string template, string value)
+ {
+ // Arrange & Act
+ var routeBuilder = CreateRouteBuilder();
+
+ // Act
+ routeBuilder.MapRoute("mockName",
+ template,
+ defaults: null,
+ constraints: null);
+
+ // Assert
+ var defaults = ((Route)routeBuilder.Routes[0]).Defaults;
+ Assert.Equal(value, defaults["p1"]);
+ }
+
+ [Fact]
+ public void ExplicitDefaultValueSpecified_WithInlineDefaultValue_Throws()
+ {
+ // Arrange
+ var routeBuilder = CreateRouteBuilder();
+
+ // Act & Assert
+ var ex = Assert.Throws<RouteCreationException>(
+ () => routeBuilder.MapRoute("mockName",
+ "{controller}/{action}/{id:int=12}",
+ defaults: new { id = 13 },
+ constraints: null));
+
+ var message = "An error occurred while creating the route with name 'mockName' and template" +
+ " '{controller}/{action}/{id:int=12}'.";
+ Assert.Equal(message, ex.Message);
+
+ Assert.NotNull(ex.InnerException);
+ message = "The route parameter 'id' has both an inline default value and an explicit default" +
+ " value specified. A route parameter cannot contain an inline default value when" +
+ " a default value is specified explicitly. Consider removing one of them.";
+ Assert.Equal(message, ex.InnerException.Message);
+ }
+
+ [Fact]
+ public void EmptyDefaultValue_WithOptionalParameter_Throws()
+ {
+ // Arrange
+ var routeBuilder = CreateRouteBuilder();
+
+ // Act & Assert
+ var ex = Assert.Throws<RouteCreationException>(
+ () => routeBuilder.MapRoute("mockName",
+ "{controller}/{action}/{id:int=?}",
+ defaults: new { id = 13 },
+ constraints: null));
+
+ var message = "An error occurred while creating the route with name 'mockName' and template" +
+ " '{controller}/{action}/{id:int=?}'.";
+ Assert.Equal(message, ex.Message);
+
+ Assert.NotNull(ex.InnerException);
+ message = "An optional parameter cannot have default value." + Environment.NewLine +
+ "Parameter name: routeTemplate";
+ Assert.Equal(message, ex.InnerException.Message);
+ }
+
+ [Fact]
+ public void NonEmptyDefaultValue_WithOptionalParameter_Throws()
+ {
+ // Arrange
+ var routeBuilder = CreateRouteBuilder();
+
+ // Act & Assert
+ var ex = Assert.Throws<RouteCreationException>(() =>
+ {
+ routeBuilder.MapRoute(
+ "mockName",
+ "{controller}/{action}/{id:int=12?}",
+ defaults: new { id = 13 },
+ constraints: null);
+ });
+
+ var message = "An error occurred while creating the route with name 'mockName' and template" +
+ " '{controller}/{action}/{id:int=12?}'.";
+ Assert.Equal(message, ex.Message);
+
+ Assert.NotNull(ex.InnerException);
+ message = "An optional parameter cannot have default value." + Environment.NewLine +
+ "Parameter name: routeTemplate";
+ Assert.Equal(message, ex.InnerException.Message);
+ }
+
+ private static IRouteBuilder CreateRouteBuilder()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton<IInlineConstraintResolver>(_inlineConstraintResolver);
+ services.AddSingleton<RoutingMarkerService>();
+
+ var applicationBuilder = Mock.Of<IApplicationBuilder>();
+ applicationBuilder.ApplicationServices = services.BuildServiceProvider();
+
+ var routeBuilder = new RouteBuilder(applicationBuilder);
+ routeBuilder.DefaultHandler = Mock.Of<IRouter>();
+ return routeBuilder;
+ }
+
+ private static IInlineConstraintResolver GetInlineConstraintResolver()
+ {
+ var services = new ServiceCollection().AddOptions();
+ var serviceProvider = services.BuildServiceProvider();
+ var accessor = serviceProvider.GetRequiredService<IOptions<RouteOptions>>();
+ return new DefaultInlineConstraintResolver(accessor);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouteBuilderTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouteBuilderTest.cs
new file mode 100644
index 0000000000..2645e78439
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouteBuilderTest.cs
@@ -0,0 +1,266 @@
+// 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.Text.Encodings.Web;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.AspNetCore.Routing.Template;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tree
+{
+ public class TreeRouteBuilderTest
+ {
+ [Fact]
+ public void TreeRouter_BuildThrows_RoutesWithTheSameNameAndDifferentTemplates()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ var message = "Two or more routes named 'Get_Products' have different templates.";
+
+ builder.MapOutbound(
+ Mock.Of<IRouter>(),
+ TemplateParser.Parse("api/Products"),
+ new RouteValueDictionary(),
+ "Get_Products",
+ order: 0);
+
+ builder.MapOutbound(
+ Mock.Of<IRouter>(),
+ TemplateParser.Parse("Products/Index"),
+ new RouteValueDictionary(),
+ "Get_Products",
+ order: 0);
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgument(() =>
+ {
+ builder.Build();
+ }, "linkGenerationEntries", message);
+ }
+
+ [Fact]
+ public void TreeRouter_BuildDoesNotThrow_RoutesWithTheSameNameAndSameTemplates()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ builder.MapOutbound(
+ Mock.Of<IRouter>(),
+ TemplateParser.Parse("api/Products"),
+ new RouteValueDictionary(),
+ "Get_Products",
+ order: 0);
+
+ builder.MapOutbound(
+ Mock.Of<IRouter>(),
+ TemplateParser.Parse("api/products"),
+ new RouteValueDictionary(),
+ "Get_Products",
+ order: 0);
+
+ // Act & Assert (does not throw)
+ builder.Build();
+ }
+
+ [Fact]
+ public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithDefaultValues()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ builder.MapInbound(
+ Mock.Of<IRouter>(),
+ TemplateParser.Parse("a/{b=3}/c"),
+ "Intermediate",
+ order: 0);
+
+ // Act
+ var tree = builder.Build();
+
+ // Assert
+ Assert.NotNull(tree);
+ Assert.NotNull(tree.MatchingTrees);
+ var matchingTree = Assert.Single(tree.MatchingTrees);
+
+ var firstSegment = Assert.Single(matchingTree.Root.Literals);
+ Assert.Equal("a", firstSegment.Key);
+ Assert.NotNull(firstSegment.Value.Parameters);
+
+ var secondSegment = firstSegment.Value.Parameters;
+ Assert.Empty(secondSegment.Matches);
+
+ var thirdSegment = Assert.Single(secondSegment.Literals);
+ Assert.Equal("c", thirdSegment.Key);
+ Assert.Single(thirdSegment.Value.Matches);
+ }
+
+ [Fact]
+ public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithMultipleIntermediateParametersWithDefaultOrOptionalValues()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ builder.MapInbound(
+ Mock.Of<IRouter>(),
+ TemplateParser.Parse("a/{b=3}/c/{d?}/e/{*f}"),
+ "Intermediate",
+ order: 0);
+
+ // Act
+ var tree = builder.Build();
+
+ // Assert
+ Assert.NotNull(tree);
+ Assert.NotNull(tree.MatchingTrees);
+ var matchingTree = Assert.Single(tree.MatchingTrees);
+
+ var firstSegment = Assert.Single(matchingTree.Root.Literals);
+ Assert.Equal("a", firstSegment.Key);
+ Assert.NotNull(firstSegment.Value.Parameters);
+
+ var secondSegment = firstSegment.Value.Parameters;
+ Assert.Empty(secondSegment.Matches);
+
+ var thirdSegment = Assert.Single(secondSegment.Literals);
+ Assert.Equal("c", thirdSegment.Key);
+ Assert.Empty(thirdSegment.Value.Matches);
+
+ var fourthSegment = thirdSegment.Value.Parameters;
+ Assert.NotNull(fourthSegment);
+ Assert.Empty(fourthSegment.Matches);
+
+ var fifthSegment = Assert.Single(fourthSegment.Literals);
+ Assert.Equal("e", fifthSegment.Key);
+ Assert.Single(fifthSegment.Value.Matches);
+
+ var sixthSegment = fifthSegment.Value.CatchAlls;
+ Assert.NotNull(sixthSegment);
+ Assert.Single(sixthSegment.Matches);
+ }
+
+ [Fact]
+ public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithOptionalValues()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ builder.MapInbound(
+ Mock.Of<IRouter>(),
+ TemplateParser.Parse("a/{b?}/c"),
+ "Intermediate",
+ order: 0);
+
+ // Act
+ var tree = builder.Build();
+
+ // Assert
+ Assert.NotNull(tree);
+ Assert.NotNull(tree.MatchingTrees);
+ var matchingTree = Assert.Single(tree.MatchingTrees);
+
+ var firstSegment = Assert.Single(matchingTree.Root.Literals);
+ Assert.Equal("a", firstSegment.Key);
+ Assert.NotNull(firstSegment.Value.Parameters);
+
+ var secondSegment = firstSegment.Value.Parameters;
+ Assert.Empty(secondSegment.Matches);
+
+ var thirdSegment = Assert.Single(secondSegment.Literals);
+ Assert.Equal("c", thirdSegment.Key);
+ Assert.Single(thirdSegment.Value.Matches);
+ }
+
+ [Fact]
+ public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithConstrainedDefaultValues()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ builder.MapInbound(
+ Mock.Of<IRouter>(),
+ TemplateParser.Parse("a/{b:int=3}/c"),
+ "Intermediate",
+ order: 0);
+
+ // Act
+ var tree = builder.Build();
+
+ // Assert
+ Assert.NotNull(tree);
+ Assert.NotNull(tree.MatchingTrees);
+ var matchingTree = Assert.Single(tree.MatchingTrees);
+
+ var firstSegment = Assert.Single(matchingTree.Root.Literals);
+ Assert.Equal("a", firstSegment.Key);
+ Assert.NotNull(firstSegment.Value.ConstrainedParameters);
+
+ var secondSegment = firstSegment.Value.ConstrainedParameters;
+ Assert.Empty(secondSegment.Matches);
+
+ var thirdSegment = Assert.Single(secondSegment.Literals);
+ Assert.Equal("c", thirdSegment.Key);
+ Assert.Single(thirdSegment.Value.Matches);
+ }
+
+ [Fact]
+ public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithConstrainedOptionalValues()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ builder.MapInbound(
+ Mock.Of<IRouter>(),
+ TemplateParser.Parse("a/{b:int?}/c"),
+ "Intermediate",
+ order: 0);
+
+ // Act
+ var tree = builder.Build();
+
+ // Assert
+ Assert.NotNull(tree);
+ Assert.NotNull(tree.MatchingTrees);
+ var matchingTree = Assert.Single(tree.MatchingTrees);
+
+ var firstSegment = Assert.Single(matchingTree.Root.Literals);
+ Assert.Equal("a", firstSegment.Key);
+ Assert.NotNull(firstSegment.Value.ConstrainedParameters);
+
+ var secondSegment = firstSegment.Value.ConstrainedParameters;
+ Assert.Empty(secondSegment.Matches);
+
+ var thirdSegment = Assert.Single(secondSegment.Literals);
+ Assert.Equal("c", thirdSegment.Key);
+ Assert.Single(thirdSegment.Value.Matches);
+ }
+
+ private static TreeRouteBuilder CreateBuilder()
+ {
+ var objectPoolProvider = new DefaultObjectPoolProvider();
+ var objectPolicy = new UriBuilderContextPooledObjectPolicy();
+ var objectPool = objectPoolProvider.Create(objectPolicy);
+
+ var constraintResolver = GetInlineConstraintResolver();
+ var builder = new TreeRouteBuilder(
+ NullLoggerFactory.Instance,
+ objectPool,
+ constraintResolver);
+ return builder;
+ }
+
+ private static IInlineConstraintResolver GetInlineConstraintResolver()
+ {
+ var services = new ServiceCollection().AddOptions();
+ var serviceProvider = services.BuildServiceProvider();
+ var accessor = serviceProvider.GetRequiredService<IOptions<RouteOptions>>();
+ return new DefaultInlineConstraintResolver(accessor);
+ }
+ }
+}
diff --git a/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs
new file mode 100644
index 0000000000..66d15c5314
--- /dev/null
+++ b/src/Routing/test/Microsoft.AspNetCore.Routing.Tests/Tree/TreeRouterTest.cs
@@ -0,0 +1,2209 @@
+// 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.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Internal;
+using Microsoft.AspNetCore.Routing.Template;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing.Tree
+{
+ public class TreeRouterTest
+ {
+ private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0);
+
+ private static ObjectPool<UriBuildingContext> Pool = new DefaultObjectPoolProvider().Create(
+ new UriBuilderContextPooledObjectPolicy());
+
+ [Fact]
+ public async Task TreeRouter_RouteAsync_MatchesCatchAllRoutesWithDefaults_UsingObsoleteConstructo()
+ {
+ // Arrange
+ var routes = new[] {
+ "{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}",
+ };
+ var url = "/a/b/c";
+ var routeValues = new[] { "a", "b", "c", "4" };
+
+ var expectedRouteGroup = CreateRouteGroup(0, "{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}");
+ var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" };
+ var expectedRouteValues = new RouteValueDictionary();
+ for (int i = 0; i < routeValueKeys.Length; i++)
+ {
+ expectedRouteValues.Add(routeValueKeys[i], routeValues[i]);
+ }
+
+ var builder = CreateBuilderUsingObsoleteConstructor();
+
+ // We setup the route entries in reverse order of precedence to ensure that when we
+ // try to route the request, the route with a higher precedence gets tried first.
+ foreach (var template in routes.Reverse())
+ {
+ MapInboundEntry(builder, template);
+ }
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
+ foreach (var entry in expectedRouteValues)
+ {
+ var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key);
+ Assert.Equal(entry.Value, data.Value);
+ }
+ }
+
+ [Fact]
+ public async Task TreeRouter_RouteAsync_DoesNotMatchRoutesWithIntermediateDefaultRouteValues_UsingObsoleteConstructor()
+ {
+ // Arrange
+ var url = "/a/b";
+
+ var builder = CreateBuilderUsingObsoleteConstructor();
+
+ MapInboundEntry(builder, "a/b/{parameter3=3}/d");
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+ }
+
+ [Theory]
+ [InlineData("template/5", "template/{parameter:int}")]
+ [InlineData("template/5", "template/{parameter}")]
+ [InlineData("template/5", "template/{*parameter:int}")]
+ [InlineData("template/5", "template/{*parameter}")]
+ [InlineData("template/{parameter}", "template/{parameter:alpha}")] // constraint doesn't match
+ [InlineData("template/{parameter:int}", "template/{parameter}")]
+ [InlineData("template/{parameter:int}", "template/{*parameter:int}")]
+ [InlineData("template/{parameter:int}", "template/{*parameter}")]
+ [InlineData("template/{parameter}", "template/{*parameter:int}")]
+ [InlineData("template/{parameter}", "template/{*parameter}")]
+ [InlineData("template/{*parameter:int}", "template/{*parameter}")]
+ public async Task TreeRouter_RouteAsync_RespectsPrecedence(
+ string firstTemplate,
+ string secondTemplate)
+ {
+ // Arrange
+ var expectedRouteGroup = CreateRouteGroup(0, firstTemplate);
+
+ var builder = CreateBuilder();
+
+ // We setup the route entries in reverse order of precedence to ensure that when we
+ // try to route the request, the route with a higher precedence gets tried first.
+ MapInboundEntry(builder, secondTemplate);
+ MapInboundEntry(builder, firstTemplate);
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext("/template/5");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
+ }
+
+ [Theory]
+ [InlineData("/", "")]
+ [InlineData("/Literal1", "Literal1")]
+ [InlineData("/Literal1/Literal2", "Literal1/Literal2")]
+ [InlineData("/Literal1/Literal2/Literal3", "Literal1/Literal2/Literal3")]
+ [InlineData("/Literal1/Literal2/Literal3/4", "Literal1/Literal2/Literal3/{*constrainedCatchAll:int}")]
+ [InlineData("/Literal1/Literal2/Literal3/Literal4", "Literal1/Literal2/Literal3/{*catchAll}")]
+ [InlineData("/1", "{constrained1:int}")]
+ [InlineData("/1/2", "{constrained1:int}/{constrained2:int}")]
+ [InlineData("/1/2/3", "{constrained1:int}/{constrained2:int}/{constrained3:int}")]
+ [InlineData("/1/2/3/4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*constrainedCatchAll:int}")]
+ [InlineData("/1/2/3/CatchAll4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*catchAll}")]
+ [InlineData("/parameter1", "{parameter1}")]
+ [InlineData("/parameter1/parameter2", "{parameter1}/{parameter2}")]
+ [InlineData("/parameter1/parameter2/parameter3", "{parameter1}/{parameter2}/{parameter3}")]
+ [InlineData("/parameter1/parameter2/parameter3/4", "{parameter1}/{parameter2}/{parameter3}/{*constrainedCatchAll:int}")]
+ [InlineData("/parameter1/parameter2/parameter3/CatchAll4", "{parameter1}/{parameter2}/{parameter3}/{*catchAll}")]
+ public async Task TreeRouter_RouteAsync_MatchesRouteWithTheRightLength(string url, string expected)
+ {
+ // Arrange
+ var routes = new[] {
+ "",
+ "Literal1",
+ "Literal1/Literal2",
+ "Literal1/Literal2/Literal3",
+ "Literal1/Literal2/Literal3/{*constrainedCatchAll:int}",
+ "Literal1/Literal2/Literal3/{*catchAll}",
+ "{constrained1:int}",
+ "{constrained1:int}/{constrained2:int}",
+ "{constrained1:int}/{constrained2:int}/{constrained3:int}",
+ "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*constrainedCatchAll:int}",
+ "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*catchAll}",
+ "{parameter1}",
+ "{parameter1}/{parameter2}",
+ "{parameter1}/{parameter2}/{parameter3}",
+ "{parameter1}/{parameter2}/{parameter3}/{*constrainedCatchAll:int}",
+ "{parameter1}/{parameter2}/{parameter3}/{*catchAll}",
+ };
+
+ var expectedRouteGroup = CreateRouteGroup(0, expected);
+
+ var builder = CreateBuilder();
+
+ // We setup the route entries in reverse order of precedence to ensure that when we
+ // try to route the request, the route with a higher precedence gets tried first.
+ foreach (var template in routes.Reverse())
+ {
+ MapInboundEntry(builder, template);
+ }
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
+ }
+
+ public static TheoryData<string, object[]> MatchesRoutesWithDefaultsData =>
+ new TheoryData<string, object[]>
+ {
+ { "/", new object[] { "1", "2", "3", "4" } },
+ { "/a", new object[] { "a", "2", "3", "4" } },
+ { "/a/b", new object[] { "a", "b", "3", "4" } },
+ { "/a/b/c", new object[] { "a", "b", "c", "4" } },
+ { "/a/b/c/d", new object[] { "a", "b", "c", "d" } }
+ };
+
+ [Theory]
+ [MemberData(nameof(MatchesRoutesWithDefaultsData))]
+ public async Task TreeRouter_RouteAsync_MatchesRoutesWithDefaults(string url, object[] routeValues)
+ {
+ // Arrange
+ var routes = new[] {
+ "{parameter1=1}/{parameter2=2}/{parameter3=3}/{parameter4=4}",
+ };
+
+ var expectedRouteGroup = CreateRouteGroup(0, "{parameter1=1}/{parameter2=2}/{parameter3=3}/{parameter4=4}");
+ var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" };
+ var expectedRouteValues = new RouteValueDictionary();
+ for (int i = 0; i < routeValueKeys.Length; i++)
+ {
+ expectedRouteValues.Add(routeValueKeys[i], routeValues[i]);
+ }
+
+ var builder = CreateBuilder();
+
+ // We setup the route entries in reverse order of precedence to ensure that when we
+ // try to route the request, the route with a higher precedence gets tried first.
+ foreach (var template in routes.Reverse())
+ {
+ MapInboundEntry(builder, template);
+ }
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
+ foreach (var entry in expectedRouteValues)
+ {
+ var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key);
+ Assert.Equal(entry.Value, data.Value);
+ }
+ }
+
+ public static TheoryData<string, object[]> MatchesConstrainedRoutesWithDefaultsData =>
+ new TheoryData<string, object[]>
+ {
+ { "/", new object[] { "1", "2", "3", "4" } },
+ { "/10", new object[] { "10", "2", "3", "4" } },
+ { "/10/11", new object[] { "10", "11", "3", "4" } },
+ { "/10/11/12", new object[] { "10", "11", "12", "4" } },
+ { "/10/11/12/13", new object[] { "10", "11", "12", "13" } }
+ };
+
+ [Theory]
+ [MemberData(nameof(MatchesConstrainedRoutesWithDefaultsData))]
+ public async Task TreeRouter_RouteAsync_MatchesConstrainedRoutesWithDefaults(string url, object[] routeValues)
+ {
+ // Arrange
+ var routes = new[] {
+ "{parameter1:int=1}/{parameter2:int=2}/{parameter3:int=3}/{parameter4:int=4}",
+ };
+
+ var expectedRouteGroup = CreateRouteGroup(0, "{parameter1:int=1}/{parameter2:int=2}/{parameter3:int=3}/{parameter4:int=4}");
+ var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" };
+ var expectedRouteValues = new RouteValueDictionary();
+ for (int i = 0; i < routeValueKeys.Length; i++)
+ {
+ expectedRouteValues.Add(routeValueKeys[i], routeValues[i]);
+ }
+
+ var builder = CreateBuilder();
+
+ // We setup the route entries in reverse order of precedence to ensure that when we
+ // try to route the request, the route with a higher precedence gets tried first.
+ foreach (var template in routes.Reverse())
+ {
+ MapInboundEntry(builder, template);
+ }
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
+ foreach (var entry in expectedRouteValues)
+ {
+ var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key);
+ Assert.Equal(entry.Value, data.Value);
+ }
+ }
+
+ [Fact]
+ public async Task TreeRouter_RouteAsync_MatchesCatchAllRoutesWithDefaults()
+ {
+ // Arrange
+ var routes = new[] {
+ "{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}",
+ };
+ var url = "/a/b/c";
+ var routeValues = new[] { "a", "b", "c", "4" };
+
+ var expectedRouteGroup = CreateRouteGroup(0, "{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}");
+ var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" };
+ var expectedRouteValues = new RouteValueDictionary();
+ for (int i = 0; i < routeValueKeys.Length; i++)
+ {
+ expectedRouteValues.Add(routeValueKeys[i], routeValues[i]);
+ }
+
+ var builder = CreateBuilder();
+
+ // We setup the route entries in reverse order of precedence to ensure that when we
+ // try to route the request, the route with a higher precedence gets tried first.
+ foreach (var template in routes.Reverse())
+ {
+ MapInboundEntry(builder, template);
+ }
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
+ foreach (var entry in expectedRouteValues)
+ {
+ var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key);
+ Assert.Equal(entry.Value, data.Value);
+ }
+ }
+
+ [Fact]
+ public async Task TreeRouter_RouteAsync_DoesNotMatchRoutesWithIntermediateDefaultRouteValues()
+ {
+ // Arrange
+ var url = "/a/b";
+
+ var builder = CreateBuilder();
+
+ MapInboundEntry(builder, "a/b/{parameter3=3}/d");
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+ }
+
+ [Theory]
+ [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a")]
+ [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b")]
+ [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c")]
+ [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d")]
+ public async Task TreeRouter_RouteAsync_DoesNotMatchRoutesWithMultipleIntermediateDefaultOrOptionalRouteValues(string template, string url)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ MapInboundEntry(builder, template);
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+ }
+
+ [Theory]
+ [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e")]
+ [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e/f")]
+ public async Task RouteAsync_MatchRoutesWithMultipleIntermediateDefaultOrOptionalRouteValues_WhenAllIntermediateValuesAreProvided(string template, string url)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ MapInboundEntry(builder, template);
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ }
+
+ [Fact]
+ public async Task TreeRouter_RouteAsync_DoesNotMatchShorterUrl()
+ {
+ // Arrange
+ var routes = new[] {
+ "Literal1/Literal2/Literal3",
+ };
+
+ var builder = CreateBuilder();
+
+ // We setup the route entries in reverse order of precedence to ensure that when we
+ // try to route the request, the route with a higher precedence gets tried first.
+ foreach (var template in routes.Reverse())
+ {
+ MapInboundEntry(builder, template);
+ }
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext("/Literal1");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+ }
+
+ [Theory]
+ [InlineData("template/5", "template/{parameter:int}")]
+ [InlineData("template/5", "template/{parameter}")]
+ [InlineData("template/5", "template/{*parameter:int}")]
+ [InlineData("template/5", "template/{*parameter}")]
+ [InlineData("template/{parameter:int}", "template/{parameter}")]
+ [InlineData("template/{parameter:int}", "template/{*parameter:int}")]
+ [InlineData("template/{parameter:int}", "template/{*parameter}")]
+ [InlineData("template/{parameter}", "template/{*parameter:int}")]
+ [InlineData("template/{parameter}", "template/{*parameter}")]
+ [InlineData("template/{*parameter:int}", "template/{*parameter}")]
+ public async Task TreeRouter_RouteAsync_RespectsOrderOverPrecedence(
+ string firstTemplate,
+ string secondTemplate)
+ {
+ // Arrange
+ var expectedRouteGroup = CreateRouteGroup(0, secondTemplate);
+
+ var builder = CreateBuilder();
+
+ // We setup the route entries with a lower relative order and higher relative precedence
+ // first to ensure that when we try to route the request, the route with the higher
+ // relative order gets tried first.
+ MapInboundEntry(builder, firstTemplate, order: 1);
+ MapInboundEntry(builder, secondTemplate, order: 0);
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext("/template/5");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
+ }
+
+ [Theory]
+ [InlineData("///")]
+ [InlineData("/a//")]
+ [InlineData("/a/b//")]
+ [InlineData("//b//")]
+ [InlineData("///c")]
+ [InlineData("///c/")]
+ public async Task TryMatch_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ MapInboundEntry(builder, "{controller?}/{action?}/{id?}");
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("/")]
+ [InlineData("/a")]
+ [InlineData("/a/")]
+ [InlineData("/a/b")]
+ [InlineData("/a/b/")]
+ [InlineData("/a/b/c")]
+ [InlineData("/a/b/c/")]
+ public async Task TryMatch_MultipleOptionalParameters_WithIncrementalOptionalValues(string url)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ MapInboundEntry(builder, "{controller?}/{action?}/{id?}");
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ }
+
+ [Theory]
+ [InlineData("///")]
+ [InlineData("////")]
+ [InlineData("/a//")]
+ [InlineData("/a///")]
+ [InlineData("//b/")]
+ [InlineData("//b//")]
+ [InlineData("///c")]
+ [InlineData("///c/")]
+ public async Task TryMatch_MultipleParameters_WithEmptyValues(string url)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ MapInboundEntry(builder, "{controller}/{action}/{id}");
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+ }
+
+ [Theory]
+ [InlineData("/a/b/c//")]
+ [InlineData("/a/b/c/////")]
+ public async Task TryMatch_CatchAllParameters_WithEmptyValuesAtTheEnd(string url)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ MapInboundEntry(builder, "{controller}/{action}/{*id}");
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ }
+
+ [Theory]
+ [InlineData("/a/b//")]
+ [InlineData("/a/b///c")]
+ public async Task TryMatch_CatchAllParameters_WithEmptyValues(string url)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ MapInboundEntry(builder, "{controller}/{action}/{*id}");
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext(url);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+ }
+
+ [Theory]
+ [InlineData("{*path}", "/a", "a")]
+ [InlineData("{*path}", "/a/b/c", "a/b/c")]
+ [InlineData("a/{*path}", "/a/b", "b")]
+ [InlineData("a/{*path}", "/a/b/c/d", "b/c/d")]
+ [InlineData("a/{*path:regex(10/20/30)}", "/a/10/20/30", "10/20/30")]
+ public async Task TreeRouter_RouteAsync_MatchesWildCard_ForLargerPathSegments(
+ string template,
+ string requestPath,
+ string expectedResult)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapInboundEntry(builder, template);
+ var route = builder.Build();
+
+ var context = CreateRouteContext(requestPath);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.Equal(expectedResult, context.RouteData.Values["path"]);
+ }
+
+ [Theory]
+ [InlineData("a/{*path}", "/a")]
+ [InlineData("a/{*path}", "/a/")]
+ public async Task TreeRouter_RouteAsync_MatchesCatchAll_NullValue(
+ string template,
+ string requestPath)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapInboundEntry(builder, template);
+ var route = builder.Build();
+
+ var context = CreateRouteContext(requestPath);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.Null(context.RouteData.Values["path"]);
+ }
+
+ [Theory]
+ [InlineData("a/{*path}", "/a")]
+ [InlineData("a/{*path}", "/a/")]
+ public async Task TreeRouter_RouteAsync_MatchesCatchAll_NullValue_DoesNotReplaceExistingValue(
+ string template,
+ string requestPath)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapInboundEntry(builder, template);
+ var route = builder.Build();
+
+ var context = CreateRouteContext(requestPath);
+ context.RouteData.Values["path"] = "existing-value";
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.Equal("existing-value", context.RouteData.Values["path"]);
+ }
+
+ [Theory]
+ [InlineData("a/{*path=default}", "/a")]
+ [InlineData("a/{*path=default}", "/a/")]
+ public async Task TreeRouter_RouteAsync_MatchesCatchAll_UsesDefaultValue(
+ string template,
+ string requestPath)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapInboundEntry(builder, template);
+ var route = builder.Build();
+
+ var context = CreateRouteContext(requestPath);
+ context.RouteData.Values["path"] = "existing-value";
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ Assert.Equal("default", context.RouteData.Values["path"]);
+ }
+
+ [Theory]
+ [InlineData("template/5")]
+ [InlineData("template/{parameter:int}")]
+ [InlineData("template/{parameter}")]
+ [InlineData("template/{*parameter:int}")]
+ [InlineData("template/{*parameter}")]
+ public async Task TreeRouter_RouteAsync_RespectsOrder(string template)
+ {
+ // Arrange
+ var expectedRouteGroup = CreateRouteGroup(0, template);
+
+ var builder = CreateBuilder();
+
+ // We setup the route entries with a lower relative order first to ensure that when
+ // we try to route the request, the route with the higher relative order gets tried first.
+ MapInboundEntry(builder, template, order: 1);
+ MapInboundEntry(builder, template, order: 0);
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext("/template/5");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
+ }
+
+ [Theory]
+ [InlineData("template/{first:int}", "template/{second:int}")]
+ [InlineData("template/{first}", "template/{second}")]
+ [InlineData("template/{*first:int}", "template/{*second:int}")]
+ [InlineData("template/{*first}", "template/{*second}")]
+ public async Task TreeRouter_RouteAsync_EnsuresStableOrdering(string first, string second)
+ {
+ // Arrange
+ var expectedRouteGroup = CreateRouteGroup(0, first);
+
+ var builder = CreateBuilder();
+
+ // We setup the route entries with a lower relative template order first to ensure that when
+ // we try to route the request, the route with the higher template order gets tried first.
+ MapInboundEntry(builder, first);
+ MapInboundEntry(builder, second);
+
+ var route = builder.Build();
+
+ var context = CreateRouteContext("/template/5");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
+ }
+
+ [Theory]
+ [InlineData("template/{parameter:int}", "/template/5", true)]
+ [InlineData("template/{parameter:int?}", "/template/5", true)]
+ [InlineData("template/{parameter:int?}", "/template", true)]
+ [InlineData("template/{parameter:int?}", "/template/qwer", false)]
+ public async Task TreeRouter_WithOptionalInlineConstraint(
+ string template,
+ string request,
+ bool expectedResult)
+ {
+ // Arrange
+ var expectedRouteGroup = CreateRouteGroup(0, template);
+
+ var builder = CreateBuilder();
+ MapInboundEntry(builder, template);
+ var route = builder.Build();
+
+ var context = CreateRouteContext(request);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ if (expectedResult)
+ {
+ Assert.NotNull(context.Handler);
+ Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
+ }
+ else
+ {
+ Assert.Null(context.Handler);
+ }
+ }
+
+ [Theory]
+ [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", "foo", "bar", null)]
+ [InlineData("moo/{p1?}", "/moo/foo", "foo", null, null)]
+ [InlineData("moo/{p1?}", "/moo", null, null, null)]
+ [InlineData("moo/{p1}.{p2?}", "/moo/foo", "foo", null, null)]
+ [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", "foo.", "bar", null)]
+ [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", "foo.moo", "bar", null)]
+ [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", "foo", "bar", null)]
+ [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", "moo", "bar", null)]
+ [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", "moo", null, null)]
+ [InlineData("moo/.{p2?}", "/moo/.foo", null, "foo", null)]
+ [InlineData("moo/{p1}.{p2?}", "/moo/....", "..", ".", null)]
+ [InlineData("moo/{p1}.{p2?}", "/moo/.bar", ".bar", null, null)]
+ [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")]
+ [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)]
+ [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")]
+ [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")]
+ [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")]
+ [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")]
+ public async Task TreeRouter_WithOptionalCompositeParameter_Valid(
+ string template,
+ string request,
+ string p1,
+ string p2,
+ string p3)
+ {
+ // Arrange
+ var expectedRouteGroup = CreateRouteGroup(0, template);
+
+ var builder = CreateBuilder();
+ MapInboundEntry(builder, template);
+ var route = builder.Build();
+
+ var context = CreateRouteContext(request);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(context.Handler);
+ if (p1 != null)
+ {
+ Assert.Equal(p1, context.RouteData.Values["p1"]);
+ }
+ if (p2 != null)
+ {
+ Assert.Equal(p2, context.RouteData.Values["p2"]);
+ }
+ if (p3 != null)
+ {
+ Assert.Equal(p3, context.RouteData.Values["p3"]);
+ }
+ }
+
+ [Theory]
+ [InlineData("moo/{p1}.{p2?}", "/moo/foo.")]
+ [InlineData("moo/{p1}.{p2?}", "/moo/.")]
+ [InlineData("moo/{p1}.{p2}", "/foo.")]
+ [InlineData("moo/{p1}.{p2}", "/foo")]
+ [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")]
+ [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")]
+ [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")]
+ [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")]
+ [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")]
+ [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")]
+ [InlineData("moo/.{p2?}", "/moo/.")]
+ [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")]
+ public async Task TreeRouter_WithOptionalCompositeParameter_Invalid(
+ string template,
+ string request)
+ {
+ // Arrange
+ var expectedRouteGroup = CreateRouteGroup(0, template);
+
+ var builder = CreateBuilder();
+ MapInboundEntry(builder, template);
+ var route = builder.Build();
+
+ var context = CreateRouteContext(request);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Null(context.Handler);
+ }
+
+ [Theory]
+ [InlineData("template", "{*url:alpha}", "/template?url=dingo&id=5")]
+ [InlineData("{*url:alpha}", "{*url}", "/dingo?id=5")]
+ [InlineData("{id}", "{*url}", "/5?url=dingo")]
+ [InlineData("{id}", "{*url:alpha}", "/5?url=dingo")]
+ [InlineData("{id:int}", "{id}", "/5?url=dingo")]
+ [InlineData("{id}", "{id:alpha}/{url}", "/5?url=dingo")] // constraint doesn't match
+ [InlineData("template/api/{*url}", "template/api", "/template/api/dingo?id=5")]
+ [InlineData("template/api", "template/{*url}", "/template/api?url=dingo&id=5")]
+ [InlineData("template/api", "template/api{id}location", "/template/api?url=dingo&id=5")]
+ [InlineData("template/api{id}location", "template/{id:int}", "/template/api5location?url=dingo")]
+ public void TreeRouter_GenerateLink(string firstTemplate, string secondTemplate, string expectedPath)
+ {
+ // Arrange
+ var values = new Dictionary<string, object>
+ {
+ {"url", "dingo" },
+ {"id", 5 }
+ };
+
+ var route = CreateTreeRouter(firstTemplate, secondTemplate);
+ var context = CreateVirtualPathContext(
+ values: values,
+ ambientValues: null);
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(expectedPath, result.VirtualPath);
+ Assert.Same(route, result.Router);
+ Assert.Empty(result.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_LongerTemplateWithDefaultIsMoreSpecific()
+ {
+ // Arrange
+ var firstTemplate = "template";
+ var secondTemplate = "template/{parameter:int=1003}";
+
+ var route = CreateTreeRouter(firstTemplate, secondTemplate);
+ var context = CreateVirtualPathContext(
+ values: null,
+ ambientValues: null);
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(result);
+ // The Binder binds to /template
+ Assert.Equal("/template", result.VirtualPath);
+ Assert.Same(route, result.Router);
+ Assert.Empty(result.DataTokens);
+ }
+
+ [Theory]
+ [InlineData("template/{parameter:int=5}", "template", "/template/5")]
+ [InlineData("template/{parameter}", "template", "/template/5")]
+ [InlineData("template/{parameter}/{id}", "template/{parameter}", "/template/5/1234")]
+ public void TreeRouter_GenerateLink_OrderingAgnostic(
+ string firstTemplate,
+ string secondTemplate,
+ string expectedPath)
+ {
+ // Arrange
+ var route = CreateTreeRouter(firstTemplate, secondTemplate);
+ var parameter = 5;
+ var id = 1234;
+ var values = new Dictionary<string, object>
+ {
+ { nameof(parameter) , parameter},
+ { nameof(id), id }
+ };
+ var context = CreateVirtualPathContext(
+ values: null,
+ ambientValues: values);
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(expectedPath, result.VirtualPath);
+ Assert.Same(route, result.Router);
+ Assert.Empty(result.DataTokens);
+ }
+
+ [Theory]
+ [InlineData("template", "template/{parameter}", "/template/5")]
+ [InlineData("template/{parameter}", "template/{parameter}/{id}", "/template/5/1234")]
+ [InlineData("template", "template/{parameter:int=5}", "/template/5")]
+ public void TreeRouter_GenerateLink_UseAvailableVariables(
+ string firstTemplate,
+ string secondTemplate,
+ string expectedPath)
+ {
+ // Arrange
+ var route = CreateTreeRouter(firstTemplate, secondTemplate);
+ var parameter = 5;
+ var id = 1234;
+ var values = new Dictionary<string, object>
+ {
+ { nameof(parameter) , parameter},
+ { nameof(id), id }
+ };
+ var context = CreateVirtualPathContext(
+ values: null,
+ ambientValues: values);
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(expectedPath, result.VirtualPath);
+ Assert.Same(route, result.Router);
+ Assert.Empty(result.DataTokens);
+ }
+
+ [Theory]
+ [InlineData("template/5", "template/{parameter:int}")]
+ [InlineData("template/5", "template/{parameter}")]
+ [InlineData("template/5", "template/{*parameter:int}")]
+ [InlineData("template/5", "template/{*parameter}")]
+ [InlineData("template/{parameter:int}", "template/{parameter}")]
+ [InlineData("template/{parameter:int}", "template/{*parameter:int}")]
+ [InlineData("template/{parameter:int}", "template/{*parameter}")]
+ [InlineData("template/{parameter}", "template/{*parameter:int}")]
+ [InlineData("template/{parameter}", "template/{*parameter}")]
+ [InlineData("template/{*parameter:int}", "template/{*parameter}")]
+ public void TreeRouter_GenerateLink_RespectsPrecedence(string firstTemplate, string secondTemplate)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // We setup the route entries in reverse order of precedence to ensure that when we
+ // try to generate a link, the route with a higher precedence gets tried first.
+ MapOutboundEntry(builder, secondTemplate);
+ MapOutboundEntry(builder, firstTemplate);
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = 5 });
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("/template/5", result.VirtualPath);
+ Assert.Same(route, result.Router);
+ Assert.Empty(result.DataTokens);
+ }
+
+ [Theory]
+ [InlineData("template/{parameter:int}", "/template/5", 5)]
+ [InlineData("template/{parameter:int?}", "/template/5", 5)]
+ [InlineData("template/{parameter:int?}", "/template", null)]
+ [InlineData("template/{parameter:int?}", null, "asdf")]
+ [InlineData("template/{parameter:alpha?}", "/template/asdf", "asdf")]
+ [InlineData("template/{parameter:alpha?}", "/template", null)]
+ [InlineData("template/{parameter:int:range(1,20)?}", "/template", null)]
+ [InlineData("template/{parameter:int:range(1,20)?}", "/template/5", 5)]
+ [InlineData("template/{parameter:int:range(1,20)?}", null, 21)]
+ public void TreeRouter_GenerateLink_OptionalInlineParameter(
+ string template,
+ string expectedPath,
+ object parameter)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, template);
+ var route = builder.Build();
+
+ VirtualPathContext context;
+ if (parameter != null)
+ {
+ context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = parameter });
+ }
+ else
+ {
+ context = CreateVirtualPathContext(values: null, ambientValues: null);
+ }
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ if (expectedPath == null)
+ {
+ Assert.Null(result);
+ }
+ else
+ {
+ Assert.NotNull(result);
+ Assert.Equal(expectedPath, result.VirtualPath);
+ Assert.Same(route, result.Router);
+ Assert.Empty(result.DataTokens);
+ }
+ }
+
+ [Theory]
+ [InlineData("template/5", "template/{parameter:int}")]
+ [InlineData("template/5", "template/{parameter}")]
+ [InlineData("template/5", "template/{*parameter:int}")]
+ [InlineData("template/5", "template/{*parameter}")]
+ [InlineData("template/{parameter:int}", "template/{parameter}")]
+ [InlineData("template/{parameter:int}", "template/{*parameter:int}")]
+ [InlineData("template/{parameter:int}", "template/{*parameter}")]
+ [InlineData("template/{parameter}", "template/{*parameter:int}")]
+ [InlineData("template/{parameter}", "template/{*parameter}")]
+ [InlineData("template/{*parameter:int}", "template/{*parameter}")]
+ public void TreeRouter_GenerateLink_RespectsOrderOverPrecedence(string firstTemplate, string secondTemplate)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // We setup the route entries with a lower relative order and higher relative precedence
+ // first to ensure that when we try to generate a link, the route with the higher
+ // relative order gets tried first.
+ MapOutboundEntry(builder, firstTemplate, order: 1);
+ MapOutboundEntry(builder, secondTemplate, order: 0);
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(null, ambientValues: new { parameter = 5 });
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("/template/5", result.VirtualPath);
+ Assert.Same(route, result.Router);
+ Assert.Empty(result.DataTokens);
+ }
+
+ [Theory]
+ [InlineData("template/5", "template/5")]
+ [InlineData("template/{first:int}", "template/{second:int}")]
+ [InlineData("template/{first}", "template/{second}")]
+ [InlineData("template/{*first:int}", "template/{*second:int}")]
+ [InlineData("template/{*first}", "template/{*second}")]
+ public void TreeRouter_GenerateLink_RespectsOrder(string firstTemplate, string secondTemplate)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // We setup the route entries with a lower relative order first to ensure that when
+ // we try to generate a link, the route with the higher relative order gets tried first.
+ MapOutboundEntry(builder, firstTemplate, requiredValues: null, order: 1);
+ MapOutboundEntry(builder, secondTemplate, requiredValues: null, order: 0);
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 });
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("/template/5", result.VirtualPath);
+ Assert.Same(route, result.Router);
+ Assert.Empty(result.DataTokens);
+ }
+
+ [Theory]
+ [InlineData("first/5", "second/5")]
+ [InlineData("first/{first:int}", "second/{second:int}")]
+ [InlineData("first/{first}", "second/{second}")]
+ [InlineData("first/{*first:int}", "second/{*second:int}")]
+ [InlineData("first/{*first}", "second/{*second}")]
+ public void TreeRouter_GenerateLink_EnsuresStableOrder(string firstTemplate, string secondTemplate)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // We setup the route entries with a lower relative template order first to ensure that when
+ // we try to generate a link, the route with the higher template order gets tried first.
+ MapOutboundEntry(builder, secondTemplate, requiredValues: null, order: 0);
+ MapOutboundEntry(builder, firstTemplate, requiredValues: null, order: 0);
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 });
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("/first/5", result.VirtualPath);
+ Assert.Same(route, result.Router);
+ Assert.Empty(result.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_CreatesLinksForRoutesWithIntermediateDefaultRouteValues()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ MapOutboundEntry(builder, template: "a/b/{parameter3=3}/d", requiredValues: null, order: 0);
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(values: null, ambientValues: null);
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("/a/b/3/d", result.VirtualPath);
+ }
+
+
+ [Fact]
+ public void TreeRouter_GeneratesLink_ForMultipleNamedEntriesWithTheSameTemplate()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ MapOutboundEntry(builder, "Template", name: "NamedEntry", order: 1);
+ MapOutboundEntry(builder, "TEMPLATE", name: "NamedEntry", order: 2);
+
+ // Act & Assert (does not throw)
+ builder.Build();
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_WithName()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // The named route has a lower order which will ensure that we aren't trying the route as
+ // if it were an unnamed route.
+ MapOutboundEntry(builder, "named", requiredValues: null, order: 1, name: "NamedRoute");
+ MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0);
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NamedRoute");
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("/named", result.VirtualPath);
+ Assert.Same(route, result.Router);
+ Assert.Empty(result.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_DoesNotGenerateLink_IfThereIsNoRouteForAGivenName()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // The named route has a lower order which will ensure that we aren't trying the route as
+ // if it were an unnamed route.
+ MapOutboundEntry(builder, "named", requiredValues: null, order: 1, name: "NamedRoute");
+
+ // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route.
+ MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0);
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NonExistingNamedRoute");
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Theory]
+ [InlineData("template/{parameter:int}", null)]
+ [InlineData("template/{parameter:int}", "NaN")]
+ [InlineData("template/{parameter}", null)]
+ [InlineData("template/{*parameter:int}", null)]
+ [InlineData("template/{*parameter:int}", "NaN")]
+ public void TreeRouter_DoesNotGenerateLink_IfValuesDoNotMatchNamedEntry(string template, string value)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // The named route has a lower order which will ensure that we aren't trying the route as
+ // if it were an unnamed route.
+ MapOutboundEntry(builder, template, requiredValues: null, order: 1, name: "NamedRoute");
+
+ // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route.
+ MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0);
+
+ var route = builder.Build();
+
+ var ambientValues = value == null ? null : new { parameter = value };
+ var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute");
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Theory]
+ [InlineData("template/{parameter:int}", "5")]
+ [InlineData("template/{parameter}", "5")]
+ [InlineData("template/{*parameter:int}", "5")]
+ [InlineData("template/{*parameter}", "5")]
+ public void TreeRouter_GeneratesLink_IfValuesMatchNamedEntry(string template, string value)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // The named route has a lower order which will ensure that we aren't trying the route as
+ // if it were an unnamed route.
+ MapOutboundEntry(builder, template, requiredValues: null, order: 1, name: "NamedRoute");
+
+ // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route.
+ MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0);
+
+ var route = builder.Build();
+
+ var ambientValues = value == null ? null : new { parameter = value };
+ var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute");
+
+ // Act
+ var result = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("/template/5", result.VirtualPath);
+ Assert.Same(route, result.Router);
+ Assert.Empty(result.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_NoRequiredValues()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, "api/Store", new { });
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(new { });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/api/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_Match()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" });
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/api/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_NoMatch()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, "api/Store", new { action = "Details", controller = "Store" });
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" });
+
+ // Act
+ var path = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Null(path);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_Match_WithAmbientValues()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" });
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(new { }, new { action = "Index", controller = "Store" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/api/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_Match_HasTwoOptionalParametersWithoutValues()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, "Customers/SeparatePageModels/{handler?}/{id?}", new { page = "/Customers/SeparatePageModels/Index" });
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(new { page = "/Customers/SeparatePageModels/Index" }, new { page = "/Customers/SeparatePageModels/Edit", id = "17" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/Customers/SeparatePageModels", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_Match_WithParameters()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, "api/Store/{action}", new { action = "Index", controller = "Store" });
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/api/Store/Index", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_Match_WithMoreParameters()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder,
+ "api/{area}/dosomething/{controller}/{action}",
+ new { action = "Index", controller = "Store", area = "AwesomeCo" });
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(
+ new { action = "Index", controller = "Store" },
+ new { area = "AwesomeCo" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/api/AwesomeCo/dosomething/Store/Index", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_Match_WithDefault()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, "api/Store/{action=Index}", new { action = "Index", controller = "Store" });
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/api/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_Match_WithConstraint()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, "api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" });
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = 5 });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/api/Store/Index/5", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_NoMatch_WithConstraint()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, "api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" });
+ var route = builder.Build();
+
+ var next = new StubRouter();
+ var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = "heyyyy" });
+
+ // Act
+ var path = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.Null(path);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_Match_WithMixedAmbientValues()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" });
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Store" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/api/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_Match_WithQueryString()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" });
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(new { action = "Index", id = 5 }, new { controller = "Store" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/api/Store?id=5", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_RejectedByFirstRoute()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" });
+ MapOutboundEntry(builder, "api2/{controller}", new { action = "Index", controller = "Blog" });
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(new { action = "Index", controller = "Blog" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/api2/Blog", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_ToArea()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" });
+ entry1.Precedence = 2;
+
+ var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" });
+ entry2.Precedence = 1;
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/Help/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_ToArea_PredecedenceReversed()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" });
+ entry1.Precedence = 1;
+
+ var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" });
+ entry2.Precedence = 2;
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/Help/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_ToArea_WithAmbientValues()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" });
+ entry1.Precedence = 2;
+
+ var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" });
+ entry2.Precedence = 1;
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Edit", controller = "Store" },
+ ambientValues: new { area = "Help" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/Help/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_OutOfArea_IgnoresAmbientValue()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" });
+ entry1.Precedence = 2;
+
+ var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" });
+ entry2.Precedence = 1;
+
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(
+ values: new { action = "Edit", controller = "Store" },
+ ambientValues: new { area = "Blog" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ public static IEnumerable<object[]> OptionalParamValues
+ {
+ get
+ {
+ return new object[][]
+ {
+ // defaults
+ // ambient values
+ // values
+ new object[]
+ {
+ "Test/{val1}/{val2}.{val3?}",
+ new {val1 = "someval1", val2 = "someval2", val3 = "someval3a"},
+ new {val3 = "someval3v"},
+ "/Test/someval1/someval2.someval3v",
+ },
+ new object[]
+ {
+ "Test/{val1}/{val2}.{val3?}",
+ new {val3 = "someval3a"},
+ new {val1 = "someval1", val2 = "someval2", val3 = "someval3v" },
+ "/Test/someval1/someval2.someval3v",
+ },
+ new object[]
+ {
+ "Test/{val1}/{val2}.{val3?}",
+ null,
+ new {val1 = "someval1", val2 = "someval2" },
+ "/Test/someval1/someval2",
+ },
+ new object[]
+ {
+ "Test/{val1}.{val2}.{val3}.{val4?}",
+ new {val1 = "someval1", val2 = "someval2" },
+ new {val4 = "someval4", val3 = "someval3" },
+ "/Test/someval1.someval2.someval3.someval4",
+ },
+ new object[]
+ {
+ "Test/{val1}.{val2}.{val3}.{val4?}",
+ new {val1 = "someval1", val2 = "someval2" },
+ new {val3 = "someval3" },
+ "/Test/someval1.someval2.someval3",
+ },
+ new object[]
+ {
+ "Test/.{val2?}",
+ null,
+ new {val2 = "someval2" },
+ "/Test/.someval2",
+ },
+ new object[]
+ {
+ "Test/.{val2?}",
+ null,
+ null,
+ "/Test/",
+ },
+ new object[]
+ {
+ "Test/{val1}.{val2}",
+ new {val1 = "someval1", val2 = "someval2" },
+ new {val3 = "someval3" },
+ "/Test/someval1.someval2?val3=someval3",
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(OptionalParamValues))]
+ public void TreeRouter_GenerateLink_Match_WithOptionalParameters(
+ string template,
+ object ambientValues,
+ object values,
+ string expected)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, template);
+ var route = builder.Build();
+
+ var context = CreateVirtualPathContext(values, ambientValues);
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal(expected, pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public async Task TreeRouter_ReplacesExistingRouteValues_IfNotNull()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapInboundEntry(builder, "Foo/{*path}");
+ var route = builder.Build();
+
+ var context = CreateRouteContext("/Foo/Bar");
+
+ var originalRouteData = context.RouteData;
+ originalRouteData.Values.Add("path", "default");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Equal("Bar", context.RouteData.Values["path"]);
+ }
+
+ [Fact]
+ public async Task TreeRouter_DoesNotReplaceExistingRouteValues_IfNull()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ MapInboundEntry(builder, "Foo/{*path}");
+ var route = builder.Build();
+
+ var context = CreateRouteContext("/Foo/");
+
+ var originalRouteData = context.RouteData;
+ originalRouteData.Values.Add("path", "default");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Equal("default", context.RouteData.Values["path"]);
+ }
+
+ [Fact]
+ public async Task TreeRouter_SnapshotsRouteData()
+ {
+ // Arrange
+ RouteValueDictionary nestedValues = null;
+ List<IRouter> nestedRouters = null;
+
+ var next = new Mock<IRouter>();
+ next
+ .Setup(r => r.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>(c =>
+ {
+ nestedValues = new RouteValueDictionary(c.RouteData.Values);
+ nestedRouters = new List<IRouter>(c.RouteData.Routers);
+ c.Handler = null; // Not a match
+ })
+ .Returns(Task.FromResult(0));
+
+ var builder = CreateBuilder();
+ MapInboundEntry(builder, "api/Store", handler: next.Object);
+ var route = builder.Build();
+
+ var context = CreateRouteContext("/api/Store");
+
+ var routeData = context.RouteData;
+ routeData.Values.Add("action", "Index");
+
+ var originalValues = new RouteValueDictionary(context.RouteData.Values);
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.Equal(originalValues, context.RouteData.Values);
+ Assert.NotEqual(nestedValues, context.RouteData.Values);
+ }
+
+ [Fact]
+ public async Task TreeRouter_SnapshotsRouteData_ResetsWhenNotMatched()
+ {
+ // Arrange
+ RouteValueDictionary nestedValues = null;
+ List<IRouter> nestedRouters = null;
+
+ var next = new Mock<IRouter>();
+ next
+ .Setup(r => r.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>(c =>
+ {
+ nestedValues = new RouteValueDictionary(c.RouteData.Values);
+ nestedRouters = new List<IRouter>(c.RouteData.Routers);
+ c.Handler = null; // Not a match
+ })
+ .Returns(Task.FromResult(0));
+
+ var builder = CreateBuilder();
+ MapInboundEntry(builder, "api/Store", handler: next.Object);
+ var route = builder.Build();
+
+ var context = CreateRouteContext("/api/Store");
+
+ context.RouteData.Values.Add("action", "Index");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotEqual(nestedValues, context.RouteData.Values);
+
+ // The new routedata is a copy
+ Assert.Equal("Index", context.RouteData.Values["action"]);
+ Assert.Equal("Index", nestedValues["action"]);
+ Assert.DoesNotContain(context.RouteData.Values, kvp => kvp.Key == "test_route_group");
+ Assert.Single(nestedValues, kvp => kvp.Key == "test_route_group");
+
+ Assert.Empty(context.RouteData.Routers);
+
+ Assert.Single(nestedRouters);
+ Assert.Equal(next.Object.GetType(), nestedRouters[0].GetType());
+ }
+
+ [Fact]
+ public async Task TreeRouter_SnapshotsRouteData_ResetsWhenThrows()
+ {
+ // Arrange
+ RouteValueDictionary nestedValues = null;
+ List<IRouter> nestedRouters = null;
+
+ var next = new Mock<IRouter>();
+ next
+ .Setup(r => r.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>(c =>
+ {
+ nestedValues = new RouteValueDictionary(c.RouteData.Values);
+ nestedRouters = new List<IRouter>(c.RouteData.Routers);
+ throw new Exception();
+ })
+ .Returns(Task.FromResult(0));
+
+ var builder = CreateBuilder();
+ MapInboundEntry(builder, "api/Store", handler: next.Object);
+ var route = builder.Build();
+
+ var context = CreateRouteContext("/api/Store");
+ context.RouteData.Values.Add("action", "Index");
+
+ // Act
+ await Assert.ThrowsAsync<Exception>(() => route.RouteAsync(context));
+
+ // Assert
+ Assert.NotEqual(nestedValues, context.RouteData.Values);
+
+ Assert.Equal("Index", context.RouteData.Values["action"]);
+ Assert.Equal("Index", nestedValues["action"]);
+ Assert.DoesNotContain(context.RouteData.Values, kvp => kvp.Key == "test_route_group");
+ Assert.Single(nestedValues, kvp => kvp.Key == "test_route_group");
+
+ Assert.Empty(context.RouteData.Routers);
+
+ Assert.Single(nestedRouters);
+ Assert.Equal(next.Object.GetType(), nestedRouters[0].GetType());
+ }
+
+ [Fact]
+ public async Task TreeRouter_SnapshotsRouteData_ResetsBeforeMatchingEachRouteEntry()
+ {
+ // This test replicates a scenario raised as issue https://github.com/aspnet/Routing/issues/394
+ // The RouteValueDictionary entries populated while matching route entries should not be left
+ // in place if the route entry turns out not to match, because that would leak unwanted state
+ // to subsequent route entries and might cause "An element with the key ... already exists"
+ // exceptions.
+
+ // Arrange
+ RouteValueDictionary nestedValues = null;
+ var next = new Mock<IRouter>();
+ next
+ .Setup(r => r.RouteAsync(It.IsAny<RouteContext>()))
+ .Callback<RouteContext>(c =>
+ {
+ nestedValues = new RouteValueDictionary(c.RouteData.Values);
+ c.Handler = NullHandler;
+ })
+ .Returns(Task.CompletedTask);
+
+ var builder = CreateBuilder();
+ MapInboundEntry(builder, "cat_{category1}/prod1_{product}"); // Matches on first segment but not on second
+ MapInboundEntry(builder, "cat_{category2}/prod2_{product}", handler: next.Object);
+ var route = builder.Build();
+
+ var context = CreateRouteContext("/cat_examplecategory/prod2_exampleproduct");
+
+ // Act
+ await route.RouteAsync(context);
+
+ // Assert
+ Assert.NotNull(nestedValues);
+ Assert.Equal("examplecategory", nestedValues["category2"]);
+ Assert.Equal("exampleproduct", nestedValues["product"]);
+ Assert.DoesNotContain(nestedValues, kvp => kvp.Key == "category1");
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_MatchesNullRequiredValue_WithNullRequestValueString()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ var entry = MapOutboundEntry(
+ builder,
+ "Help/Store",
+ requiredValues: new { area = (string)null, action = "Edit", controller = "Store" });
+ var route = builder.Build();
+ var context = CreateVirtualPathContext(new { area = (string)null, action = "Edit", controller = "Store" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/Help/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_MatchesNullRequiredValue_WithEmptyRequestValueString()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ var entry = MapOutboundEntry(
+ builder,
+ "Help/Store",
+ requiredValues: new { area = (string)null, action = "Edit", controller = "Store" });
+ var route = builder.Build();
+ var context = CreateVirtualPathContext(new { area = "", action = "Edit", controller = "Store" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/Help/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_MatchesEmptyStringRequiredValue_WithNullRequestValueString()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ var entry = MapOutboundEntry(
+ builder,
+ "Help/Store",
+ requiredValues: new { foo = "", action = "Edit", controller = "Store" });
+ var route = builder.Build();
+ var context = CreateVirtualPathContext(new { foo = (string)null, action = "Edit", controller = "Store" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/Help/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ [Fact]
+ public void TreeRouter_GenerateLink_MatchesEmptyStringRequiredValue_WithEmptyRequestValueString()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+ var entry = MapOutboundEntry(
+ builder,
+ "Help/Store",
+ requiredValues: new { foo = "", action = "Edit", controller = "Store" });
+ var route = builder.Build();
+ var context = CreateVirtualPathContext(new { foo = "", action = "Edit", controller = "Store" });
+
+ // Act
+ var pathData = route.GetVirtualPath(context);
+
+ // Assert
+ Assert.NotNull(pathData);
+ Assert.Equal("/Help/Store", pathData.VirtualPath);
+ Assert.Same(route, pathData.Router);
+ Assert.Empty(pathData.DataTokens);
+ }
+
+ private static RouteContext CreateRouteContext(string requestPath)
+ {
+ var request = new Mock<HttpRequest>(MockBehavior.Strict);
+ request.SetupGet(r => r.Path).Returns(new PathString(requestPath));
+
+ var context = new Mock<HttpContext>(MockBehavior.Strict);
+ context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory)))
+ .Returns(NullLoggerFactory.Instance);
+
+ context.SetupGet(c => c.Request).Returns(request.Object);
+
+ return new RouteContext(context.Object);
+ }
+
+ private static VirtualPathContext CreateVirtualPathContext(
+ object values,
+ object ambientValues = null,
+ string name = null)
+ {
+ var mockHttpContext = new Mock<HttpContext>();
+ mockHttpContext.Setup(h => h.RequestServices.GetService(typeof(ILoggerFactory)))
+ .Returns(NullLoggerFactory.Instance);
+
+ return new VirtualPathContext(
+ mockHttpContext.Object,
+ new RouteValueDictionary(ambientValues),
+ new RouteValueDictionary(values),
+ name);
+ }
+
+ private static InboundRouteEntry MapInboundEntry(
+ TreeRouteBuilder builder,
+ string template,
+ int order = 0,
+ IRouter handler = null)
+ {
+ var entry = builder.MapInbound(
+ handler ?? new StubRouter(),
+ TemplateParser.Parse(template),
+ routeName: null,
+ order: order);
+
+ // Add a generated 'route group' so we can identify later which entry matched.
+ entry.Defaults["test_route_group"] = CreateRouteGroup(order, template);
+
+ return entry;
+ }
+
+ private static OutboundRouteEntry MapOutboundEntry(
+ TreeRouteBuilder builder,
+ string template,
+ object requiredValues = null,
+ int order = 0,
+ string name = null,
+ IRouter handler = null)
+ {
+ var entry = builder.MapOutbound(
+ handler ?? new StubRouter(),
+ TemplateParser.Parse(template),
+ requiredLinkValues: new RouteValueDictionary(requiredValues),
+ routeName: name,
+ order: order);
+
+ // Add a generated 'route group' so we can identify later which entry matched.
+ entry.Defaults["test_route_group"] = CreateRouteGroup(order, template);
+
+ return entry;
+ }
+
+
+ private static string CreateRouteGroup(int order, string template)
+ {
+ return string.Format("{0}&{1}", order, template);
+ }
+
+ private static DefaultInlineConstraintResolver CreateConstraintResolver()
+ {
+ var options = new RouteOptions();
+ var optionsMock = new Mock<IOptions<RouteOptions>>();
+ optionsMock.SetupGet(o => o.Value).Returns(options);
+
+ return new DefaultInlineConstraintResolver(optionsMock.Object);
+ }
+
+ private static TreeRouteBuilder CreateBuilder()
+ {
+ var objectPoolProvider = new DefaultObjectPoolProvider();
+ var objectPolicy = new UriBuilderContextPooledObjectPolicy();
+ var objectPool = objectPoolProvider.Create<UriBuildingContext>(objectPolicy);
+
+ var constraintResolver = CreateConstraintResolver();
+ var builder = new TreeRouteBuilder(
+ NullLoggerFactory.Instance,
+ objectPool,
+ constraintResolver);
+ return builder;
+ }
+
+ private static TreeRouteBuilder CreateBuilderUsingObsoleteConstructor()
+ {
+ var objectPoolProvider = new DefaultObjectPoolProvider();
+ var objectPolicy = new UriBuilderContextPooledObjectPolicy();
+ var objectPool = objectPoolProvider.Create<UriBuildingContext>(objectPolicy);
+
+ var constraintResolver = CreateConstraintResolver();
+#pragma warning disable CS0618 // Type or member is obsolete
+ var builder = new TreeRouteBuilder(
+ NullLoggerFactory.Instance,
+ UrlEncoder.Default,
+ objectPool,
+ constraintResolver);
+#pragma warning restore CS0618 // Type or member is obsolete
+ return builder;
+ }
+
+ private static TreeRouter CreateTreeRouter(
+ string firstTemplate,
+ string secondTemplate)
+ {
+ var builder = CreateBuilder();
+ MapOutboundEntry(builder, firstTemplate);
+ MapOutboundEntry(builder, secondTemplate);
+ return builder.Build();
+ }
+
+ private class StubRouter : IRouter
+ {
+ public VirtualPathContext GenerationContext { get; set; }
+
+ public RouteContext MatchingContext { get; set; }
+
+ public Func<RouteContext, bool> MatchingDelegate { get; set; }
+
+ public VirtualPathData GetVirtualPath(VirtualPathContext context)
+ {
+ GenerationContext = context;
+ return null;
+ }
+
+ public Task RouteAsync(RouteContext context)
+ {
+ if (MatchingDelegate == null)
+ {
+ context.Handler = NullHandler;
+ }
+ else
+ {
+ context.Handler = MatchingDelegate(context) ? NullHandler : null;
+ }
+
+ return Task.FromResult(true);
+ }
+ }
+ }
+}
diff --git a/src/Routing/version.props b/src/Routing/version.props
new file mode 100644
index 0000000000..669c874829
--- /dev/null
+++ b/src/Routing/version.props
@@ -0,0 +1,12 @@
+<Project>
+ <PropertyGroup>
+ <VersionPrefix>2.1.1</VersionPrefix>
+ <VersionSuffix>rtm</VersionSuffix>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' == 'rtm' ">$(VersionPrefix)</PackageVersion>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' != 'rtm' ">$(VersionPrefix)-$(VersionSuffix)-final</PackageVersion>
+ <BuildNumber Condition="'$(BuildNumber)' == ''">t000</BuildNumber>
+ <FeatureBranchVersionPrefix Condition="'$(FeatureBranchVersionPrefix)' == ''">a-</FeatureBranchVersionPrefix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
+ </PropertyGroup>
+</Project>
diff --git a/src/Security/.gitignore b/src/Security/.gitignore
new file mode 100644
index 0000000000..d5717b3f3f
--- /dev/null
+++ b/src/Security/.gitignore
@@ -0,0 +1,32 @@
+[Oo]bj/
+[Bb]in/
+TestResults/
+.nuget/
+_ReSharper.*/
+packages/
+artifacts/
+PublishProfiles/
+*.user
+*.suo
+*.cache
+*.docstates
+_ReSharper.*
+nuget.exe
+*net45.csproj
+*net451.csproj
+*k10.csproj
+*.psess
+*.vsp
+*.pidb
+*.userprefs
+*DS_Store
+*.ncrunchsolution
+*.*sdf
+*.ipch
+*.sln.ide
+project.lock.json
+.build/
+.testPublish/
+/.vs/
+.vscode/
+global.json
diff --git a/src/Security/Directory.Build.props b/src/Security/Directory.Build.props
new file mode 100644
index 0000000000..f1986d9953
--- /dev/null
+++ b/src/Security/Directory.Build.props
@@ -0,0 +1,20 @@
+<Project>
+ <Import
+ Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))\AspNetCoreSettings.props"
+ Condition=" '$(CI)' != 'true' AND '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))' != '' " />
+
+ <Import Project="version.props" />
+ <Import Project="build\dependencies.props" />
+ <Import Project="build\sources.props" />
+
+ <PropertyGroup>
+ <Product>Microsoft ASP.NET Core</Product>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
+ <RepositoryType>git</RepositoryType>
+ <RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
+ <AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)build\Key.snk</AssemblyOriginatorKeyFile>
+ <SignAssembly>true</SignAssembly>
+ <PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+</Project>
diff --git a/src/Security/Directory.Build.targets b/src/Security/Directory.Build.targets
new file mode 100644
index 0000000000..53b3f6e1da
--- /dev/null
+++ b/src/Security/Directory.Build.targets
@@ -0,0 +1,7 @@
+<Project>
+ <PropertyGroup>
+ <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">$(MicrosoftNETCoreApp20PackageVersion)</RuntimeFrameworkVersion>
+ <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">$(MicrosoftNETCoreApp21PackageVersion)</RuntimeFrameworkVersion>
+ <NETStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard2.0' ">$(NETStandardLibrary20PackageVersion)</NETStandardImplicitPackageVersion>
+ </PropertyGroup>
+</Project>
diff --git a/src/Security/NuGetPackageVerifier.json b/src/Security/NuGetPackageVerifier.json
new file mode 100644
index 0000000000..974eb365c9
--- /dev/null
+++ b/src/Security/NuGetPackageVerifier.json
@@ -0,0 +1,13 @@
+{
+ "adx-nonshipping": {
+ "rules": [],
+ "packages": {
+ "Microsoft.AspNetCore.ChunkingCookieManager.Sources": {}
+ }
+ },
+ "Default": {
+ "rules": [
+ "DefaultCompositeRule"
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/Security/README.md b/src/Security/README.md
new file mode 100644
index 0000000000..e8e64c2936
--- /dev/null
+++ b/src/Security/README.md
@@ -0,0 +1,17 @@
+ASP.NET Security
+========
+
+AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/fujhh8n956v5ohfd/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/Security/branch/dev)
+
+Travis: [![Travis](https://travis-ci.org/aspnet/Security.svg?branch=dev)](https://travis-ci.org/aspnet/Security)
+
+Contains the security and authorization middlewares for ASP.NET Core.
+
+A list of community projects related to authentication and security for ASP.NET Core are listed in the [documentation](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/community).
+
+### Notes
+
+ASP.NET Security will not include Basic Authentication middleware due to its potential insecurity and performance problems. If you host under IIS you can enable it via IIS configuration.
+
+
+This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
diff --git a/src/Security/Security.sln b/src/Security/Security.sln
new file mode 100644
index 0000000000..3df759651b
--- /dev/null
+++ b/src/Security/Security.sln
@@ -0,0 +1,556 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27130.2027
+MinimumVisualStudioVersion = 15.0.26730.03
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4D2B6A51-2F9F-44F5-8131-EA5CAC053652}"
+ ProjectSection(SolutionItems) = preProject
+ src\Directory.Build.props = src\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookieSample", "samples\CookieSample\CookieSample.csproj", "{558C2C2A-AED8-49DE-BB60-D5F8AE06C714}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7BF11F3A-60B6-4796-B504-579C67FFBA34}"
+ ProjectSection(SolutionItems) = preProject
+ test\Directory.Build.props = test\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SocialSample", "samples\SocialSample\SocialSample.csproj", "{8C73D216-332D-41D8-BFD0-45BC4BC36552}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookieSessionSample", "samples\CookieSessionSample\CookieSessionSample.csproj", "{19711880-46DA-4A26-9E0F-9B2E41D27651}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIdConnectSample", "samples\OpenIdConnectSample\OpenIdConnectSample.csproj", "{BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Cookies", "src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.csproj", "{FC152CC4-054B-457E-8D91-389C5DE3C561}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication", "src\Microsoft.AspNetCore.Authentication\Microsoft.AspNetCore.Authentication.csproj", "{BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Facebook", "src\Microsoft.AspNetCore.Authentication.Facebook\Microsoft.AspNetCore.Authentication.Facebook.csproj", "{EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Google", "src\Microsoft.AspNetCore.Authentication.Google\Microsoft.AspNetCore.Authentication.Google.csproj", "{76579C39-B829-490D-B8BE-1BD35FE8412E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.OpenIdConnect", "src\Microsoft.AspNetCore.Authentication.OpenIdConnect\Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj", "{35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.MicrosoftAccount", "src\Microsoft.AspNetCore.Authentication.MicrosoftAccount\Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj", "{ACB45E19-F520-4D0C-8916-B0CEB9C017FE}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Twitter", "src\Microsoft.AspNetCore.Authentication.Twitter\Microsoft.AspNetCore.Authentication.Twitter.csproj", "{0330FFF6-B4B5-42DD-8C99-26A789569000}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.OAuth", "src\Microsoft.AspNetCore.Authentication.OAuth\Microsoft.AspNetCore.Authentication.OAuth.csproj", "{1657C79E-7755-4AEE-9D61-571295B69A30}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Test", "test\Microsoft.AspNetCore.Authentication.Test\Microsoft.AspNetCore.Authentication.Test.csproj", "{8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authorization.Test", "test\Microsoft.AspNetCore.Authorization.Test\Microsoft.AspNetCore.Authorization.Test.csproj", "{7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authorization", "src\Microsoft.AspNetCore.Authorization\Microsoft.AspNetCore.Authorization.csproj", "{6AB3E514-5894-4131-9399-DC7D5284ADDB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.CookiePolicy", "src\Microsoft.AspNetCore.CookiePolicy\Microsoft.AspNetCore.CookiePolicy.csproj", "{86183DC3-02A8-4A68-8B60-71ECEC066E79}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.CookiePolicy.Test", "test\Microsoft.AspNetCore.CookiePolicy.Test\Microsoft.AspNetCore.CookiePolicy.Test.csproj", "{1790E052-646F-4529-B90E-6FEA95520D69}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.JwtBearer", "src\Microsoft.AspNetCore.Authentication.JwtBearer\Microsoft.AspNetCore.Authentication.JwtBearer.csproj", "{2755BFE5-7421-4A31-A644-F817DF5CAA98}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JwtBearerSample", "samples\JwtBearerSample\JwtBearerSample.csproj", "{D399B84F-591B-4E98-92BA-B0F63E7B6957}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Owin.Security.Interop", "src\Microsoft.Owin.Security.Interop\Microsoft.Owin.Security.Interop.csproj", "{A7922DD8-09F1-43E4-938B-CC523EA08898}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Owin.Security.Interop.Test", "test\Microsoft.Owin.Security.Interop.Test\Microsoft.Owin.Security.Interop.Test.csproj", "{A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIdConnect.AzureAdSample", "samples\OpenIdConnect.AzureAdSample\OpenIdConnect.AzureAdSample.csproj", "{3A7AD414-EBDE-4F92-B307-4E8F19B6117E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test", "test\Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test\Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj", "{51563775-C659-4907-9BAF-9995BAB87D01}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{86BD08B1-F978-4F58-9982-2A017807F01C}"
+ ProjectSection(SolutionItems) = preProject
+ build\dependencies.props = build\dependencies.props
+ Directory.Build.props = Directory.Build.props
+ Directory.Build.targets = Directory.Build.targets
+ build\Key.snk = build\Key.snk
+ NuGet.config = NuGet.config
+ build\repo.props = build\repo.props
+ build\sources.props = build\sources.props
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authorization.Policy", "src\Microsoft.AspNetCore.Authorization.Policy\Microsoft.AspNetCore.Authorization.Policy.csproj", "{58194599-F07D-47A3-9DF2-E21A22C5EF9E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookiePolicySample", "samples\CookiePolicySample\CookiePolicySample.csproj", "{24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.WsFederation", "src\Microsoft.AspNetCore.Authentication.WsFederation\Microsoft.AspNetCore.Authentication.WsFederation.csproj", "{B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WsFedSample", "samples\WsFedSample\WsFedSample.csproj", "{5EC2E398-E46A-430D-8E4B-E91C8FC3E800}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|Mixed Platforms = Debug|Mixed Platforms
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|Mixed Platforms = Release|Mixed Platforms
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|x64.Build.0 = Debug|Any CPU
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Any CPU.Build.0 = Release|Any CPU
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|x64.ActiveCfg = Release|Any CPU
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|x64.Build.0 = Release|Any CPU
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|x86.ActiveCfg = Release|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|x64.Build.0 = Debug|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|x64.ActiveCfg = Release|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|x64.Build.0 = Release|Any CPU
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|x86.ActiveCfg = Release|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|x64.Build.0 = Debug|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Any CPU.Build.0 = Release|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|x64.ActiveCfg = Release|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|x64.Build.0 = Release|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|x86.ActiveCfg = Release|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|x64.Build.0 = Debug|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|x86.Build.0 = Debug|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|x64.ActiveCfg = Release|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|x64.Build.0 = Release|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|x86.ActiveCfg = Release|Any CPU
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|x86.Build.0 = Release|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|x64.Build.0 = Debug|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|x86.Build.0 = Debug|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|x64.ActiveCfg = Release|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|x64.Build.0 = Release|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|x86.ActiveCfg = Release|Any CPU
+ {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|x86.Build.0 = Release|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|x64.Build.0 = Debug|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|x86.Build.0 = Debug|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|x64.ActiveCfg = Release|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|x64.Build.0 = Release|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|x86.ActiveCfg = Release|Any CPU
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|x86.Build.0 = Release|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|x64.Build.0 = Debug|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|x86.Build.0 = Debug|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|x64.ActiveCfg = Release|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|x64.Build.0 = Release|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|x86.ActiveCfg = Release|Any CPU
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|x86.Build.0 = Release|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|x64.Build.0 = Debug|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|x86.Build.0 = Debug|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|x64.ActiveCfg = Release|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|x64.Build.0 = Release|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|x86.ActiveCfg = Release|Any CPU
+ {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|x86.Build.0 = Release|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|x64.Build.0 = Debug|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|x86.Build.0 = Debug|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|x64.ActiveCfg = Release|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|x64.Build.0 = Release|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|x86.ActiveCfg = Release|Any CPU
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|x86.Build.0 = Release|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|x64.Build.0 = Debug|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|x86.Build.0 = Debug|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|x64.ActiveCfg = Release|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|x64.Build.0 = Release|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|x86.ActiveCfg = Release|Any CPU
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|x86.Build.0 = Release|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|x64.Build.0 = Debug|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|x86.Build.0 = Debug|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|x64.ActiveCfg = Release|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|x64.Build.0 = Release|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|x86.ActiveCfg = Release|Any CPU
+ {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|x86.Build.0 = Release|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|x64.Build.0 = Debug|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|x86.Build.0 = Debug|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|x64.ActiveCfg = Release|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|x64.Build.0 = Release|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|x86.ActiveCfg = Release|Any CPU
+ {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|x86.Build.0 = Release|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x64.Build.0 = Debug|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x86.Build.0 = Debug|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x64.ActiveCfg = Release|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x64.Build.0 = Release|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x86.ActiveCfg = Release|Any CPU
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x86.Build.0 = Release|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|x64.Build.0 = Debug|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|x86.Build.0 = Debug|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|x64.ActiveCfg = Release|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|x64.Build.0 = Release|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|x86.ActiveCfg = Release|Any CPU
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|x86.Build.0 = Release|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|x64.Build.0 = Debug|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|x86.Build.0 = Debug|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|x64.ActiveCfg = Release|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|x64.Build.0 = Release|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|x86.ActiveCfg = Release|Any CPU
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|x86.Build.0 = Release|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|x64.Build.0 = Debug|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|x86.Build.0 = Debug|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|Any CPU.Build.0 = Release|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|x64.ActiveCfg = Release|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|x64.Build.0 = Release|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|x86.ActiveCfg = Release|Any CPU
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|x86.Build.0 = Release|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|x64.Build.0 = Debug|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|x86.Build.0 = Debug|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Release|x64.ActiveCfg = Release|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Release|x64.Build.0 = Release|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Release|x86.ActiveCfg = Release|Any CPU
+ {1790E052-646F-4529-B90E-6FEA95520D69}.Release|x86.Build.0 = Release|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|x64.Build.0 = Debug|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|x86.Build.0 = Debug|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|x64.ActiveCfg = Release|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|x64.Build.0 = Release|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|x86.ActiveCfg = Release|Any CPU
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|x86.Build.0 = Release|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|x64.Build.0 = Debug|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|x86.Build.0 = Debug|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|x64.ActiveCfg = Release|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|x64.Build.0 = Release|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|x86.ActiveCfg = Release|Any CPU
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|x86.Build.0 = Release|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|x64.Build.0 = Debug|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|x86.Build.0 = Debug|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|x64.ActiveCfg = Release|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|x64.Build.0 = Release|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|x86.ActiveCfg = Release|Any CPU
+ {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|x86.Build.0 = Release|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|x64.Build.0 = Debug|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|x86.Build.0 = Debug|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|x64.ActiveCfg = Release|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|x64.Build.0 = Release|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|x86.ActiveCfg = Release|Any CPU
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|x86.Build.0 = Release|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|x64.Build.0 = Debug|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|x86.Build.0 = Debug|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|x64.ActiveCfg = Release|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|x64.Build.0 = Release|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|x86.ActiveCfg = Release|Any CPU
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|x86.Build.0 = Release|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|x64.Build.0 = Debug|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|x86.Build.0 = Debug|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Release|Any CPU.Build.0 = Release|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Release|x64.ActiveCfg = Release|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Release|x64.Build.0 = Release|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Release|x86.ActiveCfg = Release|Any CPU
+ {51563775-C659-4907-9BAF-9995BAB87D01}.Release|x86.Build.0 = Release|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|x64.Build.0 = Debug|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|x86.Build.0 = Debug|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|x64.ActiveCfg = Release|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|x64.Build.0 = Release|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|x86.ActiveCfg = Release|Any CPU
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|x86.Build.0 = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|x64.Build.0 = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|x86.Build.0 = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|x64.ActiveCfg = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|x64.Build.0 = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|x86.ActiveCfg = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|x86.Build.0 = Release|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|x64.Build.0 = Debug|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|x86.Build.0 = Debug|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|x64.ActiveCfg = Release|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|x64.Build.0 = Release|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|x86.ActiveCfg = Release|Any CPU
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|x86.Build.0 = Release|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|x64.Build.0 = Debug|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|x86.Build.0 = Debug|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|x64.ActiveCfg = Release|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|x64.Build.0 = Release|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|x86.ActiveCfg = Release|Any CPU
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {558C2C2A-AED8-49DE-BB60-D5F8AE06C714} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
+ {8C73D216-332D-41D8-BFD0-45BC4BC36552} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
+ {19711880-46DA-4A26-9E0F-9B2E41D27651} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
+ {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
+ {FC152CC4-054B-457E-8D91-389C5DE3C561} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {76579C39-B829-490D-B8BE-1BD35FE8412E} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {ACB45E19-F520-4D0C-8916-B0CEB9C017FE} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {0330FFF6-B4B5-42DD-8C99-26A789569000} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {1657C79E-7755-4AEE-9D61-571295B69A30} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B} = {7BF11F3A-60B6-4796-B504-579C67FFBA34}
+ {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2} = {7BF11F3A-60B6-4796-B504-579C67FFBA34}
+ {6AB3E514-5894-4131-9399-DC7D5284ADDB} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {86183DC3-02A8-4A68-8B60-71ECEC066E79} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {1790E052-646F-4529-B90E-6FEA95520D69} = {7BF11F3A-60B6-4796-B504-579C67FFBA34}
+ {2755BFE5-7421-4A31-A644-F817DF5CAA98} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {D399B84F-591B-4E98-92BA-B0F63E7B6957} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
+ {A7922DD8-09F1-43E4-938B-CC523EA08898} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24} = {7BF11F3A-60B6-4796-B504-579C67FFBA34}
+ {3A7AD414-EBDE-4F92-B307-4E8F19B6117E} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
+ {51563775-C659-4907-9BAF-9995BAB87D01} = {7BF11F3A-60B6-4796-B504-579C67FFBA34}
+ {58194599-F07D-47A3-9DF2-E21A22C5EF9E} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
+ {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {5EC2E398-E46A-430D-8E4B-E91C8FC3E800} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {ABF8089E-43D0-4010-84A7-7A9DCFE49357}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Security/build/Key.snk b/src/Security/build/Key.snk
new file mode 100644
index 0000000000..e10e4889c1
--- /dev/null
+++ b/src/Security/build/Key.snk
Binary files differ
diff --git a/src/Security/build/dependencies.props b/src/Security/build/dependencies.props
new file mode 100644
index 0000000000..828f9c7ab2
--- /dev/null
+++ b/src/Security/build/dependencies.props
@@ -0,0 +1,58 @@
+<Project>
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ </PropertyGroup>
+
+ <!-- These package versions may be overridden or updated by automation. -->
+ <PropertyGroup Label="Package Versions: Auto">
+ <InternalAspNetCoreSdkPackageVersion>2.1.3-rtm-15802</InternalAspNetCoreSdkPackageVersion>
+ <MicrosoftIdentityModelClientsActiveDirectoryPackageVersion>3.14.2</MicrosoftIdentityModelClientsActiveDirectoryPackageVersion>
+ <MicrosoftIdentityModelProtocolsOpenIdConnectPackageVersion>5.2.0</MicrosoftIdentityModelProtocolsOpenIdConnectPackageVersion>
+ <MicrosoftIdentityModelProtocolsWsFederationPackageVersion>5.2.0</MicrosoftIdentityModelProtocolsWsFederationPackageVersion>
+ <MicrosoftNETCoreApp20PackageVersion>2.0.0</MicrosoftNETCoreApp20PackageVersion>
+ <MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion>
+ <MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
+ <MicrosoftOwinSecurityCookiesPackageVersion>3.0.1</MicrosoftOwinSecurityCookiesPackageVersion>
+ <MicrosoftOwinSecurityPackageVersion>3.0.1</MicrosoftOwinSecurityPackageVersion>
+ <MicrosoftOwinTestingPackageVersion>3.0.1</MicrosoftOwinTestingPackageVersion>
+ <NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
+ <NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
+ <SystemIdentityModelTokensJwtPackageVersion>5.2.0</SystemIdentityModelTokensJwtPackageVersion>
+ <XunitAnalyzersPackageVersion>0.8.0</XunitAnalyzersPackageVersion>
+ <XunitPackageVersion>2.3.1</XunitPackageVersion>
+ <XunitRunnerVisualStudioPackageVersion>2.4.0-beta.1.build3945</XunitRunnerVisualStudioPackageVersion>
+ </PropertyGroup>
+
+ <!-- This may import a generated file which may override the variables above. -->
+ <Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
+
+ <!-- These are package versions that should not be overridden or updated by automation. -->
+ <PropertyGroup Label="Package Versions: Pinned">
+ <MicrosoftAspNetCoreAuthenticationAbstractionsPackageVersion>2.1.1</MicrosoftAspNetCoreAuthenticationAbstractionsPackageVersion>
+ <MicrosoftAspNetCoreAuthenticationCorePackageVersion>2.1.1</MicrosoftAspNetCoreAuthenticationCorePackageVersion>
+ <MicrosoftAspNetCoreDataProtectionExtensionsPackageVersion>2.1.1</MicrosoftAspNetCoreDataProtectionExtensionsPackageVersion>
+ <MicrosoftAspNetCoreDataProtectionPackageVersion>2.1.1</MicrosoftAspNetCoreDataProtectionPackageVersion>
+ <MicrosoftAspNetCoreDiagnosticsPackageVersion>2.1.1</MicrosoftAspNetCoreDiagnosticsPackageVersion>
+ <MicrosoftAspNetCoreHostingPackageVersion>2.1.1</MicrosoftAspNetCoreHostingPackageVersion>
+ <MicrosoftAspNetCoreHttpExtensionsPackageVersion>2.1.1</MicrosoftAspNetCoreHttpExtensionsPackageVersion>
+ <MicrosoftAspNetCoreHttpPackageVersion>2.1.1</MicrosoftAspNetCoreHttpPackageVersion>
+ <MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.1.1</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
+ <MicrosoftAspNetCoreServerKestrelHttpsPackageVersion>2.1.2</MicrosoftAspNetCoreServerKestrelHttpsPackageVersion>
+ <MicrosoftAspNetCoreServerKestrelPackageVersion>2.1.2</MicrosoftAspNetCoreServerKestrelPackageVersion>
+ <MicrosoftAspNetCoreStaticFilesPackageVersion>2.1.1</MicrosoftAspNetCoreStaticFilesPackageVersion>
+ <MicrosoftAspNetCoreTestHostPackageVersion>2.1.1</MicrosoftAspNetCoreTestHostPackageVersion>
+ <MicrosoftAspNetCoreTestingPackageVersion>2.1.0</MicrosoftAspNetCoreTestingPackageVersion>
+ <MicrosoftExtensionsCachingMemoryPackageVersion>2.1.1</MicrosoftExtensionsCachingMemoryPackageVersion>
+ <MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>2.1.1</MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>
+ <MicrosoftExtensionsConfigurationUserSecretsPackageVersion>2.1.1</MicrosoftExtensionsConfigurationUserSecretsPackageVersion>
+ <MicrosoftExtensionsDependencyInjectionPackageVersion>2.1.1</MicrosoftExtensionsDependencyInjectionPackageVersion>
+ <MicrosoftExtensionsFileProvidersEmbeddedPackageVersion>2.1.1</MicrosoftExtensionsFileProvidersEmbeddedPackageVersion>
+ <MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
+ <MicrosoftExtensionsLoggingConsolePackageVersion>2.1.1</MicrosoftExtensionsLoggingConsolePackageVersion>
+ <MicrosoftExtensionsLoggingDebugPackageVersion>2.1.1</MicrosoftExtensionsLoggingDebugPackageVersion>
+ <MicrosoftExtensionsLoggingPackageVersion>2.1.1</MicrosoftExtensionsLoggingPackageVersion>
+ <MicrosoftExtensionsOptionsPackageVersion>2.1.1</MicrosoftExtensionsOptionsPackageVersion>
+ <MicrosoftExtensionsSecurityHelperSourcesPackageVersion>2.1.1</MicrosoftExtensionsSecurityHelperSourcesPackageVersion>
+ <MicrosoftExtensionsWebEncodersPackageVersion>2.1.1</MicrosoftExtensionsWebEncodersPackageVersion>
+ </PropertyGroup>
+</Project> \ No newline at end of file
diff --git a/src/Security/build/repo.props b/src/Security/build/repo.props
new file mode 100644
index 0000000000..a4f86fb2f6
--- /dev/null
+++ b/src/Security/build/repo.props
@@ -0,0 +1,18 @@
+<Project>
+ <Import Project="dependencies.props" />
+
+ <ItemGroup>
+ <ExcludeFromTest Include="$(RepositoryRoot)test\Microsoft.Owin.Security.Interop.Test\*.csproj" Condition="'$(OS)' != 'Windows_NT'" />
+ </ItemGroup>
+ <PropertyGroup>
+ <!-- These properties are use by the automation that updates dependencies.props -->
+ <LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
+ <LineupPackageVersion>2.1.0-rc1-*</LineupPackageVersion>
+ <LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp20PackageVersion)" />
+ <DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/Security/build/sources.props b/src/Security/build/sources.props
new file mode 100644
index 0000000000..9215df9751
--- /dev/null
+++ b/src/Security/build/sources.props
@@ -0,0 +1,17 @@
+<Project>
+ <Import Project="$(DotNetRestoreSourcePropsPath)" Condition="'$(DotNetRestoreSourcePropsPath)' != ''"/>
+
+ <PropertyGroup Label="RestoreSources">
+ <RestoreSources>$(DotNetRestoreSources)</RestoreSources>
+ <RestoreSources Condition="'$(DotNetBuildOffline)' != 'true' AND '$(AspNetUniverseBuildOffline)' != 'true' ">
+ $(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>
+ <RestoreSources Condition="'$(DotNetBuildOffline)' != 'true'">
+ $(RestoreSources);
+ https://api.nuget.org/v3/index.json;
+ </RestoreSources>
+ </PropertyGroup>
+</Project>
diff --git a/src/Security/samples/CookiePolicySample/CookiePolicySample.csproj b/src/Security/samples/CookiePolicySample/CookiePolicySample.csproj
new file mode 100644
index 0000000000..fb2e7d9172
--- /dev/null
+++ b/src/Security/samples/CookiePolicySample/CookiePolicySample.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFrameworks>net461;netcoreapp2.1</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.CookiePolicy\Microsoft.AspNetCore.CookiePolicy.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/samples/CookiePolicySample/Program.cs b/src/Security/samples/CookiePolicySample/Program.cs
new file mode 100644
index 0000000000..3fc09a3db2
--- /dev/null
+++ b/src/Security/samples/CookiePolicySample/Program.cs
@@ -0,0 +1,26 @@
+using System.IO;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace CookiePolicySample
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging(factory =>
+ {
+ factory.AddConsole();
+ factory.AddFilter("Microsoft", LogLevel.Trace);
+ })
+ .UseKestrel()
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseStartup<Startup>()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/Security/samples/CookiePolicySample/Properties/launchSettings.json b/src/Security/samples/CookiePolicySample/Properties/launchSettings.json
new file mode 100644
index 0000000000..38ca6fc37f
--- /dev/null
+++ b/src/Security/samples/CookiePolicySample/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:1788/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "CookieSample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:12345",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/samples/CookiePolicySample/Startup.cs b/src/Security/samples/CookiePolicySample/Startup.cs
new file mode 100644
index 0000000000..7ce9c2d2d2
--- /dev/null
+++ b/src/Security/samples/CookiePolicySample/Startup.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+
+namespace CookiePolicySample
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
+ .AddCookie();
+ services.Configure<CookiePolicyOptions>(options =>
+ {
+ options.CheckConsentNeeded = context => context.Request.PathBase.Equals("/NeedsConsent");
+
+ options.OnAppendCookie = context => { };
+ });
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseCookiePolicy();
+ app.UseAuthentication();
+
+ app.Map("/NeedsConsent", NestedApp);
+ app.Map("/NeedsNoConsent", NestedApp);
+ NestedApp(app);
+ }
+
+ private void NestedApp(IApplicationBuilder app)
+ {
+ app.Run(async context =>
+ {
+ var path = context.Request.Path;
+ switch (path)
+ {
+ case "/Login":
+ var user = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "bob") },
+ CookieAuthenticationDefaults.AuthenticationScheme));
+ await context.SignInAsync(user);
+ break;
+ case "/Logout":
+ await context.SignOutAsync();
+ break;
+ case "/CreateTempCookie":
+ context.Response.Cookies.Append("Temp", "1");
+ break;
+ case "/RemoveTempCookie":
+ context.Response.Cookies.Delete("Temp");
+ break;
+ case "/GrantConsent":
+ context.Features.Get<ITrackingConsentFeature>().GrantConsent();
+ break;
+ case "/WithdrawConsent":
+ context.Features.Get<ITrackingConsentFeature>().WithdrawConsent();
+ break;
+ }
+
+ // TODO: Debug log when cookie is suppressed
+
+ await HomePage(context);
+ });
+ }
+
+ private async Task HomePage(HttpContext context)
+ {
+ var response = context.Response;
+ var cookies = context.Request.Cookies;
+ response.ContentType = "text/html";
+ await response.WriteAsync("<html><body>\r\n");
+
+ await response.WriteAsync($"<a href=\"{context.Request.PathBase}/\">Home</a><br>\r\n");
+ await response.WriteAsync($"<a href=\"{context.Request.PathBase}/Login\">Login</a><br>\r\n");
+ await response.WriteAsync($"<a href=\"{context.Request.PathBase}/Logout\">Logout</a><br>\r\n");
+ await response.WriteAsync($"<a href=\"{context.Request.PathBase}/CreateTempCookie\">Create Temp Cookie</a><br>\r\n");
+ await response.WriteAsync($"<a href=\"{context.Request.PathBase}/RemoveTempCookie\">Remove Temp Cookie</a><br>\r\n");
+ await response.WriteAsync($"<a href=\"{context.Request.PathBase}/GrantConsent\">Grant Consent</a><br>\r\n");
+ await response.WriteAsync($"<a href=\"{context.Request.PathBase}/WithdrawConsent\">Withdraw Consent</a><br>\r\n");
+ await response.WriteAsync("<br>\r\n");
+ await response.WriteAsync($"<a href=\"/NeedsConsent{context.Request.Path}\">Needs Consent</a><br>\r\n");
+ await response.WriteAsync($"<a href=\"/NeedsNoConsent{context.Request.Path}\">Needs No Consent</a><br>\r\n");
+ await response.WriteAsync("<br>\r\n");
+
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ await response.WriteAsync($"Consent: <br>\r\n");
+ await response.WriteAsync($" - IsNeeded: {feature.IsConsentNeeded} <br>\r\n");
+ await response.WriteAsync($" - Has: {feature.HasConsent} <br>\r\n");
+ await response.WriteAsync($" - Can Track: {feature.CanTrack} <br>\r\n");
+ await response.WriteAsync("<br>\r\n");
+
+ await response.WriteAsync($"{cookies.Count} Request Cookies:<br>\r\n");
+ foreach (var cookie in cookies)
+ {
+ await response.WriteAsync($" - {cookie.Key} = {cookie.Value} <br>\r\n");
+ }
+ await response.WriteAsync("<br>\r\n");
+
+ var responseCookies = response.Headers[HeaderNames.SetCookie];
+ await response.WriteAsync($"{responseCookies.Count} Response Cookies:<br>\r\n");
+ foreach (var cookie in responseCookies)
+ {
+ await response.WriteAsync($" - {cookie} <br>\r\n");
+ }
+
+ await response.WriteAsync("</body></html>");
+ }
+ }
+}
diff --git a/src/Security/samples/CookieSample/CookieSample.csproj b/src/Security/samples/CookieSample/CookieSample.csproj
new file mode 100644
index 0000000000..193137b861
--- /dev/null
+++ b/src/Security/samples/CookieSample/CookieSample.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFrameworks>net461;netcoreapp2.1</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(MicrosoftAspNetCoreHostingPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="$(MicrosoftAspNetCoreDataProtectionPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/samples/CookieSample/Program.cs b/src/Security/samples/CookieSample/Program.cs
new file mode 100644
index 0000000000..3f40d3194b
--- /dev/null
+++ b/src/Security/samples/CookieSample/Program.cs
@@ -0,0 +1,26 @@
+using System.IO;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace CookieSample
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging(factory =>
+ {
+ factory.AddConsole();
+ factory.AddFilter("Console", level => level >= LogLevel.Information);
+ })
+ .UseKestrel()
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseStartup<Startup>()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/Security/samples/CookieSample/Properties/launchSettings.json b/src/Security/samples/CookieSample/Properties/launchSettings.json
new file mode 100644
index 0000000000..38ca6fc37f
--- /dev/null
+++ b/src/Security/samples/CookieSample/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:1788/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "CookieSample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:12345",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/samples/CookieSample/Startup.cs b/src/Security/samples/CookieSample/Startup.cs
new file mode 100644
index 0000000000..a91791070a
--- /dev/null
+++ b/src/Security/samples/CookieSample/Startup.cs
@@ -0,0 +1,45 @@
+using System.Linq;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CookieSample
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ // This can be removed after https://github.com/aspnet/IISIntegration/issues/371
+ services.AddAuthentication(options =>
+ {
+ options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ }).AddCookie();
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseAuthentication();
+
+ app.Run(async context =>
+ {
+ if (!context.User.Identities.Any(identity => identity.IsAuthenticated))
+ {
+ var user = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "bob") }, CookieAuthenticationDefaults.AuthenticationScheme));
+ await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, user);
+
+ context.Response.ContentType = "text/plain";
+ await context.Response.WriteAsync("Hello First timer");
+ return;
+ }
+
+ context.Response.ContentType = "text/plain";
+ await context.Response.WriteAsync("Hello old timer");
+ });
+ }
+ }
+}
diff --git a/src/Security/samples/CookieSessionSample/CookieSessionSample.csproj b/src/Security/samples/CookieSessionSample/CookieSessionSample.csproj
new file mode 100644
index 0000000000..6241edd667
--- /dev/null
+++ b/src/Security/samples/CookieSessionSample/CookieSessionSample.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFrameworks>net461;netcoreapp2.1</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="$(MicrosoftAspNetCoreDataProtectionPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(MicrosoftExtensionsCachingMemoryPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/samples/CookieSessionSample/MemoryCacheTicketStore.cs b/src/Security/samples/CookieSessionSample/MemoryCacheTicketStore.cs
new file mode 100644
index 0000000000..ebb660361b
--- /dev/null
+++ b/src/Security/samples/CookieSessionSample/MemoryCacheTicketStore.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace CookieSessionSample
+{
+ public class MemoryCacheTicketStore : ITicketStore
+ {
+ private const string KeyPrefix = "AuthSessionStore-";
+ private IMemoryCache _cache;
+
+ public MemoryCacheTicketStore()
+ {
+ _cache = new MemoryCache(new MemoryCacheOptions());
+ }
+
+ public async Task<string> StoreAsync(AuthenticationTicket ticket)
+ {
+ var guid = Guid.NewGuid();
+ var key = KeyPrefix + guid.ToString();
+ await RenewAsync(key, ticket);
+ return key;
+ }
+
+ public Task RenewAsync(string key, AuthenticationTicket ticket)
+ {
+ var options = new MemoryCacheEntryOptions();
+ var expiresUtc = ticket.Properties.ExpiresUtc;
+ if (expiresUtc.HasValue)
+ {
+ options.SetAbsoluteExpiration(expiresUtc.Value);
+ }
+ options.SetSlidingExpiration(TimeSpan.FromHours(1)); // TODO: configurable.
+
+ _cache.Set(key, ticket, options);
+
+ return Task.FromResult(0);
+ }
+
+ public Task<AuthenticationTicket> RetrieveAsync(string key)
+ {
+ AuthenticationTicket ticket;
+ _cache.TryGetValue(key, out ticket);
+ return Task.FromResult(ticket);
+ }
+
+ public Task RemoveAsync(string key)
+ {
+ _cache.Remove(key);
+ return Task.FromResult(0);
+ }
+ }
+}
diff --git a/src/Security/samples/CookieSessionSample/Program.cs b/src/Security/samples/CookieSessionSample/Program.cs
new file mode 100644
index 0000000000..1a19850e64
--- /dev/null
+++ b/src/Security/samples/CookieSessionSample/Program.cs
@@ -0,0 +1,26 @@
+using System.IO;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace CookieSessionSample
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging(factory =>
+ {
+ factory.AddConsole();
+ factory.AddFilter("Console", level => level >= LogLevel.Information);
+ })
+ .UseKestrel()
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseStartup<Startup>()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/Security/samples/CookieSessionSample/Properties/launchSettings.json b/src/Security/samples/CookieSessionSample/Properties/launchSettings.json
new file mode 100644
index 0000000000..25de3e478e
--- /dev/null
+++ b/src/Security/samples/CookieSessionSample/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:1790/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "CookieSessionSample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:12345",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/samples/CookieSessionSample/Startup.cs b/src/Security/samples/CookieSessionSample/Startup.cs
new file mode 100644
index 0000000000..f7b8f2bb88
--- /dev/null
+++ b/src/Security/samples/CookieSessionSample/Startup.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CookieSessionSample
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ // This can be removed after https://github.com/aspnet/IISIntegration/issues/371
+ services.AddAuthentication(options =>
+ {
+ options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ }).AddCookie(o => o.SessionStore = new MemoryCacheTicketStore());
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseAuthentication();
+
+ app.Run(async context =>
+ {
+ if (!context.User.Identities.Any(identity => identity.IsAuthenticated))
+ {
+ // Make a large identity
+ var claims = new List<Claim>(1001);
+ claims.Add(new Claim(ClaimTypes.Name, "bob"));
+ for (int i = 0; i < 1000; i++)
+ {
+ claims.Add(new Claim(ClaimTypes.Role, "SomeRandomGroup" + i, ClaimValueTypes.String, "IssuedByBob", "OriginalIssuerJoe"));
+ }
+
+ await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
+ new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme)));
+
+ context.Response.ContentType = "text/plain";
+ await context.Response.WriteAsync("Hello First timer");
+ return;
+ }
+
+ context.Response.ContentType = "text/plain";
+ await context.Response.WriteAsync("Hello old timer");
+ });
+ }
+ }
+}
diff --git a/src/Security/samples/JwtBearerSample/JwtBearerSample.csproj b/src/Security/samples/JwtBearerSample/JwtBearerSample.csproj
new file mode 100644
index 0000000000..84b436581a
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/JwtBearerSample.csproj
@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFrameworks>net461;netcoreapp2.1</TargetFrameworks>
+ <UserSecretsId>aspnet5-JwtBearerSample-20151210102827</UserSecretsId>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.JwtBearer\Microsoft.AspNetCore.Authentication.JwtBearer.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(MicrosoftAspNetCoreStaticFilesPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="$(MicrosoftExtensionsConfigurationUserSecretsPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/samples/JwtBearerSample/Program.cs b/src/Security/samples/JwtBearerSample/Program.cs
new file mode 100644
index 0000000000..44d2fe0c4f
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/Program.cs
@@ -0,0 +1,21 @@
+using System.IO;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+
+namespace JwtBearerSample
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .UseKestrel()
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseStartup<Startup>()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/Security/samples/JwtBearerSample/Properties/launchSettings.json b/src/Security/samples/JwtBearerSample/Properties/launchSettings.json
new file mode 100644
index 0000000000..6922375c98
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:42023",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "JwtBearer": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:42023",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/samples/JwtBearerSample/Startup.cs b/src/Security/samples/JwtBearerSample/Startup.cs
new file mode 100644
index 0000000000..8c4a63cad6
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/Startup.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.ExceptionServices;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+using Newtonsoft.Json.Linq;
+
+namespace JwtBearerSample
+{
+ public class Startup
+ {
+ public Startup(IHostingEnvironment env)
+ {
+ Environment = env;
+
+ var builder = new ConfigurationBuilder()
+ .SetBasePath(env.ContentRootPath);
+
+ if (env.IsDevelopment())
+ {
+ // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
+ builder.AddUserSecrets<Startup>();
+ }
+
+ builder.AddEnvironmentVariables();
+ Configuration = builder.Build();
+ }
+
+ public IConfiguration Configuration { get; set; }
+
+ public IHostingEnvironment Environment { get; set; }
+
+ // Shared between users in memory
+ public IList<Todo> Todos { get; } = new List<Todo>();
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddJwtBearer(o =>
+ {
+ // You also need to update /wwwroot/app/scripts/app.js
+ o.Authority = Configuration["oidc:authority"];
+ o.Audience = Configuration["oidc:clientid"];
+ });
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseDeveloperExceptionPage();
+
+ app.UseDefaultFiles();
+ app.UseStaticFiles();
+
+ app.UseAuthentication();
+
+ // [Authorize] would usually handle this
+ app.Use(async (context, next) =>
+ {
+ // Use this if there are multiple authentication schemes
+ var authResult = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
+ if (authResult.Succeeded && authResult.Principal.Identity.IsAuthenticated)
+ {
+ await next();
+ }
+ else if (authResult.Failure != null)
+ {
+ // Rethrow, let the exception page handle it.
+ ExceptionDispatchInfo.Capture(authResult.Failure).Throw();
+ }
+ else
+ {
+ await context.ChallengeAsync();
+ }
+ });
+
+ // MVC would usually handle this:
+ app.Map("/api/TodoList", todoApp =>
+ {
+ todoApp.Run(async context =>
+ {
+ var response = context.Response;
+ if (context.Request.Method.Equals("POST", System.StringComparison.OrdinalIgnoreCase))
+ {
+ var reader = new StreamReader(context.Request.Body);
+ var body = await reader.ReadToEndAsync();
+ var obj = JObject.Parse(body);
+ var todo = new Todo() { Description = obj["Description"].Value<string>(), Owner = context.User.Identity.Name };
+ Todos.Add(todo);
+ }
+ else
+ {
+ response.ContentType = "application/json";
+ response.Headers[HeaderNames.CacheControl] = "no-cache";
+ var json = JToken.FromObject(Todos);
+ await response.WriteAsync(json.ToString());
+ }
+ });
+ });
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/samples/JwtBearerSample/Todo.cs b/src/Security/samples/JwtBearerSample/Todo.cs
new file mode 100644
index 0000000000..3ddf40414e
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/Todo.cs
@@ -0,0 +1,8 @@
+namespace JwtBearerSample
+{
+ public class Todo
+ {
+ public string Description { get; set; }
+ public string Owner { get; set; }
+ }
+}
diff --git a/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/app.js b/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/app.js
new file mode 100644
index 0000000000..b45cb760b2
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/app.js
@@ -0,0 +1,28 @@
+'use strict';
+angular.module('todoApp', ['ngRoute','AdalAngular'])
+.config(['$routeProvider', '$httpProvider', 'adalAuthenticationServiceProvider', function ($routeProvider, $httpProvider, adalProvider) {
+
+ $routeProvider.when("/Home", {
+ controller: "homeCtrl",
+ templateUrl: "/App/Views/Home.html",
+ }).when("/TodoList", {
+ controller: "todoListCtrl",
+ templateUrl: "/App/Views/TodoList.html",
+ requireADLogin: true,
+ }).when("/UserData", {
+ controller: "userDataCtrl",
+ templateUrl: "/App/Views/UserData.html",
+ }).otherwise({ redirectTo: "/Home" });
+
+ adalProvider.init(
+ {
+ instance: 'https://login.microsoftonline.com/',
+ tenant: 'tratcheroutlook.onmicrosoft.com',
+ clientId: '63a87a83-64b9-4ac1-b2c5-092126f8474f',
+ extraQueryParameter: 'nux=1',
+ // cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.
+ },
+ $httpProvider
+ );
+
+}]);
diff --git a/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/homeCtrl.js b/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/homeCtrl.js
new file mode 100644
index 0000000000..09a3beae53
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/homeCtrl.js
@@ -0,0 +1,13 @@
+'use strict';
+angular.module('todoApp')
+.controller('homeCtrl', ['$scope', 'adalAuthenticationService','$location', function ($scope, adalService, $location) {
+ $scope.login = function () {
+ adalService.login();
+ };
+ $scope.logout = function () {
+ adalService.logOut();
+ };
+ $scope.isActive = function (viewLocation) {
+ return viewLocation === $location.path();
+ };
+}]); \ No newline at end of file
diff --git a/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/indexCtrl.js b/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/indexCtrl.js
new file mode 100644
index 0000000000..baaa4b2421
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/indexCtrl.js
@@ -0,0 +1,5 @@
+'use strict';
+angular.module('todoApp')
+.controller('indexCtrl', ['$scope', 'adalAuthenticationService', function ($scope, adalService) {
+
+}]); \ No newline at end of file
diff --git a/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/todoListCtrl.js b/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/todoListCtrl.js
new file mode 100644
index 0000000000..7dbd4f29ca
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/todoListCtrl.js
@@ -0,0 +1,71 @@
+'use strict';
+angular.module('todoApp')
+.controller('todoListCtrl', ['$scope', '$location', 'todoListSvc', 'adalAuthenticationService', function ($scope, $location, todoListSvc, adalService) {
+ $scope.error = "";
+ $scope.loadingMessage = "Loading...";
+ $scope.todoList = null;
+ $scope.editingInProgress = false;
+ $scope.newTodoCaption = "";
+
+
+ $scope.editInProgressTodo = {
+ Description: "",
+ ID: 0
+ };
+
+
+
+ $scope.editSwitch = function (todo) {
+ todo.edit = !todo.edit;
+ if (todo.edit) {
+ $scope.editInProgressTodo.Description = todo.Description;
+ $scope.editInProgressTodo.ID = todo.ID;
+ $scope.editingInProgress = true;
+ } else {
+ $scope.editingInProgress = false;
+ }
+ };
+
+ $scope.populate = function () {
+ todoListSvc.getItems().success(function (results) {
+ $scope.todoList = results;
+ $scope.loadingMessage = "";
+ }).error(function (err) {
+ $scope.error = err;
+ $scope.loadingMessage = "";
+ })
+ };
+ $scope.delete = function (id) {
+ todoListSvc.deleteItem(id).success(function (results) {
+ $scope.loadingMessage = "";
+ $scope.populate();
+ }).error(function (err) {
+ $scope.error = err;
+ $scope.loadingMessage = "";
+ })
+ };
+ $scope.update = function (todo) {
+ todoListSvc.putItem($scope.editInProgressTodo).success(function (results) {
+ $scope.loadingMsg = "";
+ $scope.populate();
+ $scope.editSwitch(todo);
+ }).error(function (err) {
+ $scope.error = err;
+ $scope.loadingMessage = "";
+ })
+ };
+ $scope.add = function () {
+
+ todoListSvc.postItem({
+ 'Description': $scope.newTodoCaption,
+ 'Owner': adalService.userInfo.userName
+ }).success(function (results) {
+ $scope.loadingMsg = "";
+ $scope.newTodoCaption = "";
+ $scope.populate();
+ }).error(function (err) {
+ $scope.error = err;
+ $scope.loadingMsg = "";
+ })
+ };
+}]); \ No newline at end of file
diff --git a/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/todoListSvc.js b/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/todoListSvc.js
new file mode 100644
index 0000000000..87d0641084
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/todoListSvc.js
@@ -0,0 +1,24 @@
+'use strict';
+angular.module('todoApp')
+.factory('todoListSvc', ['$http', function ($http) {
+ return {
+ getItems : function(){
+ return $http.get('/api/TodoList');
+ },
+ getItem : function(id){
+ return $http.get('/api/TodoList/' + id);
+ },
+ postItem : function(item){
+ return $http.post('/api/TodoList/',item);
+ },
+ putItem : function(item){
+ return $http.put('/api/TodoList/', item);
+ },
+ deleteItem : function(id){
+ return $http({
+ method: 'DELETE',
+ url: '/api/TodoList/' + id
+ });
+ }
+ };
+}]); \ No newline at end of file
diff --git a/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/userDataCtrl.js b/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/userDataCtrl.js
new file mode 100644
index 0000000000..1b2511fdcc
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/wwwroot/App/Scripts/userDataCtrl.js
@@ -0,0 +1,6 @@
+'use strict';
+angular.module('todoApp')
+.controller('userDataCtrl', ['$scope', 'adalAuthenticationService', function ($scope, adalService) {
+
+
+}]); \ No newline at end of file
diff --git a/src/Security/samples/JwtBearerSample/wwwroot/App/Views/Home.html b/src/Security/samples/JwtBearerSample/wwwroot/App/Views/Home.html
new file mode 100644
index 0000000000..49f14e1d5b
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/wwwroot/App/Views/Home.html
@@ -0,0 +1,3 @@
+<div>
+ home sweet home
+</div>
diff --git a/src/Security/samples/JwtBearerSample/wwwroot/App/Views/TodoList.html b/src/Security/samples/JwtBearerSample/wwwroot/App/Views/TodoList.html
new file mode 100644
index 0000000000..8ade518a5c
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/wwwroot/App/Views/TodoList.html
@@ -0,0 +1,24 @@
+<div ng-init="populate()">
+ <p class="error">{{error}}</p>
+ <p>{{loadingMessage}}</p>
+ <div class="panel">
+ <div class="input-group">
+ <input ng-model="newTodoCaption" class="form-control" />
+ <span class="input-group-btn">
+ <button ng-click="add();" class="btn btn-default">Add</button>
+ </span>
+ </div>
+ <table class="table table-striped">
+ <tbody>
+ <tr data-ng-repeat="item in todoList">
+ <td>
+ <p data-ng-hide="item.edit">{{item.Description}}</p>
+ </td>
+ <td>
+ <p data-ng-hide="item.edit">{{item.Owner}}</p>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</div> \ No newline at end of file
diff --git a/src/Security/samples/JwtBearerSample/wwwroot/App/Views/UserData.html b/src/Security/samples/JwtBearerSample/wwwroot/App/Views/UserData.html
new file mode 100644
index 0000000000..cacd57edb9
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/wwwroot/App/Views/UserData.html
@@ -0,0 +1,23 @@
+<div>
+ <h3>
+ Id_token content
+ </h3>
+ <p>{{userInfo.userName}}</p>
+ <p>aud:{{userInfo.profile.aud}}</p>
+ <p>iss:{{userInfo.profile.iss}}</p>
+ <p>iat:{{userInfo.profile.iat}}</p>
+ <p>nbf:{{userInfo.profile.nbf}}</p>
+ <p>exp:{{userInfo.profile.exp}}</p>
+ <p>ver:{{userInfo.profile.ver}}</p>
+ <p>tid:{{userInfo.profile.tid}}</p>
+ <p>amr:{{userInfo.profile.amr}}</p>
+ <p>oid:{{userInfo.profile.oid}}</p>
+ <p>upn:{{userInfo.profile.upn}}</p>
+ <p>unique_name:{{userInfo.profile.unique_name}}</p>
+ <p>sub:{{userInfo.profile.sub}}</p>
+ <p>family_name:{{userInfo.profile.family_name}}</p>
+ <p>given_name:{{userInfo.profile.given_name}}</p>
+ <p>pwd_exp:{{userInfo.profile.pwd_exp}}</p>
+ <p>pwd_url:{{userInfo.profile.pwd_url}}</p>
+
+</div> \ No newline at end of file
diff --git a/src/Security/samples/JwtBearerSample/wwwroot/index.html b/src/Security/samples/JwtBearerSample/wwwroot/index.html
new file mode 100644
index 0000000000..f71dccb693
--- /dev/null
+++ b/src/Security/samples/JwtBearerSample/wwwroot/index.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Todo List: a SPA sample demonstrating Azure AD and ADAL JS</title>
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
+</head>
+<body ng-app="todoApp" ng-controller="homeCtrl" role="document">
+
+
+ <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
+ <div class="container">
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle collapsed"
+ data-toggle="collapse"
+ data-target=".navbar-collapse">
+
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ <a class="navbar-brand" href="#/Home">ADAL JS Sample</a>
+ </div>
+ <div class="navbar-collapse collapse">
+ <ul class="nav navbar-nav">
+ <li ng-class="{ active: isActive('/Home') }"><a href="#/Home">Home</a></li>
+ <li ng-class="{ active: isActive('/TodoList') }"><a href="#/TodoList">Todo List</a></li>
+ <li ng-class="{ active: isActive('/UserData') }"><a href="#/UserData" ng-show="userInfo.isAuthenticated">User</a></li>
+ </ul>
+ <ul class="nav navbar-nav navbar-right">
+ <li><a class="btn btn-link" ng-show="userInfo.isAuthenticated" ng-click="logout()">Logout</a></li>
+ <li><a class="btn btn-link" ng-hide="userInfo.isAuthenticated" ng-click="login()">Login</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+
+
+ <br />
+ <div class="container" role="main">
+ <div class="row">
+ <div class="col-xs-10 col-xs-offset-1" style="background-color:azure">
+ <div class="page-header">
+ <h1>Todo List</h1>
+ </div>
+ <p>This sample demonstrates how to take advantage of ADAL JS for adding Azure AD authentication to your AngularJS apps.</p>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-xs-10 col-xs-offset-1">
+ <div ng-view class="panel-body">
+
+ </div>
+ </div>
+ </div>
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
+ <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.25/angular.min.js"></script>
+ <script src="https://code.angularjs.org/1.2.25/angular-route.js"></script>
+ <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
+ <script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/adal.min.js"></script>
+ <script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.0/js/adal-angular.min.js"></script>
+ <script src="App/Scripts/app.js"></script>
+ <script src="App/Scripts/homeCtrl.js"></script>
+ <script src="App/Scripts/userDataCtrl.js"></script>
+ <script src="App/Scripts/todoListCtrl.js"></script>
+ <script src="App/Scripts/todoListSvc.js"></script>
+</body>
+</html>
diff --git a/src/Security/samples/OpenIdConnect.AzureAdSample/AuthPropertiesTokenCache.cs b/src/Security/samples/OpenIdConnect.AzureAdSample/AuthPropertiesTokenCache.cs
new file mode 100644
index 0000000000..7d9b391213
--- /dev/null
+++ b/src/Security/samples/OpenIdConnect.AzureAdSample/AuthPropertiesTokenCache.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Http;
+using Microsoft.IdentityModel.Clients.ActiveDirectory;
+
+namespace OpenIdConnect.AzureAdSample
+{
+ public class AuthPropertiesTokenCache : TokenCache
+ {
+ private const string TokenCacheKey = ".TokenCache";
+
+ private HttpContext _httpContext;
+ private ClaimsPrincipal _principal;
+ private AuthenticationProperties _authProperties;
+ private string _signInScheme;
+
+ private AuthPropertiesTokenCache(AuthenticationProperties authProperties) : base()
+ {
+ _authProperties = authProperties;
+ BeforeAccess = BeforeAccessNotificationWithProperties;
+ AfterAccess = AfterAccessNotificationWithProperties;
+ BeforeWrite = BeforeWriteNotification;
+ }
+
+ private AuthPropertiesTokenCache(HttpContext httpContext, string signInScheme) : base()
+ {
+ _httpContext = httpContext;
+ _signInScheme = signInScheme;
+ BeforeAccess = BeforeAccessNotificationWithContext;
+ AfterAccess = AfterAccessNotificationWithContext;
+ BeforeWrite = BeforeWriteNotification;
+ }
+
+ public static TokenCache ForCodeRedemption(AuthenticationProperties authProperties)
+ {
+ return new AuthPropertiesTokenCache(authProperties);
+ }
+
+ public static TokenCache ForApiCalls(HttpContext httpContext,
+ string signInScheme = CookieAuthenticationDefaults.AuthenticationScheme)
+ {
+ return new AuthPropertiesTokenCache(httpContext, signInScheme);
+ }
+
+ private void BeforeAccessNotificationWithProperties(TokenCacheNotificationArgs args)
+ {
+ string cachedTokensText;
+ if (_authProperties.Items.TryGetValue(TokenCacheKey, out cachedTokensText))
+ {
+ var cachedTokens = Convert.FromBase64String(cachedTokensText);
+ Deserialize(cachedTokens);
+ }
+ }
+
+ private void BeforeAccessNotificationWithContext(TokenCacheNotificationArgs args)
+ {
+ // Retrieve the auth session with the cached tokens
+ var result = _httpContext.AuthenticateAsync(_signInScheme).Result;
+ _authProperties = result.Ticket.Properties;
+ _principal = result.Ticket.Principal;
+
+ BeforeAccessNotificationWithProperties(args);
+ }
+
+ private void AfterAccessNotificationWithProperties(TokenCacheNotificationArgs args)
+ {
+ // if state changed
+ if (HasStateChanged)
+ {
+ var cachedTokens = Serialize();
+ var cachedTokensText = Convert.ToBase64String(cachedTokens);
+ _authProperties.Items[TokenCacheKey] = cachedTokensText;
+ }
+ }
+
+ private void AfterAccessNotificationWithContext(TokenCacheNotificationArgs args)
+ {
+ // if state changed
+ if (HasStateChanged)
+ {
+ AfterAccessNotificationWithProperties(args);
+
+ var cachedTokens = Serialize();
+ var cachedTokensText = Convert.ToBase64String(cachedTokens);
+ _authProperties.Items[TokenCacheKey] = cachedTokensText;
+ _httpContext.SignInAsync(_signInScheme, _principal, _authProperties).Wait();
+ }
+ }
+
+ private void BeforeWriteNotification(TokenCacheNotificationArgs args)
+ {
+ // if you want to ensure that no concurrent write take place, use this notification to place a lock on the entry
+ }
+ }
+}
diff --git a/src/Security/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.csproj b/src/Security/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.csproj
new file mode 100644
index 0000000000..b14b9590f5
--- /dev/null
+++ b/src/Security/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.csproj
@@ -0,0 +1,23 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFrameworks>net461;netcoreapp2.1</TargetFrameworks>
+ <UserSecretsId>aspnet5-OpenIdConnectSample-20151210110318</UserSecretsId>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.OpenIdConnect\Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="$(MicrosoftExtensionsConfigurationUserSecretsPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ <PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="$(MicrosoftIdentityModelClientsActiveDirectoryPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/samples/OpenIdConnect.AzureAdSample/Program.cs b/src/Security/samples/OpenIdConnect.AzureAdSample/Program.cs
new file mode 100644
index 0000000000..0e1285a9c6
--- /dev/null
+++ b/src/Security/samples/OpenIdConnect.AzureAdSample/Program.cs
@@ -0,0 +1,27 @@
+using System.IO;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace OpenIdConnect.AzureAdSample
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging(factory =>
+ {
+ factory.AddConsole();
+ factory.AddFilter("Console", level => level >= LogLevel.Information);
+ })
+ .UseKestrel()
+ .UseUrls("http://localhost:42023")
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseStartup<Startup>()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/Security/samples/OpenIdConnect.AzureAdSample/Properties/launchSettings.json b/src/Security/samples/OpenIdConnect.AzureAdSample/Properties/launchSettings.json
new file mode 100644
index 0000000000..e6436fee2a
--- /dev/null
+++ b/src/Security/samples/OpenIdConnect.AzureAdSample/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:42023",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "OpenIdConnect": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:42023",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/samples/OpenIdConnect.AzureAdSample/Readme.md b/src/Security/samples/OpenIdConnect.AzureAdSample/Readme.md
new file mode 100644
index 0000000000..767e336ac6
--- /dev/null
+++ b/src/Security/samples/OpenIdConnect.AzureAdSample/Readme.md
@@ -0,0 +1,20 @@
+# How to set up the sample locally
+
+## Set up [Azure Active Directory](https://azure.microsoft.com/en-us/documentation/services/active-directory/)
+
+1. Create your own Azure Active Directory (AD). Save the "tenent name".
+2. Add a new Application: in the Azure AD portal, select Application, and click Add in the drawer.
+3. Set the sign-on url to `http://localhost:42023`.
+4. Select the newly created Application, navigate to the Configure tab.
+5. Find and save the "Client Id"
+8. In the keys section add a new key. A key value will be generated. Save the value as "Client Secret"
+
+## Configure the local environment
+1. Set environment ASPNETCORE_ENVIRONMENT to DEVELOPMENT. ([Working with Multiple Environments](https://docs.asp.net/en/latest/fundamentals/environments.html))
+2. Set up user secrets:
+```
+dotnet user-secrets set oidc:clientid <Client Id>
+dotnet user-secrets set oidc:clientsecret <Client Secret>
+dotnet user-secrets set oidc:authority https://login.windows.net/<Tenent Name>.onmicrosoft.com
+```
+
diff --git a/src/Security/samples/OpenIdConnect.AzureAdSample/Startup.cs b/src/Security/samples/OpenIdConnect.AzureAdSample/Startup.cs
new file mode 100644
index 0000000000..c3fa3c719b
--- /dev/null
+++ b/src/Security/samples/OpenIdConnect.AzureAdSample/Startup.cs
@@ -0,0 +1,203 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.IdentityModel.Clients.ActiveDirectory;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+namespace OpenIdConnect.AzureAdSample
+{
+ public class Startup
+ {
+ public Startup(IHostingEnvironment env)
+ {
+ var builder = new ConfigurationBuilder()
+ .SetBasePath(env.ContentRootPath);
+
+ if (env.IsDevelopment())
+ {
+ // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
+ builder.AddUserSecrets<Startup>();
+ }
+
+ builder.AddEnvironmentVariables();
+ Configuration = builder.Build();
+ }
+
+ public IConfiguration Configuration { get; set; }
+
+ private string ClientId => Configuration["oidc:clientid"];
+ private string ClientSecret => Configuration["oidc:clientsecret"];
+ private string Authority => Configuration["oidc:authority"];
+ private string Resource => "https://graph.windows.net";
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddAuthentication(sharedOptions =>
+ {
+ sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ })
+ .AddCookie()
+ .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, "AAD", o =>
+ {
+ o.ClientId = ClientId;
+ o.ClientSecret = ClientSecret; // for code flow
+ o.Authority = Authority;
+ o.ResponseType = OpenIdConnectResponseType.CodeIdToken;
+ o.SignedOutRedirectUri = "/signed-out";
+ // GetClaimsFromUserInfoEndpoint = true,
+ o.Events = new OpenIdConnectEvents()
+ {
+ OnAuthorizationCodeReceived = async context =>
+ {
+ var request = context.HttpContext.Request;
+ var currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path);
+ var credential = new ClientCredential(ClientId, ClientSecret);
+ var authContext = new AuthenticationContext(Authority, AuthPropertiesTokenCache.ForCodeRedemption(context.Properties));
+
+ var result = await authContext.AcquireTokenByAuthorizationCodeAsync(
+ context.ProtocolMessage.Code, new Uri(currentUri), credential, Resource);
+
+ context.HandleCodeRedemption(result.AccessToken, result.IdToken);
+ }
+ };
+ });
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseDeveloperExceptionPage();
+
+ app.UseAuthentication();
+
+ app.Run(async context =>
+ {
+ if (context.Request.Path.Equals("/signin"))
+ {
+ if (context.User.Identities.Any(identity => identity.IsAuthenticated))
+ {
+ // User has already signed in
+ context.Response.Redirect("/");
+ return;
+ }
+
+ await context.ChallengeAsync(new AuthenticationProperties { RedirectUri = "/" });
+ }
+ else if (context.Request.Path.Equals("/signout"))
+ {
+ await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ await WriteHtmlAsync(context.Response,
+ async response =>
+ {
+ await response.WriteAsync($"<h1>Signed out locally: {HtmlEncode(context.User.Identity.Name)}</h1>");
+ await response.WriteAsync("<a class=\"btn btn-primary\" href=\"/\">Sign In</a>");
+ });
+ }
+ else if (context.Request.Path.Equals("/signout-remote"))
+ {
+ await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
+ }
+ else if (context.Request.Path.Equals("/signed-out"))
+ {
+ await WriteHtmlAsync(context.Response,
+ async response =>
+ {
+ await response.WriteAsync($"<h1>You have been signed out.</h1>");
+ await response.WriteAsync("<a class=\"btn btn-primary\" href=\"/signin\">Sign In</a>");
+ });
+ }
+ else if (context.Request.Path.Equals("/remote-signedout"))
+ {
+ await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ await WriteHtmlAsync(context.Response,
+ async response =>
+ {
+ await response.WriteAsync($"<h1>Signed out remotely: {HtmlEncode(context.User.Identity.Name)}</h1>");
+ await response.WriteAsync("<a class=\"btn btn-primary\" href=\"/\">Sign In</a>");
+ });
+ }
+ else
+ {
+ if (!context.User.Identities.Any(identity => identity.IsAuthenticated))
+ {
+ await context.ChallengeAsync(new AuthenticationProperties { RedirectUri = "/" });
+ return;
+ }
+
+ await WriteHtmlAsync(context.Response, async response =>
+ {
+ await response.WriteAsync($"<h1>Hello Authenticated User {HtmlEncode(context.User.Identity.Name)}</h1>");
+ await response.WriteAsync("<a class=\"btn btn-default\" href=\"/signout\">Sign Out Locally</a>");
+ await response.WriteAsync("<a class=\"btn btn-default\" href=\"/signout-remote\">Sign Out Remotely</a>");
+
+ await response.WriteAsync("<h2>Claims:</h2>");
+ await WriteTableHeader(response, new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value }));
+
+ await response.WriteAsync("<h2>Tokens:</h2>");
+ try
+ {
+ // Use ADAL to get the right token
+ var authContext = new AuthenticationContext(Authority, AuthPropertiesTokenCache.ForApiCalls(context, CookieAuthenticationDefaults.AuthenticationScheme));
+ var credential = new ClientCredential(ClientId, ClientSecret);
+ string userObjectID = context.User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
+ var result = await authContext.AcquireTokenSilentAsync(Resource, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
+
+ await response.WriteAsync($"<h3>access_token</h3><code>{HtmlEncode(result.AccessToken)}</code><br>");
+ }
+ catch (Exception ex)
+ {
+ await response.WriteAsync($"AquireToken error: {ex.Message}");
+ }
+ });
+ }
+ });
+ }
+
+ private static async Task WriteHtmlAsync(HttpResponse response, Func<HttpResponse, Task> writeContent)
+ {
+ var bootstrap = "<link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css\" integrity=\"sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u\" crossorigin=\"anonymous\">";
+
+ response.ContentType = "text/html";
+ await response.WriteAsync($"<html><head>{bootstrap}</head><body><div class=\"container\">");
+ await writeContent(response);
+ await response.WriteAsync("</div></body></html>");
+ }
+
+ private static async Task WriteTableHeader(HttpResponse response, IEnumerable<string> columns, IEnumerable<IEnumerable<string>> data)
+ {
+ await response.WriteAsync("<table class=\"table table-condensed\">");
+ await response.WriteAsync("<tr>");
+ foreach (var column in columns)
+ {
+ await response.WriteAsync($"<th>{HtmlEncode(column)}</th>");
+ }
+ await response.WriteAsync("</tr>");
+ foreach (var row in data)
+ {
+ await response.WriteAsync("<tr>");
+ foreach (var column in row)
+ {
+ await response.WriteAsync($"<td>{HtmlEncode(column)}</td>");
+ }
+ await response.WriteAsync("</tr>");
+ }
+ await response.WriteAsync("</table>");
+ }
+
+ private static string HtmlEncode(string content) =>
+ string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content);
+ }
+}
+
diff --git a/src/Security/samples/OpenIdConnectSample/OpenIdConnectSample.csproj b/src/Security/samples/OpenIdConnectSample/OpenIdConnectSample.csproj
new file mode 100644
index 0000000000..23e87d4f2a
--- /dev/null
+++ b/src/Security/samples/OpenIdConnectSample/OpenIdConnectSample.csproj
@@ -0,0 +1,33 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFrameworks>net461;netcoreapp2.1</TargetFrameworks>
+ <UserSecretsId>aspnet5-OpenIdConnectSample-20151210110318</UserSecretsId>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <None Remove="compiler\resources\cert.pfx" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.OpenIdConnect\Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="$(MicrosoftAspNetCoreServerKestrelHttpsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="$(MicrosoftExtensionsConfigurationUserSecretsPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="$(MicrosoftExtensionsFileProvidersEmbeddedPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(MicrosoftExtensionsLoggingDebugPackageVersion)" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <EmbeddedResource Include="compiler\resources\cert.pfx" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/samples/OpenIdConnectSample/Program.cs b/src/Security/samples/OpenIdConnectSample/Program.cs
new file mode 100644
index 0000000000..87e7755084
--- /dev/null
+++ b/src/Security/samples/OpenIdConnectSample/Program.cs
@@ -0,0 +1,59 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Reflection;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging;
+
+namespace OpenIdConnectSample
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging(factory =>
+ {
+ factory.AddConsole();
+ factory.AddDebug();
+ factory.AddFilter("Console", level => level >= LogLevel.Information);
+ factory.AddFilter("Debug", level => level >= LogLevel.Information);
+ })
+ .UseKestrel(options =>
+ {
+ options.Listen(IPAddress.Loopback, 44318, listenOptions =>
+ {
+ // Configure SSL
+ var serverCertificate = LoadCertificate();
+ listenOptions.UseHttps(serverCertificate);
+ });
+ })
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseStartup<Startup>()
+ .Build();
+
+ host.Run();
+ }
+
+ private static X509Certificate2 LoadCertificate()
+ {
+ var assembly = typeof(Startup).GetTypeInfo().Assembly;
+ var embeddedFileProvider = new EmbeddedFileProvider(assembly, "OpenIdConnectSample");
+ var certificateFileInfo = embeddedFileProvider.GetFileInfo("compiler/resources/cert.pfx");
+ using (var certificateStream = certificateFileInfo.CreateReadStream())
+ {
+ byte[] certificatePayload;
+ using (var memoryStream = new MemoryStream())
+ {
+ certificateStream.CopyTo(memoryStream);
+ certificatePayload = memoryStream.ToArray();
+ }
+
+ return new X509Certificate2(certificatePayload, "testPassword");
+ }
+ }
+ }
+}
diff --git a/src/Security/samples/OpenIdConnectSample/Properties/launchSettings.json b/src/Security/samples/OpenIdConnectSample/Properties/launchSettings.json
new file mode 100644
index 0000000000..058fa4c5dd
--- /dev/null
+++ b/src/Security/samples/OpenIdConnectSample/Properties/launchSettings.json
@@ -0,0 +1,28 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:42023",
+ "sslPort": 44318
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "https://localhost:44318/",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "OpenIdConnectSample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:44318/",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/samples/OpenIdConnectSample/Readme.md b/src/Security/samples/OpenIdConnectSample/Readme.md
new file mode 100644
index 0000000000..846e3f8e6a
--- /dev/null
+++ b/src/Security/samples/OpenIdConnectSample/Readme.md
@@ -0,0 +1,44 @@
+# How to set up the sample locally
+
+The OpenIdConnect sample supports multilpe authentication providers. In these instruction, we will explore how to set up this sample with both Azure Active Directory and Google Identity Platform.
+
+## Determine your development environment and a few key variables
+
+This sample is configured to run on port __44318__ locally. In Visual Studio, the setting is carried out in `.\properties\launchSettings.json`. When the application is run from command line, the URL is coded in `Program.cs`.
+
+If the application is run from command line or terminal, environment variable ASPNETCORE_ENVIRONMENT should be set to DEVELOPMENT to enable user secret.
+
+## Configure the Authorization server
+
+### Configure with Azure Active Directory
+
+1. Set up a new Azure Active Directory (AAD) in your Azure Subscription.
+2. Open the newly created AAD in Azure web portal.
+3. Navigate to the Applications tab.
+4. Add a new Application to the AAD. Set the "Sign-on URL" to sample application's URL.
+5. Naigate to the Application, and click the Configure tab.
+6. Find and save the "Client Id".
+7. Add a new key in the "Keys" section. Save value of the key, which is the "Client Secret".
+8. Click the "View Endpoints" on the drawer, a dialog will shows six endpoint URLs. Copy the "OAuth 2.0 Authorization Endpoint" to a text editor and remove the "/oauth2/authorize" from the string. The remaining part is the __authority URL__. It looks like `https://login.microsoftonline.com/<guid>`.
+
+### Configure with Google Identity Platform
+
+1. Create a new project through [Google APIs](https://console.developers.google.com).
+2. In the sidebar choose "Credentials".
+3. Navigate to "OAuth consent screen" tab, fill in the project name and save.
+4. Navigate to "Credentials" tab. Click "Create credentials". Choose "OAuth client ID".
+5. Select "Web application" as the application type. Fill in the "Authorized redirect URIs" with `https://localhost:44318/signin-oidc`.
+6. Save the "Client ID" and "Client Secret" shown in the dialog.
+7. The "Authority URL" for Google Authentication is `https://accounts.google.com/`.
+
+## Configure the sample application
+
+1. Restore the application.
+2. Set user secrets:
+
+ ```
+dotnet user-secrets set oidc:clientid <Client Id>
+dotnet user-secrets set oidc:clientsecret <Client Secret>
+dotnet user-secrets set oidc:authority <Authority URL>
+```
+
diff --git a/src/Security/samples/OpenIdConnectSample/Startup.cs b/src/Security/samples/OpenIdConnectSample/Startup.cs
new file mode 100644
index 0000000000..1aa7625cb0
--- /dev/null
+++ b/src/Security/samples/OpenIdConnectSample/Startup.cs
@@ -0,0 +1,297 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Net.Http;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+using Newtonsoft.Json.Linq;
+
+namespace OpenIdConnectSample
+{
+ public class Startup
+ {
+ public Startup(IHostingEnvironment env)
+ {
+ Environment = env;
+
+ var builder = new ConfigurationBuilder()
+ .SetBasePath(env.ContentRootPath);
+
+ if (env.IsDevelopment())
+ {
+ // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
+ builder.AddUserSecrets<Startup>();
+ }
+
+ builder.AddEnvironmentVariables();
+ Configuration = builder.Build();
+ }
+
+ public IConfiguration Configuration { get; set; }
+
+ public IHostingEnvironment Environment { get; set; }
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
+
+ services.AddAuthentication(sharedOptions =>
+ {
+ sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ })
+ .AddCookie()
+ .AddOpenIdConnect(o =>
+ {
+ o.ClientId = Configuration["oidc:clientid"];
+ o.ClientSecret = Configuration["oidc:clientsecret"]; // for code flow
+ o.Authority = Configuration["oidc:authority"];
+
+ o.ResponseType = OpenIdConnectResponseType.CodeIdToken;
+ o.SaveTokens = true;
+ o.GetClaimsFromUserInfoEndpoint = true;
+
+ o.ClaimActions.MapAllExcept("aud", "iss", "iat", "nbf", "exp", "aio", "c_hash", "uti", "nonce");
+
+ o.Events = new OpenIdConnectEvents()
+ {
+ OnAuthenticationFailed = c =>
+ {
+ c.HandleResponse();
+
+ c.Response.StatusCode = 500;
+ c.Response.ContentType = "text/plain";
+ if (Environment.IsDevelopment())
+ {
+ // Debug only, in production do not share exceptions with the remote host.
+ return c.Response.WriteAsync(c.Exception.ToString());
+ }
+ return c.Response.WriteAsync("An error occurred processing your authentication.");
+ }
+ };
+ });
+ }
+
+ public void Configure(IApplicationBuilder app, IOptionsMonitor<OpenIdConnectOptions> optionsMonitor)
+ {
+ app.UseDeveloperExceptionPage();
+ app.UseAuthentication();
+
+ app.Run(async context =>
+ {
+ var response = context.Response;
+
+ if (context.Request.Path.Equals("/signedout"))
+ {
+ await WriteHtmlAsync(response, async res =>
+ {
+ await res.WriteAsync($"<h1>You have been signed out.</h1>");
+ await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>");
+ });
+ return;
+ }
+
+ if (context.Request.Path.Equals("/signout"))
+ {
+ await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ await WriteHtmlAsync(response, async res =>
+ {
+ await res.WriteAsync($"<h1>Signed out {HtmlEncode(context.User.Identity.Name)}</h1>");
+ await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>");
+ });
+ return;
+ }
+
+ if (context.Request.Path.Equals("/signout-remote"))
+ {
+ // Redirects
+ await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties()
+ {
+ RedirectUri = "/signedout"
+ });
+ return;
+ }
+
+ if (context.Request.Path.Equals("/Account/AccessDenied"))
+ {
+ await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ await WriteHtmlAsync(response, async res =>
+ {
+ await res.WriteAsync($"<h1>Access Denied for user {HtmlEncode(context.User.Identity.Name)} to resource '{HtmlEncode(context.Request.Query["ReturnUrl"])}'</h1>");
+ await res.WriteAsync("<a class=\"btn btn-default\" href=\"/signout\">Sign Out</a>");
+ await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>");
+ });
+ return;
+ }
+
+ // DefaultAuthenticateScheme causes User to be set
+ // var user = context.User;
+
+ // This is what [Authorize] calls
+ var userResult = await context.AuthenticateAsync();
+ var user = userResult.Principal;
+ var props = userResult.Properties;
+
+ // This is what [Authorize(ActiveAuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)] calls
+ // var user = await context.AuthenticateAsync(OpenIdConnectDefaults.AuthenticationScheme);
+
+ // Not authenticated
+ if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated))
+ {
+ // This is what [Authorize] calls
+ await context.ChallengeAsync();
+
+ // This is what [Authorize(ActiveAuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)] calls
+ // await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme);
+
+ return;
+ }
+
+ // Authenticated, but not authorized
+ if (context.Request.Path.Equals("/restricted") && !user.Identities.Any(identity => identity.HasClaim("special", "true")))
+ {
+ await context.ForbidAsync();
+ return;
+ }
+
+ if (context.Request.Path.Equals("/refresh"))
+ {
+ var refreshToken = props.GetTokenValue("refresh_token");
+
+ if (string.IsNullOrEmpty(refreshToken))
+ {
+ await WriteHtmlAsync(response, async res =>
+ {
+ await res.WriteAsync($"No refresh_token is available.<br>");
+ await res.WriteAsync("<a class=\"btn btn-link\" href=\"/signout\">Sign Out</a>");
+ });
+
+ return;
+ }
+
+ var options = optionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme);
+ var metadata = await options.ConfigurationManager.GetConfigurationAsync(context.RequestAborted);
+
+ var pairs = new Dictionary<string, string>()
+ {
+ { "client_id", options.ClientId },
+ { "client_secret", options.ClientSecret },
+ { "grant_type", "refresh_token" },
+ { "refresh_token", refreshToken }
+ };
+ var content = new FormUrlEncodedContent(pairs);
+ var tokenResponse = await options.Backchannel.PostAsync(metadata.TokenEndpoint, content, context.RequestAborted);
+ tokenResponse.EnsureSuccessStatusCode();
+
+ var payload = JObject.Parse(await tokenResponse.Content.ReadAsStringAsync());
+
+ // Persist the new acess token
+ props.UpdateTokenValue("access_token", payload.Value<string>("access_token"));
+ props.UpdateTokenValue("refresh_token", payload.Value<string>("refresh_token"));
+ if (int.TryParse(payload.Value<string>("expires_in"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds))
+ {
+ var expiresAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(seconds);
+ props.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));
+ }
+ await context.SignInAsync(user, props);
+
+ await WriteHtmlAsync(response, async res =>
+ {
+ await res.WriteAsync($"<h1>Refreshed.</h1>");
+ await res.WriteAsync("<a class=\"btn btn-default\" href=\"/refresh\">Refresh tokens</a>");
+ await res.WriteAsync("<a class=\"btn btn-default\" href=\"/\">Home</a>");
+
+ await res.WriteAsync("<h2>Tokens:</h2>");
+ await WriteTableHeader(res, new string[] { "Token Type", "Value" }, props.GetTokens().Select(token => new string[] { token.Name, token.Value }));
+
+ await res.WriteAsync("<h2>Payload:</h2>");
+ await res.WriteAsync(HtmlEncoder.Default.Encode(payload.ToString()).Replace(",", ",<br>") + "<br>");
+ });
+
+ return;
+ }
+
+ if (context.Request.Path.Equals("/login-challenge"))
+ {
+ // Challenge the user authentication, and force a login prompt by overwriting the
+ // "prompt". This could be used for example to require the user to re-enter their
+ // credentials at the authentication provider, to add an extra confirmation layer.
+ await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new OpenIdConnectChallengeProperties()
+ {
+ Prompt = "login",
+
+ // it is also possible to specify different scopes, e.g.
+ // Scope = new string[] { "openid", "profile", "other" }
+ });
+
+ return;
+ }
+
+ await WriteHtmlAsync(response, async res =>
+ {
+ await res.WriteAsync($"<h1>Hello Authenticated User {HtmlEncode(user.Identity.Name)}</h1>");
+ await res.WriteAsync("<a class=\"btn btn-default\" href=\"/refresh\">Refresh tokens</a>");
+ await res.WriteAsync("<a class=\"btn btn-default\" href=\"/restricted\">Restricted</a>");
+ await res.WriteAsync("<a class=\"btn btn-default\" href=\"/login-challenge\">Login challenge</a>");
+ await res.WriteAsync("<a class=\"btn btn-default\" href=\"/signout\">Sign Out</a>");
+ await res.WriteAsync("<a class=\"btn btn-default\" href=\"/signout-remote\">Sign Out Remote</a>");
+
+ await res.WriteAsync("<h2>Claims:</h2>");
+ await WriteTableHeader(res, new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value }));
+
+ await res.WriteAsync("<h2>Tokens:</h2>");
+ await WriteTableHeader(res, new string[] { "Token Type", "Value" }, props.GetTokens().Select(token => new string[] { token.Name, token.Value }));
+ });
+ });
+ }
+
+ private static async Task WriteHtmlAsync(HttpResponse response, Func<HttpResponse, Task> writeContent)
+ {
+ var bootstrap = "<link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css\" integrity=\"sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u\" crossorigin=\"anonymous\">";
+
+ response.ContentType = "text/html";
+ await response.WriteAsync($"<html><head>{bootstrap}</head><body><div class=\"container\">");
+ await writeContent(response);
+ await response.WriteAsync("</div></body></html>");
+ }
+
+ private static async Task WriteTableHeader(HttpResponse response, IEnumerable<string> columns, IEnumerable<IEnumerable<string>> data)
+ {
+ await response.WriteAsync("<table class=\"table table-condensed\">");
+ await response.WriteAsync("<tr>");
+ foreach (var column in columns)
+ {
+ await response.WriteAsync($"<th>{HtmlEncode(column)}</th>");
+ }
+ await response.WriteAsync("</tr>");
+ foreach (var row in data)
+ {
+ await response.WriteAsync("<tr>");
+ foreach (var column in row)
+ {
+ await response.WriteAsync($"<td>{HtmlEncode(column)}</td>");
+ }
+ await response.WriteAsync("</tr>");
+ }
+ await response.WriteAsync("</table>");
+ }
+
+ private static string HtmlEncode(string content) =>
+ string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content);
+ }
+}
+
diff --git a/src/Security/samples/OpenIdConnectSample/compiler/resources/cert.pfx b/src/Security/samples/OpenIdConnectSample/compiler/resources/cert.pfx
new file mode 100644
index 0000000000..7118908c2d
--- /dev/null
+++ b/src/Security/samples/OpenIdConnectSample/compiler/resources/cert.pfx
Binary files differ
diff --git a/src/Security/samples/SocialSample/Program.cs b/src/Security/samples/SocialSample/Program.cs
new file mode 100644
index 0000000000..a712b6c03f
--- /dev/null
+++ b/src/Security/samples/SocialSample/Program.cs
@@ -0,0 +1,57 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Reflection;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging;
+
+namespace SocialSample
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging(factory =>
+ {
+ factory.AddConsole();
+ factory.AddFilter("Console", level => level >= LogLevel.Information);
+ })
+ .UseKestrel(options =>
+ {
+ options.Listen(IPAddress.Loopback, 44318, listenOptions =>
+ {
+ // Configure SSL
+ var serverCertificate = LoadCertificate();
+ listenOptions.UseHttps(serverCertificate);
+ });
+ })
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseStartup<Startup>()
+ .Build();
+
+ host.Run();
+ }
+
+ private static X509Certificate2 LoadCertificate()
+ {
+ var socialSampleAssembly = typeof(Startup).GetTypeInfo().Assembly;
+ var embeddedFileProvider = new EmbeddedFileProvider(socialSampleAssembly, "SocialSample");
+ var certificateFileInfo = embeddedFileProvider.GetFileInfo("compiler/resources/cert.pfx");
+ using (var certificateStream = certificateFileInfo.CreateReadStream())
+ {
+ byte[] certificatePayload;
+ using (var memoryStream = new MemoryStream())
+ {
+ certificateStream.CopyTo(memoryStream);
+ certificatePayload = memoryStream.ToArray();
+ }
+
+ return new X509Certificate2(certificatePayload, "testPassword");
+ }
+ }
+ }
+}
diff --git a/src/Security/samples/SocialSample/Properties/launchSettings.json b/src/Security/samples/SocialSample/Properties/launchSettings.json
new file mode 100644
index 0000000000..30bf2e5f6a
--- /dev/null
+++ b/src/Security/samples/SocialSample/Properties/launchSettings.json
@@ -0,0 +1,28 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:54540",
+ "sslPort": 44318
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "https://localhost:44318/",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "SocialSample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:44318/",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/samples/SocialSample/SocialSample.csproj b/src/Security/samples/SocialSample/SocialSample.csproj
new file mode 100644
index 0000000000..a423ae21a3
--- /dev/null
+++ b/src/Security/samples/SocialSample/SocialSample.csproj
@@ -0,0 +1,36 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFrameworks>net461;netcoreapp2.1</TargetFrameworks>
+ <UserSecretsId>aspnet5-SocialSample-20151210111056</UserSecretsId>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <None Remove="compiler\resources\cert.pfx" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <EmbeddedResource Include="compiler\resources\cert.pfx" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Facebook\Microsoft.AspNetCore.Authentication.Facebook.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Google\Microsoft.AspNetCore.Authentication.Google.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.MicrosoftAccount\Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Twitter\Microsoft.AspNetCore.Authentication.Twitter.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="$(MicrosoftAspNetCoreDataProtectionPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="$(MicrosoftAspNetCoreServerKestrelHttpsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="$(MicrosoftExtensionsConfigurationUserSecretsPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="$(MicrosoftExtensionsFileProvidersEmbeddedPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/samples/SocialSample/Startup.cs b/src/Security/samples/SocialSample/Startup.cs
new file mode 100644
index 0000000000..35896e84b1
--- /dev/null
+++ b/src/Security/samples/SocialSample/Startup.cs
@@ -0,0 +1,502 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.Facebook;
+using Microsoft.AspNetCore.Authentication.Google;
+using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.AspNetCore.Authentication.Twitter;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json.Linq;
+
+namespace SocialSample
+{
+ /* Note all servers must use the same address and port because these are pre-registered with the various providers. */
+ public class Startup
+ {
+ public Startup(IHostingEnvironment env)
+ {
+ var builder = new ConfigurationBuilder()
+ .SetBasePath(env.ContentRootPath)
+ .AddJsonFile("appsettings.json", optional: true);
+
+ if (env.IsDevelopment())
+ {
+ // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
+ builder.AddUserSecrets<Startup>();
+ }
+
+ builder.AddEnvironmentVariables();
+ Configuration = builder.Build();
+ }
+
+ public IConfiguration Configuration { get; set; }
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ if (string.IsNullOrEmpty(Configuration["facebook:appid"]))
+ {
+ // User-Secrets: https://docs.asp.net/en/latest/security/app-secrets.html
+ // See below for registration instructions for each provider.
+ throw new InvalidOperationException("User secrets must be configured for each authentication provider.");
+ }
+
+ services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
+ .AddCookie(o => o.LoginPath = new PathString("/login"))
+ // You must first create an app with Facebook and add its ID and Secret to your user-secrets.
+ // https://developers.facebook.com/apps/
+ .AddFacebook(o =>
+ {
+ o.AppId = Configuration["facebook:appid"];
+ o.AppSecret = Configuration["facebook:appsecret"];
+ o.Scope.Add("email");
+ o.Fields.Add("name");
+ o.Fields.Add("email");
+ o.SaveTokens = true;
+ o.Events = new OAuthEvents()
+ {
+ OnRemoteFailure = HandleOnRemoteFailure
+ };
+ })
+ // You must first create an app with Google and add its ID and Secret to your user-secrets.
+ // https://console.developers.google.com/project
+ .AddOAuth("Google-AccessToken", "Google AccessToken only", o =>
+ {
+ o.ClientId = Configuration["google:clientid"];
+ o.ClientSecret = Configuration["google:clientsecret"];
+ o.CallbackPath = new PathString("/signin-google-token");
+ o.AuthorizationEndpoint = GoogleDefaults.AuthorizationEndpoint;
+ o.TokenEndpoint = GoogleDefaults.TokenEndpoint;
+ o.Scope.Add("openid");
+ o.Scope.Add("profile");
+ o.Scope.Add("email");
+ o.SaveTokens = true;
+ o.Events = new OAuthEvents()
+ {
+ OnRemoteFailure = HandleOnRemoteFailure
+ };
+ })
+ // You must first create an app with Google and add its ID and Secret to your user-secrets.
+ // https://console.developers.google.com/project
+ .AddGoogle(o =>
+ {
+ o.ClientId = Configuration["google:clientid"];
+ o.ClientSecret = Configuration["google:clientsecret"];
+ o.AuthorizationEndpoint += "?prompt=consent"; // Hack so we always get a refresh token, it only comes on the first authorization response
+ o.AccessType = "offline";
+ o.SaveTokens = true;
+ o.Events = new OAuthEvents()
+ {
+ OnRemoteFailure = HandleOnRemoteFailure
+ };
+ o.ClaimActions.MapJsonSubKey("urn:google:image", "image", "url");
+ o.ClaimActions.Remove(ClaimTypes.GivenName);
+ })
+ // You must first create an app with Twitter and add its key and Secret to your user-secrets.
+ // https://apps.twitter.com/
+ .AddTwitter(o =>
+ {
+ o.ConsumerKey = Configuration["twitter:consumerkey"];
+ o.ConsumerSecret = Configuration["twitter:consumersecret"];
+ // http://stackoverflow.com/questions/22627083/can-we-get-email-id-from-twitter-oauth-api/32852370#32852370
+ // http://stackoverflow.com/questions/36330675/get-users-email-from-twitter-api-for-external-login-authentication-asp-net-mvc?lq=1
+ o.RetrieveUserDetails = true;
+ o.SaveTokens = true;
+ o.ClaimActions.MapJsonKey("urn:twitter:profilepicture", "profile_image_url", ClaimTypes.Uri);
+ o.Events = new TwitterEvents()
+ {
+ OnRemoteFailure = HandleOnRemoteFailure
+ };
+ })
+ /* Azure AD app model v2 has restrictions that prevent the use of plain HTTP for redirect URLs.
+ Therefore, to authenticate through microsoft accounts, tryout the sample using the following URL:
+ https://localhost:44318/
+ */
+ // You must first create an app with Microsoft Account and add its ID and Secret to your user-secrets.
+ // https://apps.dev.microsoft.com/
+ .AddOAuth("Microsoft-AccessToken", "Microsoft AccessToken only", o =>
+ {
+ o.ClientId = Configuration["microsoftaccount:clientid"];
+ o.ClientSecret = Configuration["microsoftaccount:clientsecret"];
+ o.CallbackPath = new PathString("/signin-microsoft-token");
+ o.AuthorizationEndpoint = MicrosoftAccountDefaults.AuthorizationEndpoint;
+ o.TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint;
+ o.Scope.Add("https://graph.microsoft.com/user.read");
+ o.SaveTokens = true;
+ o.Events = new OAuthEvents()
+ {
+ OnRemoteFailure = HandleOnRemoteFailure
+ };
+ })
+ // You must first create an app with Microsoft Account and add its ID and Secret to your user-secrets.
+ // https://azure.microsoft.com/en-us/documentation/articles/active-directory-v2-app-registration/
+ .AddMicrosoftAccount(o =>
+ {
+ o.ClientId = Configuration["microsoftaccount:clientid"];
+ o.ClientSecret = Configuration["microsoftaccount:clientsecret"];
+ o.SaveTokens = true;
+ o.Scope.Add("offline_access");
+ o.Events = new OAuthEvents()
+ {
+ OnRemoteFailure = HandleOnRemoteFailure
+ };
+ })
+ // You must first create an app with GitHub and add its ID and Secret to your user-secrets.
+ // https://github.com/settings/applications/
+ .AddOAuth("GitHub-AccessToken", "GitHub AccessToken only", o =>
+ {
+ o.ClientId = Configuration["github-token:clientid"];
+ o.ClientSecret = Configuration["github-token:clientsecret"];
+ o.CallbackPath = new PathString("/signin-github-token");
+ o.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
+ o.TokenEndpoint = "https://github.com/login/oauth/access_token";
+ o.SaveTokens = true;
+ o.Events = new OAuthEvents()
+ {
+ OnRemoteFailure = HandleOnRemoteFailure
+ };
+ })
+ // You must first create an app with GitHub and add its ID and Secret to your user-secrets.
+ // https://github.com/settings/applications/
+ .AddOAuth("GitHub", "Github", o =>
+ {
+ o.ClientId = Configuration["github:clientid"];
+ o.ClientSecret = Configuration["github:clientsecret"];
+ o.CallbackPath = new PathString("/signin-github");
+ o.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
+ o.TokenEndpoint = "https://github.com/login/oauth/access_token";
+ o.UserInformationEndpoint = "https://api.github.com/user";
+ o.ClaimsIssuer = "OAuth2-Github";
+ o.SaveTokens = true;
+ // Retrieving user information is unique to each provider.
+ o.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
+ o.ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
+ o.ClaimActions.MapJsonKey("urn:github:name", "name");
+ o.ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email);
+ o.ClaimActions.MapJsonKey("urn:github:url", "url");
+ o.Events = new OAuthEvents
+ {
+ OnRemoteFailure = HandleOnRemoteFailure,
+ OnCreatingTicket = async context =>
+ {
+ // Get the GitHub user
+ var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+
+ var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
+ response.EnsureSuccessStatusCode();
+
+ var user = JObject.Parse(await response.Content.ReadAsStringAsync());
+
+ context.RunClaimActions(user);
+ }
+ };
+ });
+ }
+
+ private async Task HandleOnRemoteFailure(RemoteFailureContext context)
+ {
+ context.Response.StatusCode = 500;
+ context.Response.ContentType = "text/html";
+ await context.Response.WriteAsync("<html><body>");
+ await context.Response.WriteAsync("A remote failure has occurred: " + UrlEncoder.Default.Encode(context.Failure.Message) + "<br>");
+
+ if (context.Properties != null)
+ {
+ await context.Response.WriteAsync("Properties:<br>");
+ foreach (var pair in context.Properties.Items)
+ {
+ await context.Response.WriteAsync($"-{ UrlEncoder.Default.Encode(pair.Key)}={ UrlEncoder.Default.Encode(pair.Value)}<br>");
+ }
+ }
+
+ await context.Response.WriteAsync("<a href=\"/\">Home</a>");
+ await context.Response.WriteAsync("</body></html>");
+
+ // context.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(context.Failure.Message));
+
+ context.HandleResponse();
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseDeveloperExceptionPage();
+
+ app.UseAuthentication();
+
+ // Choose an authentication type
+ app.Map("/login", signinApp =>
+ {
+ signinApp.Run(async context =>
+ {
+ var authType = context.Request.Query["authscheme"];
+ if (!string.IsNullOrEmpty(authType))
+ {
+ // By default the client will be redirect back to the URL that issued the challenge (/login?authtype=foo),
+ // send them to the home page instead (/).
+ await context.ChallengeAsync(authType, new AuthenticationProperties() { RedirectUri = "/" });
+ return;
+ }
+
+ var response = context.Response;
+ response.ContentType = "text/html";
+ await response.WriteAsync("<html><body>");
+ await response.WriteAsync("Choose an authentication scheme: <br>");
+ var schemeProvider = context.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>();
+ foreach (var provider in await schemeProvider.GetAllSchemesAsync())
+ {
+ await response.WriteAsync("<a href=\"?authscheme=" + provider.Name + "\">" + (provider.DisplayName ?? "(suppressed)") + "</a><br>");
+ }
+ await response.WriteAsync("</body></html>");
+ });
+ });
+
+ // Refresh the access token
+ app.Map("/refresh_token", signinApp =>
+ {
+ signinApp.Run(async context =>
+ {
+ var response = context.Response;
+
+ // Setting DefaultAuthenticateScheme causes User to be set
+ // var user = context.User;
+
+ // This is what [Authorize] calls
+ var userResult = await context.AuthenticateAsync();
+ var user = userResult.Principal;
+ var authProperties = userResult.Properties;
+
+ // This is what [Authorize(ActiveAuthenticationSchemes = MicrosoftAccountDefaults.AuthenticationScheme)] calls
+ // var user = await context.AuthenticateAsync(MicrosoftAccountDefaults.AuthenticationScheme);
+
+ // Deny anonymous request beyond this point.
+ if (!userResult.Succeeded || user == null || !user.Identities.Any(identity => identity.IsAuthenticated))
+ {
+ // This is what [Authorize] calls
+ // The cookie middleware will handle this and redirect to /login
+ await context.ChallengeAsync();
+
+ // This is what [Authorize(ActiveAuthenticationSchemes = MicrosoftAccountDefaults.AuthenticationScheme)] calls
+ // await context.ChallengeAsync(MicrosoftAccountDefaults.AuthenticationScheme);
+
+ return;
+ }
+
+ var currentAuthType = user.Identities.First().AuthenticationType;
+ if (string.Equals(GoogleDefaults.AuthenticationScheme, currentAuthType)
+ || string.Equals(MicrosoftAccountDefaults.AuthenticationScheme, currentAuthType))
+ {
+ var refreshToken = authProperties.GetTokenValue("refresh_token");
+
+ if (string.IsNullOrEmpty(refreshToken))
+ {
+ response.ContentType = "text/html";
+ await response.WriteAsync("<html><body>");
+ await response.WriteAsync("No refresh_token is available.<br>");
+ await response.WriteAsync("<a href=\"/\">Home</a>");
+ await response.WriteAsync("</body></html>");
+ return;
+ }
+
+ var options = await GetOAuthOptionsAsync(context, currentAuthType);
+
+ var pairs = new Dictionary<string, string>()
+ {
+ { "client_id", options.ClientId },
+ { "client_secret", options.ClientSecret },
+ { "grant_type", "refresh_token" },
+ { "refresh_token", refreshToken }
+ };
+ var content = new FormUrlEncodedContent(pairs);
+ var refreshResponse = await options.Backchannel.PostAsync(options.TokenEndpoint, content, context.RequestAborted);
+ refreshResponse.EnsureSuccessStatusCode();
+
+ var payload = JObject.Parse(await refreshResponse.Content.ReadAsStringAsync());
+
+ // Persist the new acess token
+ authProperties.UpdateTokenValue("access_token", payload.Value<string>("access_token"));
+ refreshToken = payload.Value<string>("refresh_token");
+ if (!string.IsNullOrEmpty(refreshToken))
+ {
+ authProperties.UpdateTokenValue("refresh_token", refreshToken);
+ }
+ if (int.TryParse(payload.Value<string>("expires_in"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds))
+ {
+ var expiresAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(seconds);
+ authProperties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));
+ }
+ await context.SignInAsync(user, authProperties);
+
+ await PrintRefreshedTokensAsync(response, payload, authProperties);
+
+ return;
+ }
+ // https://developers.facebook.com/docs/facebook-login/access-tokens/expiration-and-extension
+ else if (string.Equals(FacebookDefaults.AuthenticationScheme, currentAuthType))
+ {
+ var options = await GetOAuthOptionsAsync(context, currentAuthType);
+
+ var accessToken = authProperties.GetTokenValue("access_token");
+
+ var query = new QueryBuilder()
+ {
+ { "grant_type", "fb_exchange_token" },
+ { "client_id", options.ClientId },
+ { "client_secret", options.ClientSecret },
+ { "fb_exchange_token", accessToken },
+ }.ToQueryString();
+
+ var refreshResponse = await options.Backchannel.GetStringAsync(options.TokenEndpoint + query);
+ var payload = JObject.Parse(refreshResponse);
+
+ authProperties.UpdateTokenValue("access_token", payload.Value<string>("access_token"));
+ if (int.TryParse(payload.Value<string>("expires_in"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds))
+ {
+ var expiresAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(seconds);
+ authProperties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));
+ }
+ await context.SignInAsync(user, authProperties);
+
+ await PrintRefreshedTokensAsync(response, payload, authProperties);
+
+ return;
+ }
+
+ response.ContentType = "text/html";
+ await response.WriteAsync("<html><body>");
+ await response.WriteAsync("Refresh has not been implemented for this provider.<br>");
+ await response.WriteAsync("<a href=\"/\">Home</a>");
+ await response.WriteAsync("</body></html>");
+ });
+ });
+
+ // Sign-out to remove the user cookie.
+ app.Map("/logout", signoutApp =>
+ {
+ signoutApp.Run(async context =>
+ {
+ var response = context.Response;
+ response.ContentType = "text/html";
+ await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ await response.WriteAsync("<html><body>");
+ await response.WriteAsync("You have been logged out. Goodbye " + context.User.Identity.Name + "<br>");
+ await response.WriteAsync("<a href=\"/\">Home</a>");
+ await response.WriteAsync("</body></html>");
+ });
+ });
+
+ // Display the remote error
+ app.Map("/error", errorApp =>
+ {
+ errorApp.Run(async context =>
+ {
+ var response = context.Response;
+ response.ContentType = "text/html";
+ await response.WriteAsync("<html><body>");
+ await response.WriteAsync("An remote failure has occurred: " + context.Request.Query["FailureMessage"] + "<br>");
+ await response.WriteAsync("<a href=\"/\">Home</a>");
+ await response.WriteAsync("</body></html>");
+ });
+ });
+
+
+ app.Run(async context =>
+ {
+ // Setting DefaultAuthenticateScheme causes User to be set
+ var user = context.User;
+
+ // This is what [Authorize] calls
+ // var user = await context.AuthenticateAsync();
+
+ // This is what [Authorize(ActiveAuthenticationSchemes = MicrosoftAccountDefaults.AuthenticationScheme)] calls
+ // var user = await context.AuthenticateAsync(MicrosoftAccountDefaults.AuthenticationScheme);
+
+ // Deny anonymous request beyond this point.
+ if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated))
+ {
+ // This is what [Authorize] calls
+ // The cookie middleware will handle this and redirect to /login
+ await context.ChallengeAsync();
+
+ // This is what [Authorize(ActiveAuthenticationSchemes = MicrosoftAccountDefaults.AuthenticationScheme)] calls
+ // await context.ChallengeAsync(MicrosoftAccountDefaults.AuthenticationScheme);
+
+ return;
+ }
+
+ // Display user information
+ var response = context.Response;
+ response.ContentType = "text/html";
+ await response.WriteAsync("<html><body>");
+ await response.WriteAsync("Hello " + (context.User.Identity.Name ?? "anonymous") + "<br>");
+ foreach (var claim in context.User.Claims)
+ {
+ await response.WriteAsync(claim.Type + ": " + claim.Value + "<br>");
+ }
+
+ await response.WriteAsync("Tokens:<br>");
+
+ await response.WriteAsync("Access Token: " + await context.GetTokenAsync("access_token") + "<br>");
+ await response.WriteAsync("Refresh Token: " + await context.GetTokenAsync("refresh_token") + "<br>");
+ await response.WriteAsync("Token Type: " + await context.GetTokenAsync("token_type") + "<br>");
+ await response.WriteAsync("expires_at: " + await context.GetTokenAsync("expires_at") + "<br>");
+ await response.WriteAsync("<a href=\"/logout\">Logout</a><br>");
+ await response.WriteAsync("<a href=\"/refresh_token\">Refresh Token</a><br>");
+ await response.WriteAsync("</body></html>");
+ });
+ }
+
+ private Task<OAuthOptions> GetOAuthOptionsAsync(HttpContext context, string currentAuthType)
+ {
+ if (string.Equals(GoogleDefaults.AuthenticationScheme, currentAuthType))
+ {
+ return Task.FromResult<OAuthOptions>(context.RequestServices.GetRequiredService<IOptionsMonitor<GoogleOptions>>().Get(currentAuthType));
+ }
+ else if (string.Equals(MicrosoftAccountDefaults.AuthenticationScheme, currentAuthType))
+ {
+ return Task.FromResult<OAuthOptions>(context.RequestServices.GetRequiredService<IOptionsMonitor<MicrosoftAccountOptions>>().Get(currentAuthType));
+ }
+ else if (string.Equals(FacebookDefaults.AuthenticationScheme, currentAuthType))
+ {
+ return Task.FromResult<OAuthOptions>(context.RequestServices.GetRequiredService<IOptionsMonitor<FacebookOptions>>().Get(currentAuthType));
+ }
+
+ throw new NotImplementedException(currentAuthType);
+ }
+
+ private async Task PrintRefreshedTokensAsync(HttpResponse response, JObject payload, AuthenticationProperties authProperties)
+ {
+ response.ContentType = "text/html";
+ await response.WriteAsync("<html><body>");
+ await response.WriteAsync("Refreshed.<br>");
+ await response.WriteAsync(HtmlEncoder.Default.Encode(payload.ToString()).Replace(",", ",<br>") + "<br>");
+
+ await response.WriteAsync("<br>Tokens:<br>");
+
+ await response.WriteAsync("Access Token: " + authProperties.GetTokenValue("access_token") + "<br>");
+ await response.WriteAsync("Refresh Token: " + authProperties.GetTokenValue("refresh_token") + "<br>");
+ await response.WriteAsync("Token Type: " + authProperties.GetTokenValue("token_type") + "<br>");
+ await response.WriteAsync("expires_at: " + authProperties.GetTokenValue("expires_at") + "<br>");
+
+ await response.WriteAsync("<a href=\"/\">Home</a><br>");
+ await response.WriteAsync("<a href=\"/refresh_token\">Refresh Token</a><br>");
+ await response.WriteAsync("</body></html>");
+ }
+ }
+}
+
diff --git a/src/Security/samples/SocialSample/compiler/resources/cert.pfx b/src/Security/samples/SocialSample/compiler/resources/cert.pfx
new file mode 100644
index 0000000000..7118908c2d
--- /dev/null
+++ b/src/Security/samples/SocialSample/compiler/resources/cert.pfx
Binary files differ
diff --git a/src/Security/samples/SocialSample/web.config b/src/Security/samples/SocialSample/web.config
new file mode 100644
index 0000000000..f7ac679334
--- /dev/null
+++ b/src/Security/samples/SocialSample/web.config
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<configuration>
+ <system.webServer>
+ <handlers>
+ <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
+ </handlers>
+ <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" />
+ </system.webServer>
+</configuration> \ No newline at end of file
diff --git a/src/Security/samples/WsFedSample/Program.cs b/src/Security/samples/WsFedSample/Program.cs
new file mode 100644
index 0000000000..40e1945c69
--- /dev/null
+++ b/src/Security/samples/WsFedSample/Program.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Reflection;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging;
+
+namespace WsFedSample
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging(factory =>
+ {
+ factory.AddConsole();
+ factory.AddDebug();
+ factory.AddFilter("Console", level => level >= LogLevel.Information);
+ factory.AddFilter("Debug", level => level >= LogLevel.Information);
+ })
+ .UseKestrel(options =>
+ {
+ options.Listen(IPAddress.Loopback, 44307, listenOptions =>
+ {
+ // Configure SSL
+ var serverCertificate = LoadCertificate();
+ listenOptions.UseHttps(serverCertificate);
+ });
+ })
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseStartup<Startup>()
+ .Build();
+
+ host.Run();
+ }
+
+ private static X509Certificate2 LoadCertificate()
+ {
+ var assembly = typeof(Startup).GetTypeInfo().Assembly;
+ var embeddedFileProvider = new EmbeddedFileProvider(assembly, "WsFedSample");
+ var certificateFileInfo = embeddedFileProvider.GetFileInfo("compiler/resources/cert.pfx");
+ using (var certificateStream = certificateFileInfo.CreateReadStream())
+ {
+ byte[] certificatePayload;
+ using (var memoryStream = new MemoryStream())
+ {
+ certificateStream.CopyTo(memoryStream);
+ certificatePayload = memoryStream.ToArray();
+ }
+
+ return new X509Certificate2(certificatePayload, "testPassword");
+ }
+ }
+ }
+}
diff --git a/src/Security/samples/WsFedSample/Properties/launchSettings.json b/src/Security/samples/WsFedSample/Properties/launchSettings.json
new file mode 100644
index 0000000000..bdf80e2481
--- /dev/null
+++ b/src/Security/samples/WsFedSample/Properties/launchSettings.json
@@ -0,0 +1,28 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "https://localhost:44307/",
+ "sslPort": 44318
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "https://localhost:44307/",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "WsFedSample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:44307/",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/samples/WsFedSample/Startup.cs b/src/Security/samples/WsFedSample/Startup.cs
new file mode 100644
index 0000000000..0fc32769e9
--- /dev/null
+++ b/src/Security/samples/WsFedSample/Startup.cs
@@ -0,0 +1,168 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.WsFederation;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace WsFedSample
+{
+ public class Startup
+ {
+ public Startup(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+
+ public IConfiguration Configuration { get; }
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddAuthentication(sharedOptions =>
+ {
+ sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
+ })
+ .AddWsFederation(options =>
+ {
+ options.Wtrealm = "https://Tratcheroutlook.onmicrosoft.com/WsFedSample";
+ options.MetadataAddress = "https://login.windows.net/cdc690f9-b6b8-4023-813a-bae7143d1f87/FederationMetadata/2007-06/FederationMetadata.xml";
+ // options.CallbackPath = "/";
+ // options.SkipUnrecognizedRequests = true;
+ })
+ .AddCookie();
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseDeveloperExceptionPage();
+ app.UseAuthentication();
+
+ app.Run(async context =>
+ {
+ if (context.Request.Path.Equals("/signedout"))
+ {
+ await WriteHtmlAsync(context.Response, async res =>
+ {
+ await res.WriteAsync($"<h1>You have been signed out.</h1>");
+ await res.WriteAsync("<a class=\"btn btn-link\" href=\"/\">Sign In</a>");
+ });
+ return;
+ }
+
+ if (context.Request.Path.Equals("/signout"))
+ {
+ await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ await WriteHtmlAsync(context.Response, async res =>
+ {
+ await context.Response.WriteAsync($"<h1>Signed out {HtmlEncode(context.User.Identity.Name)}</h1>");
+ await context.Response.WriteAsync("<a class=\"btn btn-link\" href=\"/\">Sign In</a>");
+ });
+ return;
+ }
+
+ if (context.Request.Path.Equals("/signout-remote"))
+ {
+ // Redirects
+ await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ await context.SignOutAsync(WsFederationDefaults.AuthenticationScheme, new AuthenticationProperties()
+ {
+ RedirectUri = "/signedout"
+ });
+ return;
+ }
+
+ if (context.Request.Path.Equals("/Account/AccessDenied"))
+ {
+ await WriteHtmlAsync(context.Response, async res =>
+ {
+ await context.Response.WriteAsync($"<h1>Access Denied for user {HtmlEncode(context.User.Identity.Name)} to resource '{HtmlEncode(context.Request.Query["ReturnUrl"])}'</h1>");
+ await context.Response.WriteAsync("<a class=\"btn btn-link\" href=\"/signout\">Sign Out</a>");
+ });
+ return;
+ }
+
+ // DefaultAuthenticateScheme causes User to be set
+ var user = context.User;
+
+ // This is what [Authorize] calls
+ // var user = await context.AuthenticateAsync();
+
+ // This is what [Authorize(ActiveAuthenticationSchemes = WsFederationDefaults.AuthenticationScheme)] calls
+ // var user = await context.AuthenticateAsync(WsFederationDefaults.AuthenticationScheme);
+
+ // Not authenticated
+ if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated))
+ {
+ // This is what [Authorize] calls
+ await context.ChallengeAsync();
+
+ // This is what [Authorize(ActiveAuthenticationSchemes = WsFederationDefaults.AuthenticationScheme)] calls
+ // await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme);
+
+ return;
+ }
+
+ // Authenticated, but not authorized
+ if (context.Request.Path.Equals("/restricted") && !user.Identities.Any(identity => identity.HasClaim("special", "true")))
+ {
+ await context.ForbidAsync();
+ return;
+ }
+
+ await WriteHtmlAsync(context.Response, async response =>
+ {
+ await response.WriteAsync($"<h1>Hello Authenticated User {HtmlEncode(user.Identity.Name)}</h1>");
+ await response.WriteAsync("<a class=\"btn btn-default\" href=\"/restricted\">Restricted</a>");
+ await response.WriteAsync("<a class=\"btn btn-default\" href=\"/signout\">Sign Out</a>");
+ await response.WriteAsync("<a class=\"btn btn-default\" href=\"/signout-remote\">Sign Out Remote</a>");
+
+ await response.WriteAsync("<h2>Claims:</h2>");
+ await WriteTableHeader(response, new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value }));
+ });
+ });
+ }
+
+ private static async Task WriteHtmlAsync(HttpResponse response, Func<HttpResponse, Task> writeContent)
+ {
+ var bootstrap = "<link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css\" integrity=\"sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u\" crossorigin=\"anonymous\">";
+
+ response.ContentType = "text/html";
+ await response.WriteAsync($"<html><head>{bootstrap}</head><body><div class=\"container\">");
+ await writeContent(response);
+ await response.WriteAsync("</div></body></html>");
+ }
+
+ private static async Task WriteTableHeader(HttpResponse response, IEnumerable<string> columns, IEnumerable<IEnumerable<string>> data)
+ {
+ await response.WriteAsync("<table class=\"table table-condensed\">");
+ await response.WriteAsync("<tr>");
+ foreach (var column in columns)
+ {
+ await response.WriteAsync($"<th>{HtmlEncode(column)}</th>");
+ }
+ await response.WriteAsync("</tr>");
+ foreach (var row in data)
+ {
+ await response.WriteAsync("<tr>");
+ foreach (var column in row)
+ {
+ await response.WriteAsync($"<td>{HtmlEncode(column)}</td>");
+ }
+ await response.WriteAsync("</tr>");
+ }
+ await response.WriteAsync("</table>");
+ }
+
+ private static string HtmlEncode(string content) =>
+ string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content);
+ }
+}
diff --git a/src/Security/samples/WsFedSample/WsFedSample.csproj b/src/Security/samples/WsFedSample/WsFedSample.csproj
new file mode 100644
index 0000000000..bc3a59f10e
--- /dev/null
+++ b/src/Security/samples/WsFedSample/WsFedSample.csproj
@@ -0,0 +1,27 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.WsFederation\Microsoft.AspNetCore.Authentication.WsFederation.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="$(MicrosoftAspNetCoreServerKestrelHttpsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="$(MicrosoftExtensionsFileProvidersEmbeddedPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(MicrosoftExtensionsLoggingDebugPackageVersion)" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <EmbeddedResource Include="compiler\resources\cert.pfx" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/samples/WsFedSample/compiler/resources/cert.pfx b/src/Security/samples/WsFedSample/compiler/resources/cert.pfx
new file mode 100644
index 0000000000..7118908c2d
--- /dev/null
+++ b/src/Security/samples/WsFedSample/compiler/resources/cert.pfx
Binary files differ
diff --git a/src/Security/shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs b/src/Security/shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs
new file mode 100644
index 0000000000..42cc4e2f0f
--- /dev/null
+++ b/src/Security/shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs
@@ -0,0 +1,309 @@
+// 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.Globalization;
+using System.Linq;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+// Keep the type public for Security repo as it would be a breaking change to change the accessor now.
+// Make this type internal for other repos as it could be used by multiple projects and having it public causes type conflicts.
+#if SECURITY
+namespace Microsoft.AspNetCore.Authentication.Cookies
+{
+ /// <summary>
+ /// This handles cookies that are limited by per cookie length. It breaks down long cookies for responses, and reassembles them
+ /// from requests.
+ /// </summary>
+ public class ChunkingCookieManager : ICookieManager
+ {
+#else
+namespace Microsoft.AspNetCore.Internal
+{
+ /// <summary>
+ /// This handles cookies that are limited by per cookie length. It breaks down long cookies for responses, and reassembles them
+ /// from requests.
+ /// </summary>
+ internal class ChunkingCookieManager
+ {
+#endif
+ /// <summary>
+ /// The default maximum size of characters in a cookie to send back to the client.
+ /// </summary>
+ public const int DefaultChunkSize = 4050;
+
+ private const string ChunkKeySuffix = "C";
+ private const string ChunkCountPrefix = "chunks-";
+
+ public ChunkingCookieManager()
+ {
+ // Lowest common denominator. Safari has the lowest known limit (4093), and we leave little extra just in case.
+ // See http://browsercookielimits.x64.me/.
+ // Leave at least 40 in case CookiePolicy tries to add 'secure', 'samesite=strict' and/or 'httponly'.
+ ChunkSize = DefaultChunkSize;
+ }
+
+ /// <summary>
+ /// The maximum size of cookie to send back to the client. If a cookie exceeds this size it will be broken down into multiple
+ /// cookies. Set this value to null to disable this behavior. The default is 4090 characters, which is supported by all
+ /// common browsers.
+ ///
+ /// Note that browsers may also have limits on the total size of all cookies per domain, and on the number of cookies per domain.
+ /// </summary>
+ public int? ChunkSize { get; set; }
+
+ /// <summary>
+ /// Throw if not all chunks of a cookie are available on a request for re-assembly.
+ /// </summary>
+ public bool ThrowForPartialCookies { get; set; }
+
+ // Parse the "chunks-XX" to determine how many chunks there should be.
+ private static int ParseChunksCount(string value)
+ {
+ if (value != null && value.StartsWith(ChunkCountPrefix, StringComparison.Ordinal))
+ {
+ var chunksCountString = value.Substring(ChunkCountPrefix.Length);
+ int chunksCount;
+ if (int.TryParse(chunksCountString, NumberStyles.None, CultureInfo.InvariantCulture, out chunksCount))
+ {
+ return chunksCount;
+ }
+ }
+ return 0;
+ }
+
+ /// <summary>
+ /// Get the reassembled cookie. Non chunked cookies are returned normally.
+ /// Cookies with missing chunks just have their "chunks-XX" header returned.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="key"></param>
+ /// <returns>The reassembled cookie, if any, or null.</returns>
+ public string GetRequestCookie(HttpContext context, string key)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ var requestCookies = context.Request.Cookies;
+ var value = requestCookies[key];
+ var chunksCount = ParseChunksCount(value);
+ if (chunksCount > 0)
+ {
+ var chunks = new string[chunksCount];
+ for (var chunkId = 1; chunkId <= chunksCount; chunkId++)
+ {
+ var chunk = requestCookies[key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture)];
+ if (string.IsNullOrEmpty(chunk))
+ {
+ if (ThrowForPartialCookies)
+ {
+ var totalSize = 0;
+ for (int i = 0; i < chunkId - 1; i++)
+ {
+ totalSize += chunks[i].Length;
+ }
+ throw new FormatException(
+ string.Format(
+ CultureInfo.CurrentCulture,
+ "The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded.",
+ chunkId - 1,
+ chunksCount,
+ totalSize));
+ }
+ // Missing chunk, abort by returning the original cookie value. It may have been a false positive?
+ return value;
+ }
+
+ chunks[chunkId - 1] = chunk;
+ }
+
+ return string.Join(string.Empty, chunks);
+ }
+ return value;
+ }
+
+ /// <summary>
+ /// Appends a new response cookie to the Set-Cookie header. If the cookie is larger than the given size limit
+ /// then it will be broken down into multiple cookies as follows:
+ /// Set-Cookie: CookieName=chunks-3; path=/
+ /// Set-Cookie: CookieNameC1=Segment1; path=/
+ /// Set-Cookie: CookieNameC2=Segment2; path=/
+ /// Set-Cookie: CookieNameC3=Segment3; path=/
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="key"></param>
+ /// <param name="value"></param>
+ /// <param name="options"></param>
+ public void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ var template = new SetCookieHeaderValue(key)
+ {
+ Domain = options.Domain,
+ Expires = options.Expires,
+ SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite,
+ HttpOnly = options.HttpOnly,
+ Path = options.Path,
+ Secure = options.Secure,
+ };
+
+ var templateLength = template.ToString().Length;
+
+ value = value ?? string.Empty;
+
+ // Normal cookie
+ var responseCookies = context.Response.Cookies;
+ if (!ChunkSize.HasValue || ChunkSize.Value > templateLength + value.Length)
+ {
+ responseCookies.Append(key, value, options);
+ }
+ else if (ChunkSize.Value < templateLength + 10)
+ {
+ // 10 is the minimum data we want to put in an individual cookie, including the cookie chunk identifier "CXX".
+ // No room for data, we can't chunk the options and name
+ throw new InvalidOperationException("The cookie key and options are larger than ChunksSize, leaving no room for data.");
+ }
+ else
+ {
+ // Break the cookie down into multiple cookies.
+ // Key = CookieName, value = "Segment1Segment2Segment2"
+ // Set-Cookie: CookieName=chunks-3; path=/
+ // Set-Cookie: CookieNameC1="Segment1"; path=/
+ // Set-Cookie: CookieNameC2="Segment2"; path=/
+ // Set-Cookie: CookieNameC3="Segment3"; path=/
+ var dataSizePerCookie = ChunkSize.Value - templateLength - 3; // Budget 3 chars for the chunkid.
+ var cookieChunkCount = (int)Math.Ceiling(value.Length * 1.0 / dataSizePerCookie);
+
+ responseCookies.Append(key, ChunkCountPrefix + cookieChunkCount.ToString(CultureInfo.InvariantCulture), options);
+
+ var offset = 0;
+ for (var chunkId = 1; chunkId <= cookieChunkCount; chunkId++)
+ {
+ var remainingLength = value.Length - offset;
+ var length = Math.Min(dataSizePerCookie, remainingLength);
+ var segment = value.Substring(offset, length);
+ offset += length;
+
+ responseCookies.Append(key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture), segment, options);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Deletes the cookie with the given key by setting an expired state. If a matching chunked cookie exists on
+ /// the request, delete each chunk.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="key"></param>
+ /// <param name="options"></param>
+ public void DeleteCookie(HttpContext context, string key, CookieOptions options)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ var keys = new List<string>();
+ keys.Add(key + "=");
+
+ var requestCookie = context.Request.Cookies[key];
+ var chunks = ParseChunksCount(requestCookie);
+ if (chunks > 0)
+ {
+ for (int i = 1; i <= chunks + 1; i++)
+ {
+ var subkey = key + ChunkKeySuffix + i.ToString(CultureInfo.InvariantCulture);
+ keys.Add(subkey + "=");
+ }
+ }
+
+ var domainHasValue = !string.IsNullOrEmpty(options.Domain);
+ var pathHasValue = !string.IsNullOrEmpty(options.Path);
+
+ Func<string, bool> rejectPredicate;
+ Func<string, bool> predicate = value => keys.Any(k => value.StartsWith(k, StringComparison.OrdinalIgnoreCase));
+ if (domainHasValue)
+ {
+ rejectPredicate = value => predicate(value) && value.IndexOf("domain=" + options.Domain, StringComparison.OrdinalIgnoreCase) != -1;
+ }
+ else if (pathHasValue)
+ {
+ rejectPredicate = value => predicate(value) && value.IndexOf("path=" + options.Path, StringComparison.OrdinalIgnoreCase) != -1;
+ }
+ else
+ {
+ rejectPredicate = value => predicate(value);
+ }
+
+ var responseHeaders = context.Response.Headers;
+ var existingValues = responseHeaders[HeaderNames.SetCookie];
+ if (!StringValues.IsNullOrEmpty(existingValues))
+ {
+ responseHeaders[HeaderNames.SetCookie] = existingValues.Where(value => !rejectPredicate(value)).ToArray();
+ }
+
+ AppendResponseCookie(
+ context,
+ key,
+ string.Empty,
+ new CookieOptions()
+ {
+ Path = options.Path,
+ Domain = options.Domain,
+ SameSite = options.SameSite,
+ IsEssential = options.IsEssential,
+ Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ });
+
+ for (int i = 1; i <= chunks; i++)
+ {
+ AppendResponseCookie(
+ context,
+ key + "C" + i.ToString(CultureInfo.InvariantCulture),
+ string.Empty,
+ new CookieOptions()
+ {
+ Path = options.Path,
+ Domain = options.Domain,
+ SameSite = options.SameSite,
+ IsEssential = options.IsEssential,
+ Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ });
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Directory.Build.props b/src/Security/src/Directory.Build.props
new file mode 100644
index 0000000000..1e0980f663
--- /dev/null
+++ b/src/Security/src/Directory.Build.props
@@ -0,0 +1,7 @@
+<Project>
+ <Import Project="..\Directory.Build.props" />
+
+ <ItemGroup>
+ <PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Constants.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Constants.cs
new file mode 100644
index 0000000000..3aabf94c15
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Constants.cs
@@ -0,0 +1,13 @@
+// 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.Authentication.Cookies
+{
+ internal static class Constants
+ {
+ internal static class Headers
+ {
+ internal const string SetCookie = "Set-Cookie";
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAppBuilderExtensions.cs
new file mode 100644
index 0000000000..bdfd43c796
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAppBuilderExtensions.cs
@@ -0,0 +1,37 @@
+// 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.Authentication.Cookies;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Extension methods to add cookie authentication capabilities to an HTTP application pipeline.
+ /// </summary>
+ public static class CookieAppBuilderExtensions
+ {
+ /// <summary>
+ /// UseCookieAuthentication is obsolete. Configure Cookie authentication with AddAuthentication().AddCookie in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseCookieAuthentication is obsolete. Configure Cookie authentication with AddAuthentication().AddCookie in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseCookieAuthentication(this IApplicationBuilder app)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+
+ /// <summary>
+ /// UseCookieAuthentication is obsolete. Configure Cookie authentication with AddAuthentication().AddCookie in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <param name="options">A <see cref="CookieAuthenticationOptions"/> that specifies options for the handler.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseCookieAuthentication is obsolete. Configure Cookie authentication with AddAuthentication().AddCookie in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseCookieAuthentication(this IApplicationBuilder app, CookieAuthenticationOptions options)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationDefaults.cs
new file mode 100644
index 0000000000..700b607976
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationDefaults.cs
@@ -0,0 +1,46 @@
+// 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.Authentication.Cookies
+{
+ /// <summary>
+ /// Default values related to cookie-based authentication handler
+ /// </summary>
+ public static class CookieAuthenticationDefaults
+ {
+ /// <summary>
+ /// The default value used for CookieAuthenticationOptions.AuthenticationScheme
+ /// </summary>
+ public const string AuthenticationScheme = "Cookies";
+
+ /// <summary>
+ /// The prefix used to provide a default CookieAuthenticationOptions.CookieName
+ /// </summary>
+ public static readonly string CookiePrefix = ".AspNetCore.";
+
+ /// <summary>
+ /// The default value used by CookieAuthenticationMiddleware for the
+ /// CookieAuthenticationOptions.LoginPath
+ /// </summary>
+ public static readonly PathString LoginPath = new PathString("/Account/Login");
+
+ /// <summary>
+ /// The default value used by CookieAuthenticationMiddleware for the
+ /// CookieAuthenticationOptions.LogoutPath
+ /// </summary>
+ public static readonly PathString LogoutPath = new PathString("/Account/Logout");
+
+ /// <summary>
+ /// The default value used by CookieAuthenticationMiddleware for the
+ /// CookieAuthenticationOptions.AccessDeniedPath
+ /// </summary>
+ public static readonly PathString AccessDeniedPath = new PathString("/Account/AccessDenied");
+
+ /// <summary>
+ /// The default value of the CookieAuthenticationOptions.ReturnUrlParameter
+ /// </summary>
+ public static readonly string ReturnUrlParameter = "ReturnUrl";
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs
new file mode 100644
index 0000000000..b77a51ef4f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs
@@ -0,0 +1,451 @@
+// 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.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Authentication.Cookies
+{
+ public class CookieAuthenticationHandler : SignInAuthenticationHandler<CookieAuthenticationOptions>
+ {
+ private const string HeaderValueNoCache = "no-cache";
+ private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT";
+ private const string SessionIdClaim = "Microsoft.AspNetCore.Authentication.Cookies-SessionId";
+
+ private bool _shouldRefresh;
+ private bool _signInCalled;
+ private bool _signOutCalled;
+
+ private DateTimeOffset? _refreshIssuedUtc;
+ private DateTimeOffset? _refreshExpiresUtc;
+ private string _sessionKey;
+ private Task<AuthenticateResult> _readCookieTask;
+ private AuthenticationTicket _refreshTicket;
+
+ public CookieAuthenticationHandler(IOptionsMonitor<CookieAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
+ : base(options, logger, encoder, clock)
+ { }
+
+ /// <summary>
+ /// The handler calls methods on the events which give the application control at certain points where processing is occurring.
+ /// If it is not provided a default instance is supplied which does nothing when the methods are called.
+ /// </summary>
+ protected new CookieAuthenticationEvents Events
+ {
+ get { return (CookieAuthenticationEvents)base.Events; }
+ set { base.Events = value; }
+ }
+
+ protected override Task InitializeHandlerAsync()
+ {
+ // Cookies needs to finish the response
+ Context.Response.OnStarting(FinishResponseAsync);
+ return Task.CompletedTask;
+ }
+
+ /// <summary>
+ /// Creates a new instance of the events instance.
+ /// </summary>
+ /// <returns>A new instance of the events instance.</returns>
+ protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new CookieAuthenticationEvents());
+
+ private Task<AuthenticateResult> EnsureCookieTicket()
+ {
+ // We only need to read the ticket once
+ if (_readCookieTask == null)
+ {
+ _readCookieTask = ReadCookieTicket();
+ }
+ return _readCookieTask;
+ }
+
+ private void CheckForRefresh(AuthenticationTicket ticket)
+ {
+ var currentUtc = Clock.UtcNow;
+ var issuedUtc = ticket.Properties.IssuedUtc;
+ var expiresUtc = ticket.Properties.ExpiresUtc;
+ var allowRefresh = ticket.Properties.AllowRefresh ?? true;
+ if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration && allowRefresh)
+ {
+ var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
+ var timeRemaining = expiresUtc.Value.Subtract(currentUtc);
+
+ if (timeRemaining < timeElapsed)
+ {
+ RequestRefresh(ticket);
+ }
+ }
+ }
+
+ private void RequestRefresh(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal = null)
+ {
+ var issuedUtc = ticket.Properties.IssuedUtc;
+ var expiresUtc = ticket.Properties.ExpiresUtc;
+
+ if (issuedUtc != null && expiresUtc != null)
+ {
+ _shouldRefresh = true;
+ var currentUtc = Clock.UtcNow;
+ _refreshIssuedUtc = currentUtc;
+ var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value);
+ _refreshExpiresUtc = currentUtc.Add(timeSpan);
+ _refreshTicket = CloneTicket(ticket, replacedPrincipal);
+ }
+ }
+
+ private AuthenticationTicket CloneTicket(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal)
+ {
+ var principal = replacedPrincipal ?? ticket.Principal;
+ var newPrincipal = new ClaimsPrincipal();
+ foreach (var identity in principal.Identities)
+ {
+ newPrincipal.AddIdentity(identity.Clone());
+ }
+
+ var newProperties = new AuthenticationProperties();
+ foreach (var item in ticket.Properties.Items)
+ {
+ newProperties.Items[item.Key] = item.Value;
+ }
+
+ return new AuthenticationTicket(newPrincipal, newProperties, ticket.AuthenticationScheme);
+ }
+
+ private async Task<AuthenticateResult> ReadCookieTicket()
+ {
+ var cookie = Options.CookieManager.GetRequestCookie(Context, Options.Cookie.Name);
+ if (string.IsNullOrEmpty(cookie))
+ {
+ return AuthenticateResult.NoResult();
+ }
+
+ var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding());
+ if (ticket == null)
+ {
+ return AuthenticateResult.Fail("Unprotect ticket failed");
+ }
+
+ if (Options.SessionStore != null)
+ {
+ var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim));
+ if (claim == null)
+ {
+ return AuthenticateResult.Fail("SessionId missing");
+ }
+ _sessionKey = claim.Value;
+ ticket = await Options.SessionStore.RetrieveAsync(_sessionKey);
+ if (ticket == null)
+ {
+ return AuthenticateResult.Fail("Identity missing in session store");
+ }
+ }
+
+ var currentUtc = Clock.UtcNow;
+ var expiresUtc = ticket.Properties.ExpiresUtc;
+
+ if (expiresUtc != null && expiresUtc.Value < currentUtc)
+ {
+ if (Options.SessionStore != null)
+ {
+ await Options.SessionStore.RemoveAsync(_sessionKey);
+ }
+ return AuthenticateResult.Fail("Ticket expired");
+ }
+
+ CheckForRefresh(ticket);
+
+ // Finally we have a valid ticket
+ return AuthenticateResult.Success(ticket);
+ }
+
+ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
+ {
+ var result = await EnsureCookieTicket();
+ if (!result.Succeeded)
+ {
+ return result;
+ }
+
+ var context = new CookieValidatePrincipalContext(Context, Scheme, Options, result.Ticket);
+ await Events.ValidatePrincipal(context);
+
+ if (context.Principal == null)
+ {
+ return AuthenticateResult.Fail("No principal.");
+ }
+
+ if (context.ShouldRenew)
+ {
+ RequestRefresh(result.Ticket, context.Principal);
+ }
+
+ return AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name));
+ }
+
+ private CookieOptions BuildCookieOptions()
+ {
+ var cookieOptions = Options.Cookie.Build(Context);
+ // ignore the 'Expires' value as this will be computed elsewhere
+ cookieOptions.Expires = null;
+
+ return cookieOptions;
+ }
+
+ protected virtual async Task FinishResponseAsync()
+ {
+ // Only renew if requested, and neither sign in or sign out was called
+ if (!_shouldRefresh || _signInCalled || _signOutCalled)
+ {
+ return;
+ }
+
+ var ticket = _refreshTicket;
+ if (ticket != null)
+ {
+ var properties = ticket.Properties;
+
+ if (_refreshIssuedUtc.HasValue)
+ {
+ properties.IssuedUtc = _refreshIssuedUtc;
+ }
+
+ if (_refreshExpiresUtc.HasValue)
+ {
+ properties.ExpiresUtc = _refreshExpiresUtc;
+ }
+
+ if (Options.SessionStore != null && _sessionKey != null)
+ {
+ await Options.SessionStore.RenewAsync(_sessionKey, ticket);
+ var principal = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
+ Scheme.Name));
+ ticket = new AuthenticationTicket(principal, null, Scheme.Name);
+ }
+
+ var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());
+
+ var cookieOptions = BuildCookieOptions();
+ if (properties.IsPersistent && _refreshExpiresUtc.HasValue)
+ {
+ cookieOptions.Expires = _refreshExpiresUtc.Value.ToUniversalTime();
+ }
+
+ Options.CookieManager.AppendResponseCookie(
+ Context,
+ Options.Cookie.Name,
+ cookieValue,
+ cookieOptions);
+
+ await ApplyHeaders(shouldRedirectToReturnUrl: false, properties: properties);
+ }
+ }
+
+ protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ properties = properties ?? new AuthenticationProperties();
+
+ _signInCalled = true;
+
+ // Process the request cookie to initialize members like _sessionKey.
+ await EnsureCookieTicket();
+ var cookieOptions = BuildCookieOptions();
+
+ var signInContext = new CookieSigningInContext(
+ Context,
+ Scheme,
+ Options,
+ user,
+ properties,
+ cookieOptions);
+
+ DateTimeOffset issuedUtc;
+ if (signInContext.Properties.IssuedUtc.HasValue)
+ {
+ issuedUtc = signInContext.Properties.IssuedUtc.Value;
+ }
+ else
+ {
+ issuedUtc = Clock.UtcNow;
+ signInContext.Properties.IssuedUtc = issuedUtc;
+ }
+
+ if (!signInContext.Properties.ExpiresUtc.HasValue)
+ {
+ signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan);
+ }
+
+ await Events.SigningIn(signInContext);
+
+ if (signInContext.Properties.IsPersistent)
+ {
+ var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan);
+ signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime();
+ }
+
+ var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name);
+
+ if (Options.SessionStore != null)
+ {
+ if (_sessionKey != null)
+ {
+ await Options.SessionStore.RemoveAsync(_sessionKey);
+ }
+ _sessionKey = await Options.SessionStore.StoreAsync(ticket);
+ var principal = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
+ Options.ClaimsIssuer));
+ ticket = new AuthenticationTicket(principal, null, Scheme.Name);
+ }
+
+ var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());
+
+ Options.CookieManager.AppendResponseCookie(
+ Context,
+ Options.Cookie.Name,
+ cookieValue,
+ signInContext.CookieOptions);
+
+ var signedInContext = new CookieSignedInContext(
+ Context,
+ Scheme,
+ signInContext.Principal,
+ signInContext.Properties,
+ Options);
+
+ await Events.SignedIn(signedInContext);
+
+ // Only redirect on the login path
+ var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath;
+ await ApplyHeaders(shouldRedirect, signedInContext.Properties);
+
+ Logger.SignedIn(Scheme.Name);
+ }
+
+ protected async override Task HandleSignOutAsync(AuthenticationProperties properties)
+ {
+ properties = properties ?? new AuthenticationProperties();
+
+ _signOutCalled = true;
+
+ // Process the request cookie to initialize members like _sessionKey.
+ await EnsureCookieTicket();
+ var cookieOptions = BuildCookieOptions();
+ if (Options.SessionStore != null && _sessionKey != null)
+ {
+ await Options.SessionStore.RemoveAsync(_sessionKey);
+ }
+
+ var context = new CookieSigningOutContext(
+ Context,
+ Scheme,
+ Options,
+ properties,
+ cookieOptions);
+
+ await Events.SigningOut(context);
+
+ Options.CookieManager.DeleteCookie(
+ Context,
+ Options.Cookie.Name,
+ context.CookieOptions);
+
+ // Only redirect on the logout path
+ var shouldRedirect = Options.LogoutPath.HasValue && OriginalPath == Options.LogoutPath;
+ await ApplyHeaders(shouldRedirect, context.Properties);
+
+ Logger.SignedOut(Scheme.Name);
+ }
+
+ private async Task ApplyHeaders(bool shouldRedirectToReturnUrl, AuthenticationProperties properties)
+ {
+ Response.Headers[HeaderNames.CacheControl] = HeaderValueNoCache;
+ Response.Headers[HeaderNames.Pragma] = HeaderValueNoCache;
+ Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate;
+
+ if (shouldRedirectToReturnUrl && Response.StatusCode == 200)
+ {
+ // set redirect uri in order:
+ // 1. properties.RedirectUri
+ // 2. query parameter ReturnUrlParameter
+ //
+ // Absolute uri is not allowed if it is from query string as query string is not
+ // a trusted source.
+ var redirectUri = properties.RedirectUri;
+ if (string.IsNullOrEmpty(redirectUri))
+ {
+ redirectUri = Request.Query[Options.ReturnUrlParameter];
+ if (string.IsNullOrEmpty(redirectUri) || !IsHostRelative(redirectUri))
+ {
+ redirectUri = null;
+ }
+ }
+
+ if (redirectUri != null)
+ {
+ await Events.RedirectToReturnUrl(
+ new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, redirectUri));
+ }
+ }
+ }
+
+ private static bool IsHostRelative(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ return false;
+ }
+ if (path.Length == 1)
+ {
+ return path[0] == '/';
+ }
+ return path[0] == '/' && path[1] != '/' && path[1] != '\\';
+ }
+
+ protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
+ {
+ var returnUrl = properties.RedirectUri;
+ if (string.IsNullOrEmpty(returnUrl))
+ {
+ returnUrl = OriginalPathBase + Request.Path + Request.QueryString;
+ }
+ var accessDeniedUri = Options.AccessDeniedPath + QueryString.Create(Options.ReturnUrlParameter, returnUrl);
+ var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(accessDeniedUri));
+ await Events.RedirectToAccessDenied(redirectContext);
+ }
+
+ protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
+ {
+ var redirectUri = properties.RedirectUri;
+ if (string.IsNullOrEmpty(redirectUri))
+ {
+ redirectUri = OriginalPathBase + Request.Path + Request.QueryString;
+ }
+
+ var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri);
+ var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(loginUri));
+ await Events.RedirectToLogin(redirectContext);
+ }
+
+ private string GetTlsTokenBinding()
+ {
+ var binding = Context.Features.Get<ITlsTokenBindingFeature>()?.GetProvidedTokenBindingId();
+ return binding == null ? null : Convert.ToBase64String(binding);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs
new file mode 100644
index 0000000000..35017f9c4d
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs
@@ -0,0 +1,214 @@
+// 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.Authentication.Internal;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.Cookies
+{
+ /// <summary>
+ /// Configuration options for <see cref="CookieAuthenticationOptions"/>.
+ /// </summary>
+ public class CookieAuthenticationOptions : AuthenticationSchemeOptions
+ {
+ private CookieBuilder _cookieBuilder = new RequestPathBaseCookieBuilder
+ {
+ // the default name is configured in PostConfigureCookieAuthenticationOptions
+
+ // To support OAuth authentication, a lax mode is required, see https://github.com/aspnet/Security/issues/1231.
+ SameSite = SameSiteMode.Lax,
+ HttpOnly = true,
+ SecurePolicy = CookieSecurePolicy.SameAsRequest,
+ IsEssential = true,
+ };
+
+ /// <summary>
+ /// Create an instance of the options initialized with the default values
+ /// </summary>
+ public CookieAuthenticationOptions()
+ {
+ ExpireTimeSpan = TimeSpan.FromDays(14);
+ ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
+ SlidingExpiration = true;
+ Events = new CookieAuthenticationEvents();
+ }
+
+ /// <summary>
+ /// <para>
+ /// Determines the settings used to create the cookie.
+ /// </para>
+ /// <para>
+ /// <seealso cref="CookieBuilder.SameSite"/> defaults to <see cref="SameSiteMode.Lax"/>.
+ /// <seealso cref="CookieBuilder.HttpOnly"/> defaults to <c>true</c>.
+ /// <seealso cref="CookieBuilder.SecurePolicy"/> defaults to <see cref="CookieSecurePolicy.SameAsRequest"/>.
+ /// </para>
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The default value for cookie name is ".AspNetCore.Cookies".
+ /// This value should be changed if you change the name of the AuthenticationScheme, especially if your
+ /// system uses the cookie authentication handler multiple times.
+ /// </para>
+ /// <para>
+ /// <seealso cref="CookieBuilder.SameSite"/> determines if the browser should allow the cookie to be attached to same-site or cross-site requests.
+ /// The default is Lax, which means the cookie is only allowed to be attached to cross-site requests using safe HTTP methods and same-site requests.
+ /// </para>
+ /// <para>
+ /// <seealso cref="CookieBuilder.HttpOnly"/> determines if the browser should allow the cookie to be accessed by client-side javascript.
+ /// The default is true, which means the cookie will only be passed to http requests and is not made available to script on the page.
+ /// </para>
+ /// <para>
+ /// <seealso cref="CookieBuilder.Expiration"/> is currently ignored. Use <see cref="ExpireTimeSpan"/> to control lifetime of cookie authentication.
+ /// </para>
+ /// </remarks>
+ public CookieBuilder Cookie
+ {
+ get => _cookieBuilder;
+ set => _cookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ /// <summary>
+ /// If set this will be used by the CookieAuthenticationHandler for data protection.
+ /// </summary>
+ public IDataProtectionProvider DataProtectionProvider { get; set; }
+
+ /// <summary>
+ /// The SlidingExpiration is set to true to instruct the handler to re-issue a new cookie with a new
+ /// expiration time any time it processes a request which is more than halfway through the expiration window.
+ /// </summary>
+ public bool SlidingExpiration { get; set; }
+
+ /// <summary>
+ /// The LoginPath property is used by the handler for the redirection target when handling ChallengeAsync.
+ /// The current url which is added to the LoginPath as a query string parameter named by the ReturnUrlParameter.
+ /// Once a request to the LoginPath grants a new SignIn identity, the ReturnUrlParameter value is used to redirect
+ /// the browser back to the original url.
+ /// </summary>
+ public PathString LoginPath { get; set; }
+
+ /// <summary>
+ /// If the LogoutPath is provided the handler then a request to that path will redirect based on the ReturnUrlParameter.
+ /// </summary>
+ public PathString LogoutPath { get; set; }
+
+ /// <summary>
+ /// The AccessDeniedPath property is used by the handler for the redirection target when handling ForbidAsync.
+ /// </summary>
+ public PathString AccessDeniedPath { get; set; }
+
+ /// <summary>
+ /// The ReturnUrlParameter determines the name of the query string parameter which is appended by the handler
+ /// when during a Challenge. This is also the query string parameter looked for when a request arrives on the
+ /// login path or logout path, in order to return to the original url after the action is performed.
+ /// </summary>
+ public string ReturnUrlParameter { get; set; }
+
+ /// <summary>
+ /// The Provider may be assigned to an instance of an object created by the application at startup time. The handler
+ /// calls methods on the provider which give the application control at certain points where processing is occurring.
+ /// If it is not provided a default instance is supplied which does nothing when the methods are called.
+ /// </summary>
+ public new CookieAuthenticationEvents Events
+ {
+ get => (CookieAuthenticationEvents)base.Events;
+ set => base.Events = value;
+ }
+
+ /// <summary>
+ /// The TicketDataFormat is used to protect and unprotect the identity and other properties which are stored in the
+ /// cookie value. If not provided one will be created using <see cref="DataProtectionProvider"/>.
+ /// </summary>
+ public ISecureDataFormat<AuthenticationTicket> TicketDataFormat { get; set; }
+
+ /// <summary>
+ /// The component used to get cookies from the request or set them on the response.
+ ///
+ /// ChunkingCookieManager will be used by default.
+ /// </summary>
+ public ICookieManager CookieManager { get; set; }
+
+ /// <summary>
+ /// An optional container in which to store the identity across requests. When used, only a session identifier is sent
+ /// to the client. This can be used to mitigate potential problems with very large identities.
+ /// </summary>
+ public ITicketStore SessionStore { get; set; }
+
+ /// <summary>
+ /// <para>
+ /// Controls how much time the authentication ticket stored in the cookie will remain valid from the point it is created
+ /// The expiration information is stored in the protected cookie ticket. Because of that an expired cookie will be ignored
+ /// even if it is passed to the server after the browser should have purged it.
+ /// </para>
+ /// <para>
+ /// This is separate from the value of <seealso cref="CookieOptions.Expires"/>, which specifies
+ /// how long the browser will keep the cookie.
+ /// </para>
+ /// </summary>
+ public TimeSpan ExpireTimeSpan { get; set; }
+
+ #region Obsolete API
+ /// <summary>
+ /// <para>
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is <seealso cref="CookieBuilder.Name"/> on <see cref="Cookie"/>.
+ /// </para>
+ /// <para>
+ /// Determines the cookie name used to persist the identity. The default value is ".AspNetCore.Cookies".
+ /// This value should be changed if you change the name of the AuthenticationScheme, especially if your
+ /// system uses the cookie authentication handler multiple times.
+ /// </para>
+ /// </summary>
+ [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; }
+
+ /// <summary>
+ /// <para>
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is <seealso cref="CookieBuilder.Domain"/> on <see cref="Cookie"/>.
+ /// </para>
+ /// <para>
+ /// Determines the domain used to create the cookie. Is not provided by default.
+ /// </para>
+ /// </summary>
+ [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; }
+
+ /// <summary>
+ /// <para>
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is <seealso cref="CookieBuilder.Path"/> on <see cref="Cookie"/>.
+ /// </para>
+ /// <para>
+ /// Determines the path used to create the cookie. The default value is "/" for highest browser compatibility.
+ /// </para>
+ /// </summary>
+ [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Path) + ".")]
+ public string CookiePath { get => Cookie.Path; set => Cookie.Path = value; }
+
+ /// <summary>
+ /// <para>
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is <seealso cref="CookieBuilder.HttpOnly"/> on <see cref="Cookie"/>.
+ /// </para>
+ /// <para>
+ /// Determines if the browser should allow the cookie to be accessed by client-side javascript. The
+ /// default is true, which means the cookie will only be passed to http requests and is not made available
+ /// to script on the page.
+ /// </para>
+ /// </summary>
+ [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.HttpOnly) + ".")]
+ public bool CookieHttpOnly { get => Cookie.HttpOnly; set => Cookie.HttpOnly = value; }
+
+ /// <summary>
+ /// <para>
+ /// This property is obsolete and will be removed in a future version. The recommended alternative is <seealso cref="CookieBuilder.SecurePolicy"/> on <see cref="Cookie"/>.
+ /// </para>
+ /// <para>
+ /// Determines if the cookie should only be transmitted on HTTPS request. The default is to limit the cookie
+ /// to HTTPS requests if the page which is doing the SignIn is also HTTPS. If you have an HTTPS sign in page
+ /// and portions of your site are HTTP you may need to change this value.
+ /// </para>
+ /// </summary>
+ [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.SecurePolicy) + ".")]
+ public CookieSecurePolicy CookieSecure { get => Cookie.SecurePolicy; set => Cookie.SecurePolicy = value; }
+ #endregion
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieExtensions.cs
new file mode 100644
index 0000000000..4c41f54a9c
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/CookieExtensions.cs
@@ -0,0 +1,32 @@
+// 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.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class CookieExtensions
+ {
+ public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder)
+ => builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
+
+ public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme)
+ => builder.AddCookie(authenticationScheme, configureOptions: null);
+
+ public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, Action<CookieAuthenticationOptions> configureOptions)
+ => builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, configureOptions);
+
+ public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, Action<CookieAuthenticationOptions> configureOptions)
+ => builder.AddCookie(authenticationScheme, displayName: null, configureOptions: configureOptions);
+
+ public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<CookieAuthenticationOptions> configureOptions)
+ {
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
+ return builder.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(authenticationScheme, displayName, configureOptions);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieAuthenticationEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieAuthenticationEvents.cs
new file mode 100644
index 0000000000..2b8b0416b3
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieAuthenticationEvents.cs
@@ -0,0 +1,158 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.Cookies
+{
+ /// <summary>
+ /// This default implementation of the ICookieAuthenticationEvents may be used if the
+ /// application only needs to override a few of the interface methods. This may be used as a base class
+ /// or may be instantiated directly.
+ /// </summary>
+ public class CookieAuthenticationEvents
+ {
+ /// <summary>
+ /// A delegate assigned to this property will be invoked when the related method is called.
+ /// </summary>
+ public Func<CookieValidatePrincipalContext, Task> OnValidatePrincipal { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// A delegate assigned to this property will be invoked when the related method is called.
+ /// </summary>
+ public Func<CookieSigningInContext, Task> OnSigningIn { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// A delegate assigned to this property will be invoked when the related method is called.
+ /// </summary>
+ public Func<CookieSignedInContext, Task> OnSignedIn { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// A delegate assigned to this property will be invoked when the related method is called.
+ /// </summary>
+ public Func<CookieSigningOutContext, Task> OnSigningOut { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// A delegate assigned to this property will be invoked when the related method is called.
+ /// </summary>
+ public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToLogin { get; set; } = context =>
+ {
+ if (IsAjaxRequest(context.Request))
+ {
+ context.Response.Headers["Location"] = context.RedirectUri;
+ context.Response.StatusCode = 401;
+ }
+ else
+ {
+ context.Response.Redirect(context.RedirectUri);
+ }
+ return Task.CompletedTask;
+ };
+
+ /// <summary>
+ /// A delegate assigned to this property will be invoked when the related method is called.
+ /// </summary>
+ public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToAccessDenied { get; set; } = context =>
+ {
+ if (IsAjaxRequest(context.Request))
+ {
+ context.Response.Headers["Location"] = context.RedirectUri;
+ context.Response.StatusCode = 403;
+ }
+ else
+ {
+ context.Response.Redirect(context.RedirectUri);
+ }
+ return Task.CompletedTask;
+ };
+
+ /// <summary>
+ /// A delegate assigned to this property will be invoked when the related method is called.
+ /// </summary>
+ public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToLogout { get; set; } = context =>
+ {
+ if (IsAjaxRequest(context.Request))
+ {
+ context.Response.Headers["Location"] = context.RedirectUri;
+ }
+ else
+ {
+ context.Response.Redirect(context.RedirectUri);
+ }
+ return Task.CompletedTask;
+ };
+
+ /// <summary>
+ /// A delegate assigned to this property will be invoked when the related method is called.
+ /// </summary>
+ public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToReturnUrl { get; set; } = context =>
+ {
+ if (IsAjaxRequest(context.Request))
+ {
+ context.Response.Headers["Location"] = context.RedirectUri;
+ }
+ else
+ {
+ context.Response.Redirect(context.RedirectUri);
+ }
+ return Task.CompletedTask;
+ };
+
+ private static bool IsAjaxRequest(HttpRequest request)
+ {
+ return string.Equals(request.Query["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal) ||
+ string.Equals(request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal);
+ }
+
+ /// <summary>
+ /// Implements the interface method by invoking the related delegate method.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <returns></returns>
+ public virtual Task ValidatePrincipal(CookieValidatePrincipalContext context) => OnValidatePrincipal(context);
+
+ /// <summary>
+ /// Implements the interface method by invoking the related delegate method.
+ /// </summary>
+ /// <param name="context"></param>
+ public virtual Task SigningIn(CookieSigningInContext context) => OnSigningIn(context);
+
+ /// <summary>
+ /// Implements the interface method by invoking the related delegate method.
+ /// </summary>
+ /// <param name="context"></param>
+ public virtual Task SignedIn(CookieSignedInContext context) => OnSignedIn(context);
+
+ /// <summary>
+ /// Implements the interface method by invoking the related delegate method.
+ /// </summary>
+ /// <param name="context"></param>
+ public virtual Task SigningOut(CookieSigningOutContext context) => OnSigningOut(context);
+
+ /// <summary>
+ /// Implements the interface method by invoking the related delegate method.
+ /// </summary>
+ /// <param name="context">Contains information about the event</param>
+ public virtual Task RedirectToLogout(RedirectContext<CookieAuthenticationOptions> context) => OnRedirectToLogout(context);
+
+ /// <summary>
+ /// Implements the interface method by invoking the related delegate method.
+ /// </summary>
+ /// <param name="context">Contains information about the event</param>
+ public virtual Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context) => OnRedirectToLogin(context);
+
+ /// <summary>
+ /// Implements the interface method by invoking the related delegate method.
+ /// </summary>
+ /// <param name="context">Contains information about the event</param>
+ public virtual Task RedirectToReturnUrl(RedirectContext<CookieAuthenticationOptions> context) => OnRedirectToReturnUrl(context);
+
+ /// <summary>
+ /// Implements the interface method by invoking the related delegate method.
+ /// </summary>
+ /// <param name="context">Contains information about the event</param>
+ public virtual Task RedirectToAccessDenied(RedirectContext<CookieAuthenticationOptions> context) => OnRedirectToAccessDenied(context);
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSignedInContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSignedInContext.cs
new file mode 100644
index 0000000000..98c31dd190
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSignedInContext.cs
@@ -0,0 +1,33 @@
+// 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.Claims;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.Cookies
+{
+ /// <summary>
+ /// Context object passed to the ICookieAuthenticationEvents method SignedIn.
+ /// </summary>
+ public class CookieSignedInContext : PrincipalContext<CookieAuthenticationOptions>
+ {
+ /// <summary>
+ /// Creates a new instance of the context object.
+ /// </summary>
+ /// <param name="context">The HTTP request context</param>
+ /// <param name="scheme">The scheme data</param>
+ /// <param name="principal">Initializes Principal property</param>
+ /// <param name="properties">Initializes Properties property</param>
+ /// <param name="options">The handler options</param>
+ public CookieSignedInContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ ClaimsPrincipal principal,
+ AuthenticationProperties properties,
+ CookieAuthenticationOptions options)
+ : base(context, scheme, options, properties)
+ {
+ Principal = principal;
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningInContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningInContext.cs
new file mode 100644
index 0000000000..41d7b4f6ae
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningInContext.cs
@@ -0,0 +1,42 @@
+// 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.Claims;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.Cookies
+{
+ /// <summary>
+ /// Context object passed to the <see cref="CookieAuthenticationEvents.SigningIn(CookieSigningInContext)"/>.
+ /// </summary>
+ public class CookieSigningInContext : PrincipalContext<CookieAuthenticationOptions>
+ {
+ /// <summary>
+ /// Creates a new instance of the context object.
+ /// </summary>
+ /// <param name="context">The HTTP request context</param>
+ /// <param name="scheme">The scheme data</param>
+ /// <param name="options">The handler options</param>
+ /// <param name="principal">Initializes Principal property</param>
+ /// <param name="properties">The authentication properties.</param>
+ /// <param name="cookieOptions">Initializes options for the authentication cookie.</param>
+ public CookieSigningInContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ CookieAuthenticationOptions options,
+ ClaimsPrincipal principal,
+ AuthenticationProperties properties,
+ CookieOptions cookieOptions)
+ : base(context, scheme, options, properties)
+ {
+ CookieOptions = cookieOptions;
+ Principal = principal;
+ }
+
+ /// <summary>
+ /// The options for creating the outgoing cookie.
+ /// May be replace or altered during the SigningIn call.
+ /// </summary>
+ public CookieOptions CookieOptions { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningOutContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningOutContext.cs
new file mode 100644
index 0000000000..34f6e49ab6
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningOutContext.cs
@@ -0,0 +1,36 @@
+// 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.Authentication.Cookies
+{
+ /// <summary>
+ /// Context object passed to the <see cref="CookieAuthenticationEvents.SigningOut(CookieSigningOutContext)"/>
+ /// </summary>
+ public class CookieSigningOutContext : PropertiesContext<CookieAuthenticationOptions>
+ {
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="scheme"></param>
+ /// <param name="options"></param>
+ /// <param name="properties"></param>
+ /// <param name="cookieOptions"></param>
+ public CookieSigningOutContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ CookieAuthenticationOptions options,
+ AuthenticationProperties properties,
+ CookieOptions cookieOptions)
+ : base(context, scheme, options, properties)
+ => CookieOptions = cookieOptions;
+
+ /// <summary>
+ /// The options for creating the outgoing cookie.
+ /// May be replace or altered during the SigningOut call.
+ /// </summary>
+ public CookieOptions CookieOptions { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs
new file mode 100644
index 0000000000..d2161e42a1
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs
@@ -0,0 +1,51 @@
+// 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 Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.Cookies
+{
+ /// <summary>
+ /// Context object passed to the CookieAuthenticationEvents ValidatePrincipal method.
+ /// </summary>
+ public class CookieValidatePrincipalContext : PrincipalContext<CookieAuthenticationOptions>
+ {
+ /// <summary>
+ /// Creates a new instance of the context object.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="scheme"></param>
+ /// <param name="ticket">Contains the initial values for identity and extra data</param>
+ /// <param name="options"></param>
+ public CookieValidatePrincipalContext(HttpContext context, AuthenticationScheme scheme, CookieAuthenticationOptions options, AuthenticationTicket ticket)
+ : base(context, scheme, options, ticket?.Properties)
+ {
+ if (ticket == null)
+ {
+ throw new ArgumentNullException(nameof(ticket));
+ }
+
+ Principal = ticket.Principal;
+ }
+
+ /// <summary>
+ /// If true, the cookie will be renewed
+ /// </summary>
+ public bool ShouldRenew { get; set; }
+
+ /// <summary>
+ /// Called to replace the claims principal. The supplied principal will replace the value of the
+ /// Principal property, which determines the identity of the authenticated request.
+ /// </summary>
+ /// <param name="principal">The <see cref="ClaimsPrincipal"/> used as the replacement</param>
+ public void ReplacePrincipal(ClaimsPrincipal principal) => Principal = principal;
+
+ /// <summary>
+ /// Called to reject the incoming principal. This may be done if the application has determined the
+ /// account is no longer active, and the request should be treated as if it was anonymous.
+ /// </summary>
+ public void RejectPrincipal() => Principal = null;
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ICookieManager.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ICookieManager.cs
new file mode 100644
index 0000000000..4514fefa97
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ICookieManager.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.Authentication.Cookies
+{
+ /// <summary>
+ /// This is used by the CookieAuthenticationMiddleware to process request and response cookies.
+ /// It is abstracted from the normal cookie APIs to allow for complex operations like chunking.
+ /// </summary>
+ public interface ICookieManager
+ {
+ /// <summary>
+ /// Retrieve a cookie of the given name from the request.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="key"></param>
+ /// <returns></returns>
+ string GetRequestCookie(HttpContext context, string key);
+
+ /// <summary>
+ /// Append the given cookie to the response.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="key"></param>
+ /// <param name="value"></param>
+ /// <param name="options"></param>
+ void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options);
+
+ /// <summary>
+ /// Append a delete cookie to the response.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="key"></param>
+ /// <param name="options"></param>
+ void DeleteCookie(HttpContext context, string key, CookieOptions options);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ITicketStore.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ITicketStore.cs
new file mode 100644
index 0000000000..cff11a8929
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/ITicketStore.cs
@@ -0,0 +1,43 @@
+// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authentication.Cookies
+{
+ /// <summary>
+ /// This provides an abstract storage mechanic to preserve identity information on the server
+ /// while only sending a simple identifier key to the client. This is most commonly used to mitigate
+ /// issues with serializing large identities into cookies.
+ /// </summary>
+ public interface ITicketStore
+ {
+ /// <summary>
+ /// Store the identity ticket and return the associated key.
+ /// </summary>
+ /// <param name="ticket">The identity information to store.</param>
+ /// <returns>The key that can be used to retrieve the identity later.</returns>
+ Task<string> StoreAsync(AuthenticationTicket ticket);
+
+ /// <summary>
+ /// Tells the store that the given identity should be updated.
+ /// </summary>
+ /// <param name="key"></param>
+ /// <param name="ticket"></param>
+ /// <returns></returns>
+ Task RenewAsync(string key, AuthenticationTicket ticket);
+
+ /// <summary>
+ /// Retrieves an identity from the store for the given key.
+ /// </summary>
+ /// <param name="key">The key associated with the identity.</param>
+ /// <returns>The identity associated with the given key, or if not found.</returns>
+ Task<AuthenticationTicket> RetrieveAsync(string key);
+
+ /// <summary>
+ /// Remove the identity associated with the given key.
+ /// </summary>
+ /// <param name="key">The key associated with the identity.</param>
+ /// <returns></returns>
+ Task RemoveAsync(string key);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/LoggingExtensions.cs
new file mode 100644
index 0000000000..d12735443f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/LoggingExtensions.cs
@@ -0,0 +1,35 @@
+// 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.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static Action<ILogger, string, Exception> _authSchemeSignedIn;
+ private static Action<ILogger, string, Exception> _authSchemeSignedOut;
+
+ static LoggingExtensions()
+ {
+ _authSchemeSignedIn = LoggerMessage.Define<string>(
+ eventId: 10,
+ logLevel: LogLevel.Information,
+ formatString: "AuthenticationScheme: {AuthenticationScheme} signed in.");
+ _authSchemeSignedOut = LoggerMessage.Define<string>(
+ eventId: 11,
+ logLevel: LogLevel.Information,
+ formatString: "AuthenticationScheme: {AuthenticationScheme} signed out.");
+ }
+
+ public static void SignedIn(this ILogger logger, string authenticationScheme)
+ {
+ _authSchemeSignedIn(logger, authenticationScheme, null);
+ }
+
+ public static void SignedOut(this ILogger logger, string authenticationScheme)
+ {
+ _authSchemeSignedOut(logger, authenticationScheme, null);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj
new file mode 100644
index 0000000000..b188a58e08
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core middleware that enables an application to use cookie based authentication.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <DefineConstants>$(DefineConstants);SECURITY</DefineConstants>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;authentication;security</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\..\shared\Microsoft.AspNetCore.ChunkingCookieManager.Sources\**\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.Authentication\Microsoft.AspNetCore.Authentication.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/PostConfigureCookieAuthenticationOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/PostConfigureCookieAuthenticationOptions.cs
new file mode 100644
index 0000000000..48895072e9
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/PostConfigureCookieAuthenticationOptions.cs
@@ -0,0 +1,59 @@
+// 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.DataProtection;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Authentication.Cookies
+{
+ /// <summary>
+ /// Used to setup defaults for all <see cref="CookieAuthenticationOptions"/>.
+ /// </summary>
+ public class PostConfigureCookieAuthenticationOptions : IPostConfigureOptions<CookieAuthenticationOptions>
+ {
+ private readonly IDataProtectionProvider _dp;
+
+ public PostConfigureCookieAuthenticationOptions(IDataProtectionProvider dataProtection)
+ {
+ _dp = dataProtection;
+ }
+
+ /// <summary>
+ /// Invoked to post configure a TOptions instance.
+ /// </summary>
+ /// <param name="name">The name of the options instance being configured.</param>
+ /// <param name="options">The options instance to configure.</param>
+ public void PostConfigure(string name, CookieAuthenticationOptions options)
+ {
+ options.DataProtectionProvider = options.DataProtectionProvider ?? _dp;
+
+ if (string.IsNullOrEmpty(options.Cookie.Name))
+ {
+ options.Cookie.Name = CookieAuthenticationDefaults.CookiePrefix + name;
+ }
+ if (options.TicketDataFormat == null)
+ {
+ // Note: the purpose for the data protector must remain fixed for interop to work.
+ var dataProtector = options.DataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", name, "v2");
+ options.TicketDataFormat = new TicketDataFormat(dataProtector);
+ }
+ if (options.CookieManager == null)
+ {
+ options.CookieManager = new ChunkingCookieManager();
+ }
+ if (!options.LoginPath.HasValue)
+ {
+ options.LoginPath = CookieAuthenticationDefaults.LoginPath;
+ }
+ if (!options.LogoutPath.HasValue)
+ {
+ options.LogoutPath = CookieAuthenticationDefaults.LogoutPath;
+ }
+ if (!options.AccessDeniedPath.HasValue)
+ {
+ options.AccessDeniedPath = CookieAuthenticationDefaults.AccessDeniedPath;
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json
new file mode 100644
index 0000000000..b218669b76
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json
@@ -0,0 +1,1621 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Cookies, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.CookieExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddCookie",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddCookie",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddCookie",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddCookie",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddCookie",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.CookieAppBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseCookieAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseCookieAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "CookiePrefix",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "LoginPath",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "LogoutPath",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AccessDeniedPath",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "ReturnUrlParameter",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AuthenticationScheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": [],
+ "Constant": true,
+ "Literal": "\"Cookies\""
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.SignInAuthenticationHandler<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "InitializeHandlerAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateEventsAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Object>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleAuthenticateAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "FinishResponseAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleSignInAsync",
+ "Parameters": [
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleSignOutAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleForbiddenAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleChallengeAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions",
+ "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_DataProtectionProvider",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_DataProtectionProvider",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SlidingExpiration",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SlidingExpiration",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_LoginPath",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_LoginPath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.PathString"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_LogoutPath",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_LogoutPath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.PathString"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AccessDeniedPath",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AccessDeniedPath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.PathString"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ReturnUrlParameter",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ReturnUrlParameter",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TicketDataFormat",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationTicket>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_TicketDataFormat",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationTicket>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookieManager",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieManager",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SessionStore",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.Cookies.ITicketStore",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SessionStore",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.ITicketStore"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ExpireTimeSpan",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ExpireTimeSpan",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "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_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_CookiePath",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookiePath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookieHttpOnly",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieHttpOnly",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookieSecure",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieSecurePolicy",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieSecure",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.CookieSecurePolicy"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_OnValidatePrincipal",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.Cookies.CookieValidatePrincipalContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnValidatePrincipal",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.Cookies.CookieValidatePrincipalContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnSigningIn",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.Cookies.CookieSigningInContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnSigningIn",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.Cookies.CookieSigningInContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnSignedIn",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.Cookies.CookieSignedInContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnSignedIn",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.Cookies.CookieSignedInContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnSigningOut",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.Cookies.CookieSigningOutContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnSigningOut",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.Cookies.CookieSigningOutContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnRedirectToLogin",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnRedirectToLogin",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnRedirectToAccessDenied",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnRedirectToAccessDenied",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnRedirectToLogout",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnRedirectToLogout",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnRedirectToReturnUrl",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnRedirectToReturnUrl",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ValidatePrincipal",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieValidatePrincipalContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SigningIn",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieSigningInContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SignedIn",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieSignedInContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SigningOut",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieSigningOutContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RedirectToLogout",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RedirectToLogin",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RedirectToReturnUrl",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RedirectToAccessDenied",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieSignedInContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.PrincipalContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "principal",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieSigningInContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.PrincipalContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_CookieOptions",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieOptions",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.CookieOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions"
+ },
+ {
+ "Name": "principal",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "cookieOptions",
+ "Type": "Microsoft.AspNetCore.Http.CookieOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieSigningOutContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_CookieOptions",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieOptions",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.CookieOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "cookieOptions",
+ "Type": "Microsoft.AspNetCore.Http.CookieOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieValidatePrincipalContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.PrincipalContext<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ShouldRenew",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ShouldRenew",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ReplacePrincipal",
+ "Parameters": [
+ {
+ "Name": "principal",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RejectPrincipal",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions"
+ },
+ {
+ "Name": "ticket",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetRequestCookie",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "key",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AppendResponseCookie",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.String"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Http.CookieOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "DeleteCookie",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Http.CookieOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Cookies.ITicketStore",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "StoreAsync",
+ "Parameters": [
+ {
+ "Name": "ticket",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<System.String>",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RenewAsync",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "ticket",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RetrieveAsync",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationTicket>",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RemoveAsync",
+ "Parameters": [
+ {
+ "Name": "key",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Cookies.PostConfigureCookieAuthenticationOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.Extensions.Options.IPostConfigureOptions<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "PostConfigure",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "dataProtection",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ChunkSize",
+ "Parameters": [],
+ "ReturnType": "System.Nullable<System.Int32>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ChunkSize",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Nullable<System.Int32>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ThrowForPartialCookies",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ThrowForPartialCookies",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetRequestCookie",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "key",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AppendResponseCookie",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.String"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Http.CookieOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "DeleteCookie",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Http.CookieOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "DefaultChunkSize",
+ "Parameters": [],
+ "ReturnType": "System.Int32",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": [],
+ "Constant": true,
+ "Literal": "4050"
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookAppBuilderExtensions.cs
new file mode 100644
index 0000000000..a94dc7bc45
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookAppBuilderExtensions.cs
@@ -0,0 +1,37 @@
+// 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.Authentication.Facebook;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Extension methods to add Facebook authentication capabilities to an HTTP application pipeline.
+ /// </summary>
+ public static class FacebookAppBuilderExtensions
+ {
+ /// <summary>
+ /// UseFacebookAuthentication is obsolete. Configure Facebook authentication with AddAuthentication().AddFacebook in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseFacebookAuthentication is obsolete. Configure Facebook authentication with AddAuthentication().AddFacebook in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseFacebookAuthentication(this IApplicationBuilder app)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+
+ /// <summary>
+ /// UseFacebookAuthentication is obsolete. Configure Facebook authentication with AddAuthentication().AddFacebook in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <param name="options">A <see cref="FacebookOptions"/> that specifies options for the handler.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseFacebookAuthentication is obsolete. Configure Facebook authentication with AddAuthentication().AddFacebook in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseFacebookAuthentication(this IApplicationBuilder app, FacebookOptions options)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs
new file mode 100644
index 0000000000..92d1d003e6
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs
@@ -0,0 +1,18 @@
+// 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.Authentication.Facebook
+{
+ public static class FacebookDefaults
+ {
+ public const string AuthenticationScheme = "Facebook";
+
+ public static readonly string DisplayName = "Facebook";
+
+ public static readonly string AuthorizationEndpoint = "https://www.facebook.com/v2.12/dialog/oauth";
+
+ public static readonly string TokenEndpoint = "https://graph.facebook.com/v2.12/oauth/access_token";
+
+ public static readonly string UserInformationEndpoint = "https://graph.facebook.com/v2.12/me";
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookExtensions.cs
new file mode 100644
index 0000000000..2273724a42
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookExtensions.cs
@@ -0,0 +1,24 @@
+// 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.Authentication;
+using Microsoft.AspNetCore.Authentication.Facebook;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class FacebookAuthenticationOptionsExtensions
+ {
+ public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder)
+ => builder.AddFacebook(FacebookDefaults.AuthenticationScheme, _ => { });
+
+ public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, Action<FacebookOptions> configureOptions)
+ => builder.AddFacebook(FacebookDefaults.AuthenticationScheme, configureOptions);
+
+ public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, Action<FacebookOptions> configureOptions)
+ => builder.AddFacebook(authenticationScheme, FacebookDefaults.DisplayName, configureOptions);
+
+ public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<FacebookOptions> configureOptions)
+ => builder.AddOAuth<FacebookOptions, FacebookHandler>(authenticationScheme, displayName, configureOptions);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs
new file mode 100644
index 0000000000..eb42511431
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs
@@ -0,0 +1,80 @@
+// 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.Globalization;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.Facebook
+{
+ public class FacebookHandler : OAuthHandler<FacebookOptions>
+ {
+ public FacebookHandler(IOptionsMonitor<FacebookOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
+ : base(options, logger, encoder, clock)
+ { }
+
+ protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)
+ {
+ var endpoint = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, "access_token", tokens.AccessToken);
+ if (Options.SendAppSecretProof)
+ {
+ endpoint = QueryHelpers.AddQueryString(endpoint, "appsecret_proof", GenerateAppSecretProof(tokens.AccessToken));
+ }
+ if (Options.Fields.Count > 0)
+ {
+ endpoint = QueryHelpers.AddQueryString(endpoint, "fields", string.Join(",", Options.Fields));
+ }
+
+ var response = await Backchannel.GetAsync(endpoint, Context.RequestAborted);
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new HttpRequestException($"An error occurred when retrieving Facebook user information ({response.StatusCode}). Please check if the authentication information is correct and the corresponding Facebook Graph API is enabled.");
+ }
+
+ var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
+
+ var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload);
+ context.RunClaimActions();
+
+ await Events.CreatingTicket(context);
+
+ return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
+
+ }
+
+ private string GenerateAppSecretProof(string accessToken)
+ {
+ using (var algorithm = new HMACSHA256(Encoding.ASCII.GetBytes(Options.AppSecret)))
+ {
+ var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(accessToken));
+ var builder = new StringBuilder();
+ for (int i = 0; i < hash.Length; i++)
+ {
+ builder.Append(hash[i].ToString("x2", CultureInfo.InvariantCulture));
+ }
+ return builder.ToString();
+ }
+ }
+
+ protected override string FormatScope(IEnumerable<string> scopes)
+ {
+ // Facebook deviates from the OAuth spec here. They require comma separated instead of space separated.
+ // https://developers.facebook.com/docs/reference/dialogs/oauth
+ // http://tools.ietf.org/html/rfc6749#section-3.3
+ return string.Join(",", scopes);
+ }
+
+ protected override string FormatScope()
+ => base.FormatScope();
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs
new file mode 100644
index 0000000000..7010bb20aa
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs
@@ -0,0 +1,102 @@
+// 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 Microsoft.AspNetCore.Authentication;
+using System.Globalization;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.Facebook
+{
+ /// <summary>
+ /// Configuration options for <see cref="FacebookHandler"/>.
+ /// </summary>
+ public class FacebookOptions : OAuthOptions
+ {
+ /// <summary>
+ /// Initializes a new <see cref="FacebookOptions"/>.
+ /// </summary>
+ public FacebookOptions()
+ {
+ CallbackPath = new PathString("/signin-facebook");
+ SendAppSecretProof = true;
+ AuthorizationEndpoint = FacebookDefaults.AuthorizationEndpoint;
+ TokenEndpoint = FacebookDefaults.TokenEndpoint;
+ UserInformationEndpoint = FacebookDefaults.UserInformationEndpoint;
+ Scope.Add("public_profile");
+ Scope.Add("email");
+ Fields.Add("name");
+ Fields.Add("email");
+ Fields.Add("first_name");
+ Fields.Add("last_name");
+
+ ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
+ ClaimActions.MapJsonSubKey("urn:facebook:age_range_min", "age_range", "min");
+ ClaimActions.MapJsonSubKey("urn:facebook:age_range_max", "age_range", "max");
+ ClaimActions.MapJsonKey(ClaimTypes.DateOfBirth, "birthday");
+ ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
+ ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
+ ClaimActions.MapJsonKey(ClaimTypes.GivenName, "first_name");
+ ClaimActions.MapJsonKey("urn:facebook:middle_name", "middle_name");
+ ClaimActions.MapJsonKey(ClaimTypes.Surname, "last_name");
+ ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
+ ClaimActions.MapJsonKey("urn:facebook:link", "link");
+ ClaimActions.MapJsonSubKey("urn:facebook:location", "location", "name");
+ ClaimActions.MapJsonKey(ClaimTypes.Locality, "locale");
+ ClaimActions.MapJsonKey("urn:facebook:timezone", "timezone");
+ }
+
+ /// <summary>
+ /// Check that the options are valid. Should throw an exception if things are not ok.
+ /// </summary>
+ public override void Validate()
+ {
+ if (string.IsNullOrEmpty(AppId))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(AppId)), nameof(AppId));
+ }
+
+ if (string.IsNullOrEmpty(AppSecret))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(AppSecret)), nameof(AppSecret));
+ }
+
+ base.Validate();
+ }
+
+ // Facebook uses a non-standard term for this field.
+ /// <summary>
+ /// Gets or sets the Facebook-assigned appId.
+ /// </summary>
+ public string AppId
+ {
+ get { return ClientId; }
+ set { ClientId = value; }
+ }
+
+ // Facebook uses a non-standard term for this field.
+ /// <summary>
+ /// Gets or sets the Facebook-assigned app secret.
+ /// </summary>
+ public string AppSecret
+ {
+ get { return ClientSecret; }
+ set { ClientSecret = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets if the appsecret_proof should be generated and sent with Facebook API calls.
+ /// This is enabled by default.
+ /// </summary>
+ public bool SendAppSecretProof { get; set; }
+
+ /// <summary>
+ /// The list of fields to retrieve from the UserInformationEndpoint.
+ /// https://developers.facebook.com/docs/graph-api/reference/user
+ /// </summary>
+ public ICollection<string> Fields { get; } = new HashSet<string>();
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj
new file mode 100644
index 0000000000..62aee1367f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core middleware that enables an application to support Facebook's OAuth 2.0 authentication workflow.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;authentication;security</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.Authentication.OAuth\Microsoft.AspNetCore.Authentication.OAuth.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..655da24a30
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/Resources.Designer.cs
@@ -0,0 +1,44 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Authentication.Facebook
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Authentication.Facebook.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string Exception_OptionMustBeProvided
+ {
+ get => GetString("Exception_OptionMustBeProvided");
+ }
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string FormatException_OptionMustBeProvided(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0);
+
+ 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/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Resources.resx
new file mode 100644
index 0000000000..56ef7f56bd
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/Resources.resx
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Exception_OptionMustBeProvided" xml:space="preserve">
+ <value>The '{0}' option must be provided.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json
new file mode 100644
index 0000000000..5d95efca6f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json
@@ -0,0 +1,390 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Facebook, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.FacebookAuthenticationOptionsExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddFacebook",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddFacebook",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.Facebook.FacebookOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddFacebook",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.Facebook.FacebookOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddFacebook",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.Facebook.FacebookOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Facebook.FacebookDefaults",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "DisplayName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AuthorizationEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "TokenEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "UserInformationEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AuthenticationScheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": [],
+ "Constant": true,
+ "Literal": "\"Facebook\""
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Facebook.FacebookHandler",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler<Microsoft.AspNetCore.Authentication.Facebook.FacebookOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateTicketAsync",
+ "Parameters": [
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "tokens",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationTicket>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "FormatScope",
+ "Parameters": [
+ {
+ "Name": "scopes",
+ "Type": "System.Collections.Generic.IEnumerable<System.String>"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "FormatScope",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.Authentication.Facebook.FacebookOptions>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Facebook.FacebookOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Validate",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AppId",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AppId",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AppSecret",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AppSecret",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SendAppSecretProof",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SendAppSecretProof",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Fields",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.ICollection<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.FacebookAppBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseFacebookAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseFacebookAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.Facebook.FacebookOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleAppBuilderExtensions.cs
new file mode 100644
index 0000000000..4302d20db1
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleAppBuilderExtensions.cs
@@ -0,0 +1,37 @@
+// 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.Authentication.Google;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Extension methods to add Google authentication capabilities to an HTTP application pipeline.
+ /// </summary>
+ public static class GoogleAppBuilderExtensions
+ {
+ /// <summary>
+ /// UseGoogleAuthentication is obsolete. Configure Google authentication with AddAuthentication().AddGoogle in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseGoogleAuthentication is obsolete. Configure Google authentication with AddAuthentication().AddGoogle in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseGoogleAuthentication(this IApplicationBuilder app)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+
+ /// <summary>
+ /// UseGoogleAuthentication is obsolete. Configure Google authentication with AddAuthentication().AddGoogle in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <param name="options">A <see cref="GoogleOptions"/> that specifies options for the handler.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseGoogleAuthentication is obsolete. Configure Google authentication with AddAuthentication().AddGoogle in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseGoogleAuthentication(this IApplicationBuilder app, GoogleOptions options)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleChallengeProperties.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleChallengeProperties.cs
new file mode 100644
index 0000000000..714df45655
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleChallengeProperties.cs
@@ -0,0 +1,89 @@
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Authentication.OAuth;
+
+namespace Microsoft.AspNetCore.Authentication.Google
+{
+ public class GoogleChallengeProperties : OAuthChallengeProperties
+ {
+ /// <summary>
+ /// The parameter key for the "access_type" argument being used for a challenge request.
+ /// </summary>
+ public static readonly string AccessTypeKey = "access_type";
+
+ /// <summary>
+ /// The parameter key for the "approval_prompt" argument being used for a challenge request.
+ /// </summary>
+ public static readonly string ApprovalPromptKey = "approval_prompt";
+
+ /// <summary>
+ /// The parameter key for the "include_granted_scopes" argument being used for a challenge request.
+ /// </summary>
+ public static readonly string IncludeGrantedScopesKey = "include_granted_scopes";
+
+ /// <summary>
+ /// The parameter key for the "login_hint" argument being used for a challenge request.
+ /// </summary>
+ public static readonly string LoginHintKey = "login_hint";
+
+ /// <summary>
+ /// The parameter key for the "prompt" argument being used for a challenge request.
+ /// </summary>
+ public static readonly string PromptParameterKey = "prompt";
+
+ public GoogleChallengeProperties()
+ { }
+
+ public GoogleChallengeProperties(IDictionary<string, string> items)
+ : base(items)
+ { }
+
+ public GoogleChallengeProperties(IDictionary<string, string> items, IDictionary<string, object> parameters)
+ : base(items, parameters)
+ { }
+
+ /// <summary>
+ /// The "access_type" parameter value being used for a challenge request.
+ /// </summary>
+ public string AccessType
+ {
+ get => GetParameter<string>(AccessTypeKey);
+ set => SetParameter(AccessTypeKey, value);
+ }
+
+ /// <summary>
+ /// The "approval_prompt" parameter value being used for a challenge request.
+ /// </summary>
+ public string ApprovalPrompt
+ {
+ get => GetParameter<string>(ApprovalPromptKey);
+ set => SetParameter(ApprovalPromptKey, value);
+ }
+
+ /// <summary>
+ /// The "include_granted_scopes" parameter value being used for a challenge request.
+ /// </summary>
+ public bool? IncludeGrantedScopes
+ {
+ get => GetParameter<bool?>(IncludeGrantedScopesKey);
+ set => SetParameter(IncludeGrantedScopesKey, value);
+ }
+
+ /// <summary>
+ /// The "login_hint" parameter value being used for a challenge request.
+ /// </summary>
+ public string LoginHint
+ {
+ get => GetParameter<string>(LoginHintKey);
+ set => SetParameter(LoginHintKey, value);
+ }
+
+ /// <summary>
+ /// The "prompt" parameter value being used for a challenge request.
+ /// </summary>
+ public string Prompt
+ {
+ get => GetParameter<string>(PromptParameterKey);
+ set => SetParameter(PromptParameterKey, value);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.cs
new file mode 100644
index 0000000000..0428703180
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.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.
+
+namespace Microsoft.AspNetCore.Authentication.Google
+{
+ /// <summary>
+ /// Default values for Google authentication
+ /// </summary>
+ public static class GoogleDefaults
+ {
+ public const string AuthenticationScheme = "Google";
+
+ public static readonly string DisplayName = "Google";
+
+ public static readonly string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth";
+
+ public static readonly string TokenEndpoint = "https://www.googleapis.com/oauth2/v4/token";
+
+ public static readonly string UserInformationEndpoint = "https://www.googleapis.com/plus/v1/people/me";
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleExtensions.cs
new file mode 100644
index 0000000000..95547014ca
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleExtensions.cs
@@ -0,0 +1,24 @@
+// 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.Authentication;
+using Microsoft.AspNetCore.Authentication.Google;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class GoogleExtensions
+ {
+ public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder)
+ => builder.AddGoogle(GoogleDefaults.AuthenticationScheme, _ => { });
+
+ public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, Action<GoogleOptions> configureOptions)
+ => builder.AddGoogle(GoogleDefaults.AuthenticationScheme, configureOptions);
+
+ public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, Action<GoogleOptions> configureOptions)
+ => builder.AddGoogle(authenticationScheme, GoogleDefaults.DisplayName, configureOptions);
+
+ public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<GoogleOptions> configureOptions)
+ => builder.AddOAuth<GoogleOptions, GoogleHandler>(authenticationScheme, displayName, configureOptions);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs
new file mode 100644
index 0000000000..88d48d4467
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs
@@ -0,0 +1,108 @@
+// 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.Net.Http;
+using System.Net.Http.Headers;
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.Google
+{
+ public class GoogleHandler : OAuthHandler<GoogleOptions>
+ {
+ public GoogleHandler(IOptionsMonitor<GoogleOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
+ : base(options, logger, encoder, clock)
+ { }
+
+ protected override async Task<AuthenticationTicket> CreateTicketAsync(
+ ClaimsIdentity identity,
+ AuthenticationProperties properties,
+ OAuthTokenResponse tokens)
+ {
+ // Get the Google user
+ var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
+
+ var response = await Backchannel.SendAsync(request, Context.RequestAborted);
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new HttpRequestException($"An error occurred when retrieving Google user information ({response.StatusCode}). Please check if the authentication information is correct and the corresponding Google+ API is enabled.");
+ }
+
+ var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
+
+ var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload);
+ context.RunClaimActions();
+
+ await Events.CreatingTicket(context);
+ return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
+ }
+
+ // TODO: Abstract this properties override pattern into the base class?
+ protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
+ {
+ // Google Identity Platform Manual:
+ // https://developers.google.com/identity/protocols/OAuth2WebServer
+
+ var queryStrings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ queryStrings.Add("response_type", "code");
+ queryStrings.Add("client_id", Options.ClientId);
+ queryStrings.Add("redirect_uri", redirectUri);
+
+ AddQueryString(queryStrings, properties, GoogleChallengeProperties.ScopeKey, FormatScope, Options.Scope);
+ AddQueryString(queryStrings, properties, GoogleChallengeProperties.AccessTypeKey, Options.AccessType);
+ AddQueryString(queryStrings, properties, GoogleChallengeProperties.ApprovalPromptKey);
+ AddQueryString(queryStrings, properties, GoogleChallengeProperties.PromptParameterKey);
+ AddQueryString(queryStrings, properties, GoogleChallengeProperties.LoginHintKey);
+ AddQueryString(queryStrings, properties, GoogleChallengeProperties.IncludeGrantedScopesKey, v => v?.ToString().ToLower(), (bool?)null);
+
+ var state = Options.StateDataFormat.Protect(properties);
+ queryStrings.Add("state", state);
+
+ var authorizationEndpoint = QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings);
+ return authorizationEndpoint;
+ }
+
+ private void AddQueryString<T>(
+ IDictionary<string, string> queryStrings,
+ AuthenticationProperties properties,
+ string name,
+ Func<T, string> formatter,
+ T defaultValue)
+ {
+ string value = null;
+ var parameterValue = properties.GetParameter<T>(name);
+ if (parameterValue != null)
+ {
+ value = formatter(parameterValue);
+ }
+ else if (!properties.Items.TryGetValue(name, out value))
+ {
+ value = formatter(defaultValue);
+ }
+
+ // Remove the parameter from AuthenticationProperties so it won't be serialized into the state
+ properties.Items.Remove(name);
+
+ if (value != null)
+ {
+ queryStrings[name] = value;
+ }
+ }
+
+ private void AddQueryString(
+ IDictionary<string, string> queryStrings,
+ AuthenticationProperties properties,
+ string name,
+ string defaultValue = null)
+ => AddQueryString(queryStrings, properties, name, x => x, defaultValue);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs
new file mode 100644
index 0000000000..2cac949a03
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.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 System;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.Google
+{
+ /// <summary>
+ /// Contains static methods that allow to extract user's information from a <see cref="JObject"/>
+ /// instance retrieved from Google after a successful authentication process.
+ /// </summary>
+ public static class GoogleHelper
+ {
+ /// <summary>
+ /// Gets the user's email.
+ /// </summary>
+ public static string GetEmail(JObject user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ return TryGetFirstValue(user, "emails", "value");
+ }
+
+ // Get the given subProperty from a list property.
+ private static string TryGetFirstValue(JObject user, string propertyName, string subProperty)
+ {
+ JToken value;
+ if (user.TryGetValue(propertyName, out value))
+ {
+ var array = JArray.Parse(value.ToString());
+ if (array != null && array.Count > 0)
+ {
+ var subObject = JObject.Parse(array.First.ToString());
+ if (subObject != null)
+ {
+ if (subObject.TryGetValue(subProperty, out value))
+ {
+ return value.ToString();
+ }
+ }
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs
new file mode 100644
index 0000000000..34028bc52b
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs
@@ -0,0 +1,42 @@
+// 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.Claims;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.Google
+{
+ /// <summary>
+ /// Configuration options for <see cref="GoogleHandler"/>.
+ /// </summary>
+ public class GoogleOptions : OAuthOptions
+ {
+ /// <summary>
+ /// Initializes a new <see cref="GoogleOptions"/>.
+ /// </summary>
+ public GoogleOptions()
+ {
+ CallbackPath = new PathString("/signin-google");
+ AuthorizationEndpoint = GoogleDefaults.AuthorizationEndpoint;
+ TokenEndpoint = GoogleDefaults.TokenEndpoint;
+ UserInformationEndpoint = GoogleDefaults.UserInformationEndpoint;
+ Scope.Add("openid");
+ Scope.Add("profile");
+ Scope.Add("email");
+
+ ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
+ ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName");
+ ClaimActions.MapJsonSubKey(ClaimTypes.GivenName, "name", "givenName");
+ ClaimActions.MapJsonSubKey(ClaimTypes.Surname, "name", "familyName");
+ ClaimActions.MapJsonKey("urn:google:profile", "url");
+ ClaimActions.MapCustomJson(ClaimTypes.Email, GoogleHelper.GetEmail);
+ }
+
+ /// <summary>
+ /// access_type. Set to 'offline' to request a refresh token.
+ /// </summary>
+ public string AccessType { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj
new file mode 100644
index 0000000000..de8867f91a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core contains middleware to support Google's OpenId and OAuth 2.0 authentication workflows.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;authentication;security</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.Authentication.OAuth\Microsoft.AspNetCore.Authentication.OAuth.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..03448b408c
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Properties/Resources.Designer.cs
@@ -0,0 +1,58 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Authentication.Google
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Authentication.Google.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string Exception_OptionMustBeProvided
+ {
+ get => GetString("Exception_OptionMustBeProvided");
+ }
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string FormatException_OptionMustBeProvided(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0);
+
+ /// <summary>
+ /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.
+ /// </summary>
+ internal static string Exception_ValidatorHandlerMismatch
+ {
+ get => GetString("Exception_ValidatorHandlerMismatch");
+ }
+
+ /// <summary>
+ /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.
+ /// </summary>
+ internal static string FormatException_ValidatorHandlerMismatch()
+ => GetString("Exception_ValidatorHandlerMismatch");
+
+ 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/Security/src/Microsoft.AspNetCore.Authentication.Google/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Resources.resx
new file mode 100644
index 0000000000..2a19bea96a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/Resources.resx
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Exception_OptionMustBeProvided" xml:space="preserve">
+ <value>The '{0}' option must be provided.</value>
+ </data>
+ <data name="Exception_ValidatorHandlerMismatch" xml:space="preserve">
+ <value>An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json
new file mode 100644
index 0000000000..0a623b3b85
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json
@@ -0,0 +1,550 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Google, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.GoogleExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddGoogle",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddGoogle",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.Google.GoogleOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddGoogle",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.Google.GoogleOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddGoogle",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.Google.GoogleOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleChallengeProperties",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthChallengeProperties",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_AccessType",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AccessType",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ApprovalPrompt",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ApprovalPrompt",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IncludeGrantedScopes",
+ "Parameters": [],
+ "ReturnType": "System.Nullable<System.Boolean>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_IncludeGrantedScopes",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Nullable<System.Boolean>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_LoginHint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_LoginHint",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Prompt",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Prompt",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "items",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.String>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "items",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.String>"
+ },
+ {
+ "Name": "parameters",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AccessTypeKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "ApprovalPromptKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "IncludeGrantedScopesKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "LoginHintKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "PromptParameterKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleDefaults",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "DisplayName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AuthorizationEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "TokenEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "UserInformationEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AuthenticationScheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": [],
+ "Constant": true,
+ "Literal": "\"Google\""
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleHandler",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler<Microsoft.AspNetCore.Authentication.Google.GoogleOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateTicketAsync",
+ "Parameters": [
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "tokens",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationTicket>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "BuildChallengeUrl",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "redirectUri",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.Authentication.Google.GoogleOptions>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleHelper",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetEmail",
+ "Parameters": [
+ {
+ "Name": "user",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_AccessType",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AccessType",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.GoogleAppBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseGoogleAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseGoogleAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.Google.GoogleOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs
new file mode 100644
index 0000000000..1c2efd6c73
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs
@@ -0,0 +1,19 @@
+// 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.Authentication.JwtBearer
+{
+ public class AuthenticationFailedContext : ResultContext<JwtBearerOptions>
+ {
+ public AuthenticationFailedContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ JwtBearerOptions options)
+ : base(context, scheme, options) { }
+
+ public Exception Exception { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs
new file mode 100644
index 0000000000..6500e1e3f7
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.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.
+
+using System;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer
+{
+ public class JwtBearerChallengeContext : PropertiesContext<JwtBearerOptions>
+ {
+ public JwtBearerChallengeContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ JwtBearerOptions options,
+ AuthenticationProperties properties)
+ : base(context, scheme, options, properties) { }
+
+ /// <summary>
+ /// Any failures encountered during the authentication process.
+ /// </summary>
+ public Exception AuthenticateFailure { get; set; }
+
+ /// <summary>
+ /// Gets or sets the "error" value returned to the caller as part
+ /// of the WWW-Authenticate header. This property may be null when
+ /// <see cref="JwtBearerOptions.IncludeErrorDetails"/> is set to <c>false</c>.
+ /// </summary>
+ public string Error { get; set; }
+
+ /// <summary>
+ /// Gets or sets the "error_description" value returned to the caller as part
+ /// of the WWW-Authenticate header. This property may be null when
+ /// <see cref="JwtBearerOptions.IncludeErrorDetails"/> is set to <c>false</c>.
+ /// </summary>
+ public string ErrorDescription { get; set; }
+
+ /// <summary>
+ /// Gets or sets the "error_uri" value returned to the caller as part of the
+ /// WWW-Authenticate header. This property is always null unless explicitly set.
+ /// </summary>
+ public string ErrorUri { get; set; }
+
+ /// <summary>
+ /// If true, will skip any default logic for this challenge.
+ /// </summary>
+ public bool Handled { get; private set; }
+
+ /// <summary>
+ /// Skips any default logic for this challenge.
+ /// </summary>
+ public void HandleResponse() => Handled = true;
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerEvents.cs
new file mode 100644
index 0000000000..a9b35c310f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerEvents.cs
@@ -0,0 +1,42 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer
+{
+ /// <summary>
+ /// Specifies events which the <see cref="JwtBearerHandler"/> invokes to enable developer control over the authentication process.
+ /// </summary>
+ public class JwtBearerEvents
+ {
+ /// <summary>
+ /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
+ /// </summary>
+ public Func<AuthenticationFailedContext, Task> OnAuthenticationFailed { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked when a protocol message is first received.
+ /// </summary>
+ public Func<MessageReceivedContext, Task> OnMessageReceived { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated.
+ /// </summary>
+ public Func<TokenValidatedContext, Task> OnTokenValidated { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked before a challenge is sent back to the caller.
+ /// </summary>
+ public Func<JwtBearerChallengeContext, Task> OnChallenge { get; set; } = context => Task.CompletedTask;
+
+ public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context);
+
+ public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context);
+
+ public virtual Task TokenValidated(TokenValidatedContext context) => OnTokenValidated(context);
+
+ public virtual Task Challenge(JwtBearerChallengeContext context) => OnChallenge(context);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/MessageReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/MessageReceivedContext.cs
new file mode 100644
index 0000000000..1850ad0492
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/MessageReceivedContext.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.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer
+{
+ public class MessageReceivedContext : ResultContext<JwtBearerOptions>
+ {
+ public MessageReceivedContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ JwtBearerOptions options)
+ : base(context, scheme, options) { }
+
+ /// <summary>
+ /// Bearer Token. This will give the application an opportunity to retrieve a token from an alternative location.
+ /// </summary>
+ public string Token { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/TokenValidatedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/TokenValidatedContext.cs
new file mode 100644
index 0000000000..39b677b96d
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/TokenValidatedContext.cs
@@ -0,0 +1,19 @@
+// 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;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer
+{
+ public class TokenValidatedContext : ResultContext<JwtBearerOptions>
+ {
+ public TokenValidatedContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ JwtBearerOptions options)
+ : base(context, scheme, options) { }
+
+ public SecurityToken SecurityToken { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerAppBuilderExtensions.cs
new file mode 100644
index 0000000000..0cfc97573c
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerAppBuilderExtensions.cs
@@ -0,0 +1,37 @@
+// 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.Authentication.JwtBearer;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Extension methods to add OpenIdConnect Bearer authentication capabilities to an HTTP application pipeline.
+ /// </summary>
+ public static class JwtBearerAppBuilderExtensions
+ {
+ /// <summary>
+ /// UseJwtBearerAuthentication is obsolete. Configure JwtBearer authentication with AddAuthentication().AddJwtBearer in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseJwtBearerAuthentication is obsolete. Configure JwtBearer authentication with AddAuthentication().AddJwtBearer in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseJwtBearerAuthentication(this IApplicationBuilder app)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+
+ /// <summary>
+ /// UseJwtBearerAuthentication is obsolete. Configure JwtBearer authentication with AddAuthentication().AddJwtBearer in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <param name="options">A <see cref="JwtBearerOptions"/> that specifies options for the handler.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseJwtBearerAuthentication is obsolete. Configure JwtBearer authentication with AddAuthentication().AddJwtBearer in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseJwtBearerAuthentication(this IApplicationBuilder app, JwtBearerOptions options)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerDefaults.cs
new file mode 100644
index 0000000000..649edf94bb
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerDefaults.cs
@@ -0,0 +1,16 @@
+// 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.Authentication.JwtBearer
+{
+ /// <summary>
+ /// Default values used by bearer authentication.
+ /// </summary>
+ public static class JwtBearerDefaults
+ {
+ /// <summary>
+ /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions
+ /// </summary>
+ public const string AuthenticationScheme = "Bearer";
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerExtensions.cs
new file mode 100644
index 0000000000..334407c0da
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerExtensions.cs
@@ -0,0 +1,29 @@
+// 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.Authentication;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class JwtBearerExtensions
+ {
+ public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder)
+ => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, _ => { });
+
+ public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, Action<JwtBearerOptions> configureOptions)
+ => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, configureOptions);
+
+ public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, Action<JwtBearerOptions> configureOptions)
+ => builder.AddJwtBearer(authenticationScheme, displayName: null, configureOptions: configureOptions);
+
+ public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<JwtBearerOptions> configureOptions)
+ {
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
+ return builder.AddScheme<JwtBearerOptions, JwtBearerHandler>(authenticationScheme, displayName, configureOptions);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs
new file mode 100644
index 0000000000..6d5c7f5f5e
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs
@@ -0,0 +1,323 @@
+// 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 System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+using Microsoft.IdentityModel.Tokens;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer
+{
+ public class JwtBearerHandler : AuthenticationHandler<JwtBearerOptions>
+ {
+ private OpenIdConnectConfiguration _configuration;
+
+ public JwtBearerHandler(IOptionsMonitor<JwtBearerOptions> options, ILoggerFactory logger, UrlEncoder encoder, IDataProtectionProvider dataProtection, ISystemClock clock)
+ : base(options, logger, encoder, clock)
+ { }
+
+ /// <summary>
+ /// The handler calls methods on the events which give the application control at certain points where processing is occurring.
+ /// If it is not provided a default instance is supplied which does nothing when the methods are called.
+ /// </summary>
+ protected new JwtBearerEvents Events
+ {
+ get { return (JwtBearerEvents)base.Events; }
+ set { base.Events = value; }
+ }
+
+ protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new JwtBearerEvents());
+
+ /// <summary>
+ /// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using <see cref="TokenValidationParameters"/> set in the options.
+ /// </summary>
+ /// <returns></returns>
+ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
+ {
+ string token = null;
+ try
+ {
+ // Give application opportunity to find from a different location, adjust, or reject token
+ var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);
+
+ // event can set the token
+ await Events.MessageReceived(messageReceivedContext);
+ if (messageReceivedContext.Result != null)
+ {
+ return messageReceivedContext.Result;
+ }
+
+ // If application retrieved token from somewhere else, use that.
+ token = messageReceivedContext.Token;
+
+ if (string.IsNullOrEmpty(token))
+ {
+ string authorization = Request.Headers["Authorization"];
+
+ // If no authorization header found, nothing to process further
+ if (string.IsNullOrEmpty(authorization))
+ {
+ return AuthenticateResult.NoResult();
+ }
+
+ if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
+ {
+ token = authorization.Substring("Bearer ".Length).Trim();
+ }
+
+ // If no token found, no further work possible
+ if (string.IsNullOrEmpty(token))
+ {
+ return AuthenticateResult.NoResult();
+ }
+ }
+
+ if (_configuration == null && Options.ConfigurationManager != null)
+ {
+ _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
+ }
+
+ var validationParameters = Options.TokenValidationParameters.Clone();
+ if (_configuration != null)
+ {
+ var issuers = new[] { _configuration.Issuer };
+ validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers;
+
+ validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys)
+ ?? _configuration.SigningKeys;
+ }
+
+ List<Exception> validationFailures = null;
+ SecurityToken validatedToken;
+ foreach (var validator in Options.SecurityTokenValidators)
+ {
+ if (validator.CanReadToken(token))
+ {
+ ClaimsPrincipal principal;
+ try
+ {
+ principal = validator.ValidateToken(token, validationParameters, out validatedToken);
+ }
+ catch (Exception ex)
+ {
+ Logger.TokenValidationFailed(ex);
+
+ // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
+ if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
+ && ex is SecurityTokenSignatureKeyNotFoundException)
+ {
+ Options.ConfigurationManager.RequestRefresh();
+ }
+
+ if (validationFailures == null)
+ {
+ validationFailures = new List<Exception>(1);
+ }
+ validationFailures.Add(ex);
+ continue;
+ }
+
+ Logger.TokenValidationSucceeded();
+
+ var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
+ {
+ Principal = principal,
+ SecurityToken = validatedToken
+ };
+
+ await Events.TokenValidated(tokenValidatedContext);
+ if (tokenValidatedContext.Result != null)
+ {
+ return tokenValidatedContext.Result;
+ }
+
+ if (Options.SaveToken)
+ {
+ tokenValidatedContext.Properties.StoreTokens(new[]
+ {
+ new AuthenticationToken { Name = "access_token", Value = token }
+ });
+ }
+
+ tokenValidatedContext.Success();
+ return tokenValidatedContext.Result;
+ }
+ }
+
+ if (validationFailures != null)
+ {
+ var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
+ {
+ Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures)
+ };
+
+ await Events.AuthenticationFailed(authenticationFailedContext);
+ if (authenticationFailedContext.Result != null)
+ {
+ return authenticationFailedContext.Result;
+ }
+
+ return AuthenticateResult.Fail(authenticationFailedContext.Exception);
+ }
+
+ return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorProcessingMessage(ex);
+
+ var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
+ {
+ Exception = ex
+ };
+
+ await Events.AuthenticationFailed(authenticationFailedContext);
+ if (authenticationFailedContext.Result != null)
+ {
+ return authenticationFailedContext.Result;
+ }
+
+ throw;
+ }
+ }
+
+ protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
+ {
+ var authResult = await HandleAuthenticateOnceSafeAsync();
+ var eventContext = new JwtBearerChallengeContext(Context, Scheme, Options, properties)
+ {
+ AuthenticateFailure = authResult?.Failure
+ };
+
+ // Avoid returning error=invalid_token if the error is not caused by an authentication failure (e.g missing token).
+ if (Options.IncludeErrorDetails && eventContext.AuthenticateFailure != null)
+ {
+ eventContext.Error = "invalid_token";
+ eventContext.ErrorDescription = CreateErrorDescription(eventContext.AuthenticateFailure);
+ }
+
+ await Events.Challenge(eventContext);
+ if (eventContext.Handled)
+ {
+ return;
+ }
+
+ Response.StatusCode = 401;
+
+ if (string.IsNullOrEmpty(eventContext.Error) &&
+ string.IsNullOrEmpty(eventContext.ErrorDescription) &&
+ string.IsNullOrEmpty(eventContext.ErrorUri))
+ {
+ Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge);
+ }
+ else
+ {
+ // https://tools.ietf.org/html/rfc6750#section-3.1
+ // WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired"
+ var builder = new StringBuilder(Options.Challenge);
+ if (Options.Challenge.IndexOf(" ", StringComparison.Ordinal) > 0)
+ {
+ // Only add a comma after the first param, if any
+ builder.Append(',');
+ }
+ if (!string.IsNullOrEmpty(eventContext.Error))
+ {
+ builder.Append(" error=\"");
+ builder.Append(eventContext.Error);
+ builder.Append("\"");
+ }
+ if (!string.IsNullOrEmpty(eventContext.ErrorDescription))
+ {
+ if (!string.IsNullOrEmpty(eventContext.Error))
+ {
+ builder.Append(",");
+ }
+
+ builder.Append(" error_description=\"");
+ builder.Append(eventContext.ErrorDescription);
+ builder.Append('\"');
+ }
+ if (!string.IsNullOrEmpty(eventContext.ErrorUri))
+ {
+ if (!string.IsNullOrEmpty(eventContext.Error) ||
+ !string.IsNullOrEmpty(eventContext.ErrorDescription))
+ {
+ builder.Append(",");
+ }
+
+ builder.Append(" error_uri=\"");
+ builder.Append(eventContext.ErrorUri);
+ builder.Append('\"');
+ }
+
+ Response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString());
+ }
+ }
+
+ private static string CreateErrorDescription(Exception authFailure)
+ {
+ IEnumerable<Exception> exceptions;
+ if (authFailure is AggregateException)
+ {
+ var agEx = authFailure as AggregateException;
+ exceptions = agEx.InnerExceptions;
+ }
+ else
+ {
+ exceptions = new[] { authFailure };
+ }
+
+ var messages = new List<string>();
+
+ foreach (var ex in exceptions)
+ {
+ // Order sensitive, some of these exceptions derive from others
+ // and we want to display the most specific message possible.
+ if (ex is SecurityTokenInvalidAudienceException)
+ {
+ messages.Add("The audience is invalid");
+ }
+ else if (ex is SecurityTokenInvalidIssuerException)
+ {
+ messages.Add("The issuer is invalid");
+ }
+ else if (ex is SecurityTokenNoExpirationException)
+ {
+ messages.Add("The token has no expiration");
+ }
+ else if (ex is SecurityTokenInvalidLifetimeException)
+ {
+ messages.Add("The token lifetime is invalid");
+ }
+ else if (ex is SecurityTokenNotYetValidException)
+ {
+ messages.Add("The token is not valid yet");
+ }
+ else if (ex is SecurityTokenExpiredException)
+ {
+ messages.Add("The token is expired");
+ }
+ else if (ex is SecurityTokenSignatureKeyNotFoundException)
+ {
+ messages.Add("The signature key was not found");
+ }
+ else if (ex is SecurityTokenInvalidSignatureException)
+ {
+ messages.Add("The signature is invalid");
+ }
+ }
+
+ return string.Join("; ", messages);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs
new file mode 100644
index 0000000000..0d0a88e247
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs
@@ -0,0 +1,114 @@
+// 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.IdentityModel.Tokens.Jwt;
+using System.Net.Http;
+using Microsoft.IdentityModel.Protocols;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer
+{
+ /// <summary>
+ /// Options class provides information needed to control Bearer Authentication handler behavior
+ /// </summary>
+ public class JwtBearerOptions : AuthenticationSchemeOptions
+ {
+ /// <summary>
+ /// Gets or sets if HTTPS is required for the metadata address or authority.
+ /// The default is true. This should be disabled only in development environments.
+ /// </summary>
+ public bool RequireHttpsMetadata { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the discovery endpoint for obtaining metadata
+ /// </summary>
+ public string MetadataAddress { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Authority to use when making OpenIdConnect calls.
+ /// </summary>
+ public string Authority { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audience for any received OpenIdConnect token.
+ /// </summary>
+ /// <value>
+ /// The expected audience for any received OpenIdConnect token.
+ /// </value>
+ public string Audience { get; set; }
+
+ /// <summary>
+ /// Gets or sets the challenge to put in the "WWW-Authenticate" header.
+ /// </summary>
+ public string Challenge { get; set; } = JwtBearerDefaults.AuthenticationScheme;
+
+ /// <summary>
+ /// The object provided by the application to process events raised by the bearer authentication handler.
+ /// The application may implement the interface fully, or it may create an instance of JwtBearerEvents
+ /// and assign delegates only to the events it wants to process.
+ /// </summary>
+ public new JwtBearerEvents Events
+ {
+ get { return (JwtBearerEvents)base.Events; }
+ set { base.Events = value; }
+ }
+
+ /// <summary>
+ /// The HttpMessageHandler used to retrieve metadata.
+ /// This cannot be set at the same time as BackchannelCertificateValidator unless the value
+ /// is a WebRequestHandler.
+ /// </summary>
+ public HttpMessageHandler BackchannelHttpHandler { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timeout when using the backchannel to make an http call.
+ /// </summary>
+ public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromMinutes(1);
+
+ /// <summary>
+ /// Configuration provided directly by the developer. If provided, then MetadataAddress and the Backchannel properties
+ /// will not be used. This information should not be updated during request processing.
+ /// </summary>
+ public OpenIdConnectConfiguration Configuration { get; set; }
+
+ /// <summary>
+ /// Responsible for retrieving, caching, and refreshing the configuration from metadata.
+ /// If not provided, then one will be created using the MetadataAddress and Backchannel properties.
+ /// </summary>
+ public IConfigurationManager<OpenIdConnectConfiguration> ConfigurationManager { get; set; }
+
+ /// <summary>
+ /// Gets or sets if a metadata refresh should be attempted after a SecurityTokenSignatureKeyNotFoundException. This allows for automatic
+ /// recovery in the event of a signature key rollover. This is enabled by default.
+ /// </summary>
+ public bool RefreshOnIssuerKeyNotFound { get; set; } = true;
+
+ /// <summary>
+ /// Gets the ordered list of <see cref="ISecurityTokenValidator"/> used to validate access tokens.
+ /// </summary>
+ public IList<ISecurityTokenValidator> SecurityTokenValidators { get; } = new List<ISecurityTokenValidator> { new JwtSecurityTokenHandler() };
+
+ /// <summary>
+ /// Gets or sets the parameters used to validate identity tokens.
+ /// </summary>
+ /// <remarks>Contains the types and definitions required for validating a token.</remarks>
+ /// <exception cref="ArgumentNullException">if 'value' is null.</exception>
+ public TokenValidationParameters TokenValidationParameters { get; set; } = new TokenValidationParameters();
+
+ /// <summary>
+ /// Defines whether the bearer token should be stored in the
+ /// <see cref="Http.Authentication.AuthenticationProperties"/> after a successful authorization.
+ /// </summary>
+ public bool SaveToken { get; set; } = true;
+
+ /// <summary>
+ /// Defines whether the token validation errors should be returned to the caller.
+ /// Enabled by default, this option can be disabled to prevent the JWT handler
+ /// from returning an error and an error_description in the WWW-Authenticate header.
+ /// </summary>
+ public bool IncludeErrorDetails { get; set; } = true;
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerPostConfigureOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerPostConfigureOptions.cs
new file mode 100644
index 0000000000..8829bfac0f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerPostConfigureOptions.cs
@@ -0,0 +1,63 @@
+// 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.Net.Http;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Protocols;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer
+{
+ /// <summary>
+ /// Used to setup defaults for all <see cref="JwtBearerOptions"/>.
+ /// </summary>
+ public class JwtBearerPostConfigureOptions : IPostConfigureOptions<JwtBearerOptions>
+ {
+ /// <summary>
+ /// Invoked to post configure a JwtBearerOptions instance.
+ /// </summary>
+ /// <param name="name">The name of the options instance being configured.</param>
+ /// <param name="options">The options instance to configure.</param>
+ public void PostConfigure(string name, JwtBearerOptions options)
+ {
+ if (string.IsNullOrEmpty(options.TokenValidationParameters.ValidAudience) && !string.IsNullOrEmpty(options.Audience))
+ {
+ options.TokenValidationParameters.ValidAudience = options.Audience;
+ }
+
+ if (options.ConfigurationManager == null)
+ {
+ if (options.Configuration != null)
+ {
+ options.ConfigurationManager = new StaticConfigurationManager<OpenIdConnectConfiguration>(options.Configuration);
+ }
+ else if (!(string.IsNullOrEmpty(options.MetadataAddress) && string.IsNullOrEmpty(options.Authority)))
+ {
+ if (string.IsNullOrEmpty(options.MetadataAddress) && !string.IsNullOrEmpty(options.Authority))
+ {
+ options.MetadataAddress = options.Authority;
+ if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal))
+ {
+ options.MetadataAddress += "/";
+ }
+
+ options.MetadataAddress += ".well-known/openid-configuration";
+ }
+
+ if (options.RequireHttpsMetadata && !options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false.");
+ }
+
+ var httpClient = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler());
+ httpClient.Timeout = options.BackchannelTimeout;
+ httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
+
+ options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.MetadataAddress, new OpenIdConnectConfigurationRetriever(),
+ new HttpDocumentRetriever(httpClient) { RequireHttps = options.RequireHttpsMetadata });
+ }
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/LoggingExtensions.cs
new file mode 100644
index 0000000000..5c6ca088a8
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/LoggingExtensions.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 System;
+
+namespace Microsoft.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static Action<ILogger, Exception> _tokenValidationFailed;
+ private static Action<ILogger, Exception> _tokenValidationSucceeded;
+ private static Action<ILogger, Exception> _errorProcessingMessage;
+
+ static LoggingExtensions()
+ {
+ _tokenValidationFailed = LoggerMessage.Define(
+ eventId: 1,
+ logLevel: LogLevel.Information,
+ formatString: "Failed to validate the token.");
+ _tokenValidationSucceeded = LoggerMessage.Define(
+ eventId: 2,
+ logLevel: LogLevel.Information,
+ formatString: "Successfully validated the token.");
+ _errorProcessingMessage = LoggerMessage.Define(
+ eventId: 3,
+ logLevel: LogLevel.Error,
+ formatString: "Exception occurred while processing message.");
+ }
+
+ public static void TokenValidationFailed(this ILogger logger, Exception ex)
+ => _tokenValidationFailed(logger, ex);
+
+ public static void TokenValidationSucceeded(this ILogger logger)
+ => _tokenValidationSucceeded(logger, null);
+
+ public static void ErrorProcessingMessage(this ILogger logger, Exception ex)
+ => _errorProcessingMessage(logger, ex);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj
new file mode 100644
index 0000000000..e5bae5a3da
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core middleware that enables an application to receive an OpenID Connect bearer token.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;authentication;security</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.Authentication\Microsoft.AspNetCore.Authentication.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="$(MicrosoftIdentityModelProtocolsOpenIdConnectPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..e95b8e061b
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/Resources.Designer.cs
@@ -0,0 +1,58 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Authentication.JwtBearer
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Authentication.JwtBearer.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string Exception_OptionMustBeProvided
+ {
+ get => GetString("Exception_OptionMustBeProvided");
+ }
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string FormatException_OptionMustBeProvided(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0);
+
+ /// <summary>
+ /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.
+ /// </summary>
+ internal static string Exception_ValidatorHandlerMismatch
+ {
+ get => GetString("Exception_ValidatorHandlerMismatch");
+ }
+
+ /// <summary>
+ /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.
+ /// </summary>
+ internal static string FormatException_ValidatorHandlerMismatch()
+ => GetString("Exception_ValidatorHandlerMismatch");
+
+ 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/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Resources.resx
new file mode 100644
index 0000000000..2a19bea96a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/Resources.resx
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Exception_OptionMustBeProvided" xml:space="preserve">
+ <value>The '{0}' option must be provided.</value>
+ </data>
+ <data name="Exception_ValidatorHandlerMismatch" xml:space="preserve">
+ <value>An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json
new file mode 100644
index 0000000000..d3839022b5
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json
@@ -0,0 +1,1064 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.JwtBearer, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.JwtBearerExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddJwtBearer",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddJwtBearer",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddJwtBearer",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddJwtBearer",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.JwtBearerAppBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseJwtBearerAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseJwtBearerAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.AuthenticationFailedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Exception",
+ "Parameters": [],
+ "ReturnType": "System.Exception",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Exception",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Exception"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerChallengeContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_AuthenticateFailure",
+ "Parameters": [],
+ "ReturnType": "System.Exception",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AuthenticateFailure",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Exception"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Error",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Error",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ErrorDescription",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ErrorDescription",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ErrorUri",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ErrorUri",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Handled",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleResponse",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_OnAuthenticationFailed",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.JwtBearer.AuthenticationFailedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnAuthenticationFailed",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.JwtBearer.AuthenticationFailedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnMessageReceived",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.JwtBearer.MessageReceivedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnMessageReceived",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.JwtBearer.MessageReceivedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnTokenValidated",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnTokenValidated",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnChallenge",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerChallengeContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnChallenge",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerChallengeContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AuthenticationFailed",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.AuthenticationFailedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MessageReceived",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.MessageReceivedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "TokenValidated",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Challenge",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerChallengeContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.MessageReceivedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Token",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Token",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_SecurityToken",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Tokens.SecurityToken",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SecurityToken",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Tokens.SecurityToken"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "AuthenticationScheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": [],
+ "Constant": true,
+ "Literal": "\"Bearer\""
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationHandler<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateEventsAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Object>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleAuthenticateAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleChallengeAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "dataProtection",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_RequireHttpsMetadata",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RequireHttpsMetadata",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_MetadataAddress",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_MetadataAddress",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Authority",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Authority",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Audience",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Audience",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Challenge",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Challenge",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_BackchannelHttpHandler",
+ "Parameters": [],
+ "ReturnType": "System.Net.Http.HttpMessageHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_BackchannelHttpHandler",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Net.Http.HttpMessageHandler"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_BackchannelTimeout",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_BackchannelTimeout",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Configuration",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Configuration",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ConfigurationManager",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.IConfigurationManager<Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ConfigurationManager",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.IConfigurationManager<Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RefreshOnIssuerKeyNotFound",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RefreshOnIssuerKeyNotFound",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SecurityTokenValidators",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<Microsoft.IdentityModel.Tokens.ISecurityTokenValidator>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TokenValidationParameters",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Tokens.TokenValidationParameters",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_TokenValidationParameters",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Tokens.TokenValidationParameters"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SaveToken",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SaveToken",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IncludeErrorDetails",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_IncludeErrorDetails",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.Extensions.Options.IPostConfigureOptions<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "PostConfigure",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj
new file mode 100644
index 0000000000..0eddc6f764
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core middleware that enables an application to support the Microsoft Account authentication workflow.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;authentication;security</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.Authentication.OAuth\Microsoft.AspNetCore.Authentication.OAuth.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs
new file mode 100644
index 0000000000..7fd71d7a9b
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs
@@ -0,0 +1,37 @@
+// 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.Authentication.MicrosoftAccount;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Extension methods to add Microsoft Account authentication capabilities to an HTTP application pipeline.
+ /// </summary>
+ public static class MicrosoftAccountAppBuilderExtensions
+ {
+ /// <summary>
+ /// UseMicrosoftAccountAuthentication is obsolete. Configure MicrosoftAccount authentication with AddAuthentication().AddMicrosoftAccount in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseMicrosoftAccountAuthentication is obsolete. Configure MicrosoftAccount authentication with AddAuthentication().AddMicrosoftAccount in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseMicrosoftAccountAuthentication(this IApplicationBuilder app)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+
+ /// <summary>
+ /// UseMicrosoftAccountAuthentication is obsolete. Configure MicrosoftAccount authentication with AddAuthentication().AddMicrosoftAccount in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <param name="options">A <see cref="MicrosoftAccountOptions"/> that specifies options for the handler.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseMicrosoftAccountAuthentication is obsolete. Configure MicrosoftAccount authentication with AddAuthentication().AddMicrosoftAccount in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseMicrosoftAccountAuthentication(this IApplicationBuilder app, MicrosoftAccountOptions options)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs
new file mode 100644
index 0000000000..1b0859c5b7
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs
@@ -0,0 +1,18 @@
+// 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.Authentication.MicrosoftAccount
+{
+ public static class MicrosoftAccountDefaults
+ {
+ public const string AuthenticationScheme = "Microsoft";
+
+ public static readonly string DisplayName = "Microsoft";
+
+ public static readonly string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
+
+ public static readonly string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
+
+ public static readonly string UserInformationEndpoint = "https://graph.microsoft.com/v1.0/me";
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountExtensions.cs
new file mode 100644
index 0000000000..7f24e5af77
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountExtensions.cs
@@ -0,0 +1,24 @@
+// 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.Authentication;
+using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class MicrosoftAccountExtensions
+ {
+ public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder)
+ => builder.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, _ => { });
+
+ public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, Action<MicrosoftAccountOptions> configureOptions)
+ => builder.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, configureOptions);
+
+ public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, Action<MicrosoftAccountOptions> configureOptions)
+ => builder.AddMicrosoftAccount(authenticationScheme, MicrosoftAccountDefaults.DisplayName, configureOptions);
+
+ public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<MicrosoftAccountOptions> configureOptions)
+ => builder.AddOAuth<MicrosoftAccountOptions, MicrosoftAccountHandler>(authenticationScheme, displayName, configureOptions);
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs
new file mode 100644
index 0000000000..bba5472774
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs
@@ -0,0 +1,42 @@
+// 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.Net.Http;
+using System.Net.Http.Headers;
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount
+{
+ public class MicrosoftAccountHandler : OAuthHandler<MicrosoftAccountOptions>
+ {
+ public MicrosoftAccountHandler(IOptionsMonitor<MicrosoftAccountOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
+ : base(options, logger, encoder, clock)
+ { }
+
+ protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
+
+ var response = await Backchannel.SendAsync(request, Context.RequestAborted);
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new HttpRequestException($"An error occurred when retrieving Microsoft user information ({response.StatusCode}). Please check if the authentication information is correct and the corresponding Microsoft Account API is enabled.");
+ }
+
+ var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
+
+ var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload);
+ context.RunClaimActions();
+
+ await Events.CreatingTicket(context);
+ return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs
new file mode 100644
index 0000000000..dbca3507e9
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs
@@ -0,0 +1,35 @@
+// 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.Claims;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Authentication.OAuth;
+
+namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount
+{
+ /// <summary>
+ /// Configuration options for <see cref="MicrosoftAccountHandler"/>.
+ /// </summary>
+ public class MicrosoftAccountOptions : OAuthOptions
+ {
+ /// <summary>
+ /// Initializes a new <see cref="MicrosoftAccountOptions"/>.
+ /// </summary>
+ public MicrosoftAccountOptions()
+ {
+ CallbackPath = new PathString("/signin-microsoft");
+ AuthorizationEndpoint = MicrosoftAccountDefaults.AuthorizationEndpoint;
+ TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint;
+ UserInformationEndpoint = MicrosoftAccountDefaults.UserInformationEndpoint;
+ Scope.Add("https://graph.microsoft.com/user.read");
+
+ ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
+ ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName");
+ ClaimActions.MapJsonKey(ClaimTypes.GivenName, "givenName");
+ ClaimActions.MapJsonKey(ClaimTypes.Surname, "surname");
+ ClaimActions.MapCustomJson(ClaimTypes.Email, user => user.Value<string>("mail") ?? user.Value<string>("userPrincipalName"));
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..7ef5acecb2
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/Resources.Designer.cs
@@ -0,0 +1,72 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Authentication.MicrosoftAccount.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// The user does not have an id.
+ /// </summary>
+ internal static string Exception_MissingId
+ {
+ get => GetString("Exception_MissingId");
+ }
+
+ /// <summary>
+ /// The user does not have an id.
+ /// </summary>
+ internal static string FormatException_MissingId()
+ => GetString("Exception_MissingId");
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string Exception_OptionMustBeProvided
+ {
+ get => GetString("Exception_OptionMustBeProvided");
+ }
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string FormatException_OptionMustBeProvided(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0);
+
+ /// <summary>
+ /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.
+ /// </summary>
+ internal static string Exception_ValidatorHandlerMismatch
+ {
+ get => GetString("Exception_ValidatorHandlerMismatch");
+ }
+
+ /// <summary>
+ /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.
+ /// </summary>
+ internal static string FormatException_ValidatorHandlerMismatch()
+ => GetString("Exception_ValidatorHandlerMismatch");
+
+ 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/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Resources.resx
new file mode 100644
index 0000000000..26eb43888e
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Resources.resx
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Exception_MissingId" xml:space="preserve">
+ <value>The user does not have an id.</value>
+ </data>
+ <data name="Exception_OptionMustBeProvided" xml:space="preserve">
+ <value>The '{0}' option must be provided.</value>
+ </data>
+ <data name="Exception_ValidatorHandlerMismatch" xml:space="preserve">
+ <value>An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/baseline.netcore.json
new file mode 100644
index 0000000000..877e9035ac
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/baseline.netcore.json
@@ -0,0 +1,284 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.MicrosoftAccount, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.MicrosoftAccountExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddMicrosoftAccount",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddMicrosoftAccount",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddMicrosoftAccount",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddMicrosoftAccount",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountDefaults",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "DisplayName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AuthorizationEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "TokenEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "UserInformationEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AuthenticationScheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": [],
+ "Constant": true,
+ "Literal": "\"Microsoft\""
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountHandler",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler<Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateTicketAsync",
+ "Parameters": [
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "tokens",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationTicket>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.MicrosoftAccountAppBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseMicrosoftAccountAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseMicrosoftAccountAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimAction.cs
new file mode 100644
index 0000000000..78b63bb38e
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimAction.cs
@@ -0,0 +1,42 @@
+// 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.Claims;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth.Claims
+{
+ /// <summary>
+ /// Infrastructure for mapping user data from a json structure to claims on the ClaimsIdentity.
+ /// </summary>
+ public abstract class ClaimAction
+ {
+ /// <summary>
+ /// Create a new claim manipulation action.
+ /// </summary>
+ /// <param name="claimType">The value to use for Claim.Type when creating a Claim.</param>
+ /// <param name="valueType">The value to use for Claim.ValueType when creating a Claim.</param>
+ public ClaimAction(string claimType, string valueType)
+ {
+ ClaimType = claimType;
+ ValueType = valueType;
+ }
+
+ /// <summary>
+ /// The value to use for Claim.Type when creating a Claim.
+ /// </summary>
+ public string ClaimType { get; }
+
+ // The value to use for Claim.ValueType when creating a Claim.
+ public string ValueType { get; }
+
+ /// <summary>
+ /// Examine the given userData json, determine if the requisite data is present, and optionally add it
+ /// as a new Claim on the ClaimsIdentity.
+ /// </summary>
+ /// <param name="userData">The source data to examine. This value may be null.</param>
+ /// <param name="identity">The identity to add Claims to.</param>
+ /// <param name="issuer">The value to use for Claim.Issuer when creating a Claim.</param>
+ public abstract void Run(JObject userData, ClaimsIdentity identity, string issuer);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollection.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollection.cs
new file mode 100644
index 0000000000..63da155d7c
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollection.cs
@@ -0,0 +1,52 @@
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth.Claims
+{
+ /// <summary>
+ /// A collection of ClaimActions used when mapping user data to Claims.
+ /// </summary>
+ public class ClaimActionCollection : IEnumerable<ClaimAction>
+ {
+ private IList<ClaimAction> Actions { get; } = new List<ClaimAction>();
+
+ /// <summary>
+ /// Remove all claim actions.
+ /// </summary>
+ public void Clear() => Actions.Clear();
+
+ /// <summary>
+ /// Remove all claim actions for the given ClaimType.
+ /// </summary>
+ /// <param name="claimType">The ClaimType of maps to remove.</param>
+ public void Remove(string claimType)
+ {
+ var itemsToRemove = Actions.Where(map => string.Equals(claimType, map.ClaimType, StringComparison.OrdinalIgnoreCase)).ToList();
+ itemsToRemove.ForEach(map => Actions.Remove(map));
+ }
+
+ /// <summary>
+ /// Add a claim action to the collection.
+ /// </summary>
+ /// <param name="action">The claim action to add.</param>
+ public void Add(ClaimAction action)
+ {
+ Actions.Add(action);
+ }
+
+ public IEnumerator<ClaimAction> GetEnumerator()
+ {
+ return Actions.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return Actions.GetEnumerator();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs
new file mode 100644
index 0000000000..5a178957a0
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs
@@ -0,0 +1,139 @@
+// 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 Microsoft.AspNetCore.Authentication.OAuth.Claims;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public static class ClaimActionCollectionMapExtensions
+ {
+ /// <summary>
+ /// Select a top level value from the json user data with the given key name and add it as a Claim.
+ /// This no-ops if the key is not found or the value is empty.
+ /// </summary>
+ /// <param name="collection"></param>
+ /// <param name="claimType">The value to use for Claim.Type when creating a Claim.</param>
+ /// <param name="jsonKey">The top level key to look for in the json user data.</param>
+ public static void MapJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey)
+ {
+ collection.MapJsonKey(claimType, jsonKey, ClaimValueTypes.String);
+ }
+
+ /// <summary>
+ /// Select a top level value from the json user data with the given key name and add it as a Claim.
+ /// This no-ops if the key is not found or the value is empty.
+ /// </summary>
+ /// <param name="collection"></param>
+ /// <param name="claimType">The value to use for Claim.Type when creating a Claim.</param>
+ /// <param name="jsonKey">The top level key to look for in the json user data.</param>
+ /// <param name="valueType">The value to use for Claim.ValueType when creating a Claim.</param>
+ public static void MapJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey, string valueType)
+ {
+ collection.Add(new JsonKeyClaimAction(claimType, valueType, jsonKey));
+ }
+
+ /// <summary>
+ /// Select a second level value from the json user data with the given top level key name and second level sub key name and add it as a Claim.
+ /// This no-ops if the keys are not found or the value is empty.
+ /// </summary>
+ /// <param name="collection"></param>
+ /// <param name="claimType">The value to use for Claim.Type when creating a Claim.</param>
+ /// <param name="jsonKey">The top level key to look for in the json user data.</param>
+ /// <param name="subKey">The second level key to look for in the json user data.</param>
+ public static void MapJsonSubKey(this ClaimActionCollection collection, string claimType, string jsonKey, string subKey)
+ {
+ collection.MapJsonSubKey(claimType, jsonKey, subKey, ClaimValueTypes.String);
+ }
+
+ /// <summary>
+ /// Select a second level value from the json user data with the given top level key name and second level sub key name and add it as a Claim.
+ /// This no-ops if the keys are not found or the value is empty.
+ /// </summary>
+ /// <param name="collection"></param>
+ /// <param name="claimType">The value to use for Claim.Type when creating a Claim.</param>
+ /// <param name="jsonKey">The top level key to look for in the json user data.</param>
+ /// <param name="subKey">The second level key to look for in the json user data.</param>
+ /// <param name="valueType">The value to use for Claim.ValueType when creating a Claim.</param>
+ public static void MapJsonSubKey(this ClaimActionCollection collection, string claimType, string jsonKey, string subKey, string valueType)
+ {
+ collection.Add(new JsonSubKeyClaimAction(claimType, valueType, jsonKey, subKey));
+ }
+
+ /// <summary>
+ /// Run the given resolver to select a value from the json user data to add as a claim.
+ /// This no-ops if the returned value is empty.
+ /// </summary>
+ /// <param name="collection"></param>
+ /// <param name="claimType">The value to use for Claim.Type when creating a Claim.</param>
+ /// <param name="resolver">The Func that will be called to select value from the given json user data.</param>
+ public static void MapCustomJson(this ClaimActionCollection collection, string claimType, Func<JObject, string> resolver)
+ {
+ collection.MapCustomJson(claimType, ClaimValueTypes.String, resolver);
+ }
+
+ /// <summary>
+ /// Run the given resolver to select a value from the json user data to add as a claim.
+ /// This no-ops if the returned value is empty.
+ /// </summary>
+ /// <param name="collection"></param>
+ /// <param name="claimType">The value to use for Claim.Type when creating a Claim.</param>
+ /// <param name="valueType">The value to use for Claim.ValueType when creating a Claim.</param>
+ /// <param name="resolver">The Func that will be called to select value from the given json user data.</param>
+ public static void MapCustomJson(this ClaimActionCollection collection, string claimType, string valueType, Func<JObject, string> resolver)
+ {
+ collection.Add(new CustomJsonClaimAction(claimType, valueType, resolver));
+ }
+
+ /// <summary>
+ /// Clears any current ClaimsActions and maps all values from the json user data as claims, excluding duplicates.
+ /// </summary>
+ /// <param name="collection"></param>
+ public static void MapAll(this ClaimActionCollection collection)
+ {
+ collection.Clear();
+ collection.Add(new MapAllClaimsAction());
+ }
+
+ /// <summary>
+ /// Clears any current ClaimsActions and maps all values from the json user data as claims, excluding the specified types.
+ /// </summary>
+ /// <param name="collection"></param>
+ /// <param name="exclusions"></param>
+ public static void MapAllExcept(this ClaimActionCollection collection, params string[] exclusions)
+ {
+ collection.MapAll();
+ collection.DeleteClaims(exclusions);
+ }
+
+ /// <summary>
+ /// Delete all claims from the given ClaimsIdentity with the given ClaimType.
+ /// </summary>
+ /// <param name="collection"></param>
+ /// <param name="claimType"></param>
+ public static void DeleteClaim(this ClaimActionCollection collection, string claimType)
+ {
+ collection.Add(new DeleteClaimAction(claimType));
+ }
+
+ /// <summary>
+ /// Delete all claims from the ClaimsIdentity with the given claimTypes.
+ /// </summary>
+ /// <param name="collection"></param>
+ /// <param name="claimTypes"></param>
+ public static void DeleteClaims(this ClaimActionCollection collection, params string[] claimTypes)
+ {
+ if (claimTypes == null)
+ {
+ throw new ArgumentNullException(nameof(claimTypes));
+ }
+
+ foreach (var claimType in claimTypes)
+ {
+ collection.Add(new DeleteClaimAction(claimType));
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/CustomJsonClaimAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/CustomJsonClaimAction.cs
new file mode 100644
index 0000000000..21a4f70e12
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/CustomJsonClaimAction.cs
@@ -0,0 +1,46 @@
+// 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 Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth.Claims
+{
+ /// <summary>
+ /// A ClaimAction that selects the value from the json user data by running the given Func resolver.
+ /// </summary>
+ public class CustomJsonClaimAction : ClaimAction
+ {
+ /// <summary>
+ /// Creates a new CustomJsonClaimAction.
+ /// </summary>
+ /// <param name="claimType">The value to use for Claim.Type when creating a Claim.</param>
+ /// <param name="valueType">The value to use for Claim.ValueType when creating a Claim.</param>
+ /// <param name="resolver">The Func that will be called to select value from the given json user data.</param>
+ public CustomJsonClaimAction(string claimType, string valueType, Func<JObject, string> resolver)
+ : base(claimType, valueType)
+ {
+ Resolver = resolver;
+ }
+
+ /// <summary>
+ /// The Func that will be called to select value from the given json user data.
+ /// </summary>
+ public Func<JObject, string> Resolver { get; }
+
+ /// <inheritdoc />
+ public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
+ {
+ if (userData == null)
+ {
+ return;
+ }
+ var value = Resolver(userData);
+ if (!string.IsNullOrEmpty(value))
+ {
+ identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer));
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/DeleteClaimAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/DeleteClaimAction.cs
new file mode 100644
index 0000000000..75167cabcb
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/DeleteClaimAction.cs
@@ -0,0 +1,33 @@
+// 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.Security.Claims;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth.Claims
+{
+ /// <summary>
+ /// A ClaimAction that deletes all claims from the given ClaimsIdentity with the given ClaimType.
+ /// </summary>
+ public class DeleteClaimAction : ClaimAction
+ {
+ /// <summary>
+ /// Creates a new DeleteClaimAction.
+ /// </summary>
+ /// <param name="claimType">The ClaimType of Claims to delete.</param>
+ public DeleteClaimAction(string claimType)
+ : base(claimType, ClaimValueTypes.String)
+ {
+ }
+
+ /// <inheritdoc />
+ public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
+ {
+ foreach (var claim in identity.FindAll(ClaimType).ToList())
+ {
+ identity.TryRemoveClaim(claim);
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs
new file mode 100644
index 0000000000..ccd1a965dc
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.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.Security.Claims;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth.Claims
+{
+ /// <summary>
+ /// A ClaimAction that selects a top level value from the json user data with the given key name and adds it as a Claim.
+ /// This no-ops if the key is not found or the value is empty.
+ /// </summary>
+ public class JsonKeyClaimAction : ClaimAction
+ {
+ /// <summary>
+ /// Creates a new JsonKeyClaimAction.
+ /// </summary>
+ /// <param name="claimType">The value to use for Claim.Type when creating a Claim.</param>
+ /// <param name="valueType">The value to use for Claim.ValueType when creating a Claim.</param>
+ /// <param name="jsonKey">The top level key to look for in the json user data.</param>
+ public JsonKeyClaimAction(string claimType, string valueType, string jsonKey)
+ : base(claimType, valueType)
+ {
+ JsonKey = jsonKey;
+ }
+
+ /// <summary>
+ /// The top level key to look for in the json user data.
+ /// </summary>
+ public string JsonKey { get; }
+
+ /// <inheritdoc />
+ public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
+ {
+ var value = userData?[JsonKey];
+ if (value is JValue)
+ {
+ AddClaim(value?.ToString(), identity, issuer);
+ }
+ else if (value is JArray)
+ {
+ foreach (var v in value)
+ {
+ AddClaim(v?.ToString(), identity, issuer);
+ }
+ }
+ }
+
+ private void AddClaim(string value, ClaimsIdentity identity, string issuer)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer));
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonSubKeyClaimAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonSubKeyClaimAction.cs
new file mode 100644
index 0000000000..bc29672d0f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonSubKeyClaimAction.cs
@@ -0,0 +1,58 @@
+// 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 Newtonsoft.Json.Linq;
+using System.Security.Claims;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth.Claims
+{
+ /// <summary>
+ /// A ClaimAction that selects a second level value from the json user data with the given top level key
+ /// name and second level sub key name and add it as a Claim.
+ /// This no-ops if the keys are not found or the value is empty.
+ /// </summary>
+ public class JsonSubKeyClaimAction : JsonKeyClaimAction
+ {
+ /// <summary>
+ /// Creates a new JsonSubKeyClaimAction.
+ /// </summary>
+ /// <param name="claimType">The value to use for Claim.Type when creating a Claim.</param>
+ /// <param name="valueType">The value to use for Claim.ValueType when creating a Claim.</param>
+ /// <param name="jsonKey">The top level key to look for in the json user data.</param>
+ /// <param name="subKey">The second level key to look for in the json user data.</param>
+ public JsonSubKeyClaimAction(string claimType, string valueType, string jsonKey, string subKey)
+ : base(claimType, valueType, jsonKey)
+ {
+ SubKey = subKey;
+ }
+
+ /// <summary>
+ /// The second level key to look for in the json user data.
+ /// </summary>
+ public string SubKey { get; }
+
+ /// <inheritdoc />
+ public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
+ {
+ var value = GetValue(userData, JsonKey, SubKey);
+ if (!string.IsNullOrEmpty(value))
+ {
+ identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer));
+ }
+ }
+
+ // Get the given subProperty from a property.
+ private static string GetValue(JObject userData, string propertyName, string subProperty)
+ {
+ if (userData != null && userData.TryGetValue(propertyName, out var value))
+ {
+ var subObject = JObject.Parse(value.ToString());
+ if (subObject != null && subObject.TryGetValue(subProperty, out value))
+ {
+ return value.ToString();
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/MapAllClaimsAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/MapAllClaimsAction.cs
new file mode 100644
index 0000000000..b3bf5d99f1
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/MapAllClaimsAction.cs
@@ -0,0 +1,42 @@
+// 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 Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth.Claims
+{
+ /// <summary>
+ /// A ClaimAction that selects all top level values from the json user data and adds them as Claims.
+ /// This excludes duplicate sets of names and values.
+ /// </summary>
+ public class MapAllClaimsAction : ClaimAction
+ {
+ public MapAllClaimsAction() : base("All", ClaimValueTypes.String)
+ {
+ }
+
+ public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
+ {
+ if (userData == null)
+ {
+ return;
+ }
+ foreach (var pair in userData)
+ {
+ var claimValue = userData.TryGetValue(pair.Key, out var value) ? value.ToString() : null;
+
+ // Avoid adding a claim if there's a duplicate name and value. This often happens in OIDC when claims are
+ // retrieved both from the id_token and from the user-info endpoint.
+ var duplicate = identity.FindFirst(c => string.Equals(c.Type, pair.Key, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(c.Value, claimValue, StringComparison.Ordinal)) != null;
+
+ if (!duplicate)
+ {
+ identity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, issuer));
+ }
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs
new file mode 100644
index 0000000000..f660dd2247
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs
@@ -0,0 +1,152 @@
+// 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.Globalization;
+using System.Net.Http;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Http;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth
+{
+ /// <summary>
+ /// Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.
+ /// </summary>
+ public class OAuthCreatingTicketContext : ResultContext<OAuthOptions>
+ {
+ /// <summary>
+ /// Initializes a new <see cref="OAuthCreatingTicketContext"/>.
+ /// </summary>
+ /// <param name="principal">The <see cref="ClaimsPrincipal"/>.</param>
+ /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
+ /// <param name="context">The HTTP environment.</param>
+ /// <param name="scheme">The authentication scheme.</param>
+ /// <param name="options">The options used by the authentication middleware.</param>
+ /// <param name="backchannel">The HTTP client used by the authentication middleware</param>
+ /// <param name="tokens">The tokens returned from the token endpoint.</param>
+ public OAuthCreatingTicketContext(
+ ClaimsPrincipal principal,
+ AuthenticationProperties properties,
+ HttpContext context,
+ AuthenticationScheme scheme,
+ OAuthOptions options,
+ HttpClient backchannel,
+ OAuthTokenResponse tokens)
+ : this(principal, properties, context, scheme, options, backchannel, tokens, user: new JObject())
+ { }
+
+ /// <summary>
+ /// Initializes a new <see cref="OAuthCreatingTicketContext"/>.
+ /// </summary>
+ /// <param name="principal">The <see cref="ClaimsPrincipal"/>.</param>
+ /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
+ /// <param name="context">The HTTP environment.</param>
+ /// <param name="scheme">The authentication scheme.</param>
+ /// <param name="options">The options used by the authentication middleware.</param>
+ /// <param name="backchannel">The HTTP client used by the authentication middleware</param>
+ /// <param name="tokens">The tokens returned from the token endpoint.</param>
+ /// <param name="user">The JSON-serialized user.</param>
+ public OAuthCreatingTicketContext(
+ ClaimsPrincipal principal,
+ AuthenticationProperties properties,
+ HttpContext context,
+ AuthenticationScheme scheme,
+ OAuthOptions options,
+ HttpClient backchannel,
+ OAuthTokenResponse tokens,
+ JObject user)
+ : base(context, scheme, options)
+ {
+ if (backchannel == null)
+ {
+ throw new ArgumentNullException(nameof(backchannel));
+ }
+
+ if (tokens == null)
+ {
+ throw new ArgumentNullException(nameof(tokens));
+ }
+
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ TokenResponse = tokens;
+ Backchannel = backchannel;
+ User = user;
+ Principal = principal;
+ Properties = properties;
+ }
+
+ /// <summary>
+ /// Gets the JSON-serialized user or an empty
+ /// <see cref="JObject"/> if it is not available.
+ /// </summary>
+ public JObject User { get; }
+
+ /// <summary>
+ /// Gets the token response returned by the authentication service.
+ /// </summary>
+ public OAuthTokenResponse TokenResponse { get; }
+
+ /// <summary>
+ /// Gets the access token provided by the authentication service.
+ /// </summary>
+ public string AccessToken => TokenResponse.AccessToken;
+
+ /// <summary>
+ /// Gets the access token type provided by the authentication service.
+ /// </summary>
+ public string TokenType => TokenResponse.TokenType;
+
+ /// <summary>
+ /// Gets the refresh token provided by the authentication service.
+ /// </summary>
+ public string RefreshToken => TokenResponse.RefreshToken;
+
+ /// <summary>
+ /// Gets the access token expiration time.
+ /// </summary>
+ public TimeSpan? ExpiresIn
+ {
+ get
+ {
+ int value;
+ if (int.TryParse(TokenResponse.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
+ {
+ return TimeSpan.FromSeconds(value);
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets the backchannel used to communicate with the provider.
+ /// </summary>
+ public HttpClient Backchannel { get; }
+
+ /// <summary>
+ /// Gets the main identity exposed by the authentication ticket.
+ /// This property returns <c>null</c> when the ticket is <c>null</c>.
+ /// </summary>
+ public ClaimsIdentity Identity => Principal?.Identity as ClaimsIdentity;
+
+ public void RunClaimActions() => RunClaimActions(User);
+
+ public void RunClaimActions(JObject userData)
+ {
+ if (userData == null)
+ {
+ throw new ArgumentNullException(nameof(userData));
+ }
+
+ foreach (var action in Options.ClaimActions)
+ {
+ action.Run(userData, Identity, Options.ClaimsIssuer ?? Scheme.Name);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthEvents.cs
new file mode 100644
index 0000000000..9e194491b9
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthEvents.cs
@@ -0,0 +1,41 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth
+{
+ /// <summary>
+ /// Default implementation.
+ /// </summary>
+ public class OAuthEvents : RemoteAuthenticationEvents
+ {
+ /// <summary>
+ /// Gets or sets the function that is invoked when the CreatingTicket method is invoked.
+ /// </summary>
+ public Func<OAuthCreatingTicketContext, Task> OnCreatingTicket { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Gets or sets the delegate that is invoked when the RedirectToAuthorizationEndpoint method is invoked.
+ /// </summary>
+ public Func<RedirectContext<OAuthOptions>, Task> OnRedirectToAuthorizationEndpoint { get; set; } = context =>
+ {
+ context.Response.Redirect(context.RedirectUri);
+ return Task.CompletedTask;
+ };
+
+ /// <summary>
+ /// Invoked after the provider successfully authenticates a user.
+ /// </summary>
+ /// <param name="context">Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.</param>
+ /// <returns>A <see cref="Task"/> representing the completed operation.</returns>
+ public virtual Task CreatingTicket(OAuthCreatingTicketContext context) => OnCreatingTicket(context);
+
+ /// <summary>
+ /// Called when a Challenge causes a redirect to authorize endpoint in the OAuth handler.
+ /// </summary>
+ /// <param name="context">Contains redirect URI and <see cref="AuthenticationProperties"/> of the challenge.</param>
+ public virtual Task RedirectToAuthorizationEndpoint(RedirectContext<OAuthOptions> context) => OnRedirectToAuthorizationEndpoint(context);
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.csproj
new file mode 100644
index 0000000000..5c8a5e3a96
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core middleware that enables an application to support any standard OAuth 2.0 authentication workflow.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;authentication;security</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.Authentication\Microsoft.AspNetCore.Authentication.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthAppBuilderExtensions.cs
new file mode 100644
index 0000000000..d55f311f7b
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthAppBuilderExtensions.cs
@@ -0,0 +1,37 @@
+// 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.Authentication.OAuth;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Extension methods to add OAuth 2.0 authentication capabilities to an HTTP application pipeline.
+ /// </summary>
+ public static class OAuthAppBuilderExtensions
+ {
+ /// <summary>
+ /// UseOAuthAuthentication is obsolete. Configure OAuth authentication with AddAuthentication().AddOAuth in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseOAuthAuthentication is obsolete. Configure OAuth authentication with AddAuthentication().AddOAuth in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseOAuthAuthentication(this IApplicationBuilder app)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+
+ /// <summary>
+ /// UseOAuthAuthentication is obsolete. Configure OAuth authentication with AddAuthentication().AddOAuth in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <param name="options">A <see cref="OAuthOptions"/> that specifies options for the handler.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseOAuthAuthentication is obsolete. Configure OAuth authentication with AddAuthentication().AddOAuth in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseOAuthAuthentication(this IApplicationBuilder app, OAuthOptions options)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthChallengeProperties.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthChallengeProperties.cs
new file mode 100644
index 0000000000..fc768a8ac8
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthChallengeProperties.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth
+{
+ public class OAuthChallengeProperties : AuthenticationProperties
+ {
+ /// <summary>
+ /// The parameter key for the "scope" argument being used for a challenge request.
+ /// </summary>
+ public static readonly string ScopeKey = "scope";
+
+ public OAuthChallengeProperties()
+ { }
+
+ public OAuthChallengeProperties(IDictionary<string, string> items)
+ : base(items)
+ { }
+
+ public OAuthChallengeProperties(IDictionary<string, string> items, IDictionary<string, object> parameters)
+ : base(items, parameters)
+ { }
+
+ /// <summary>
+ /// The "scope" parameter value being used for a challenge request.
+ /// </summary>
+ public ICollection<string> Scope
+ {
+ get => GetParameter<ICollection<string>>(ScopeKey);
+ set => SetParameter(ScopeKey, value);
+ }
+
+ /// <summary>
+ /// Set the "scope" parameter value.
+ /// </summary>
+ /// <param name="scopes">List of scopes.</param>
+ public virtual void SetScope(params string[] scopes)
+ {
+ Scope = scopes;
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthDefaults.cs
new file mode 100644
index 0000000000..376f8ab01a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthDefaults.cs
@@ -0,0 +1,10 @@
+// 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.Authentication.OAuth
+{
+ public static class OAuthDefaults
+ {
+ public static readonly string DisplayName = "OAuth";
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthExtensions.cs
new file mode 100644
index 0000000000..22c541a0ac
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthExtensions.cs
@@ -0,0 +1,33 @@
+// 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.Authentication;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class OAuthExtensions
+ {
+ public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action<OAuthOptions> configureOptions)
+ => builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>>(authenticationScheme, configureOptions);
+
+ public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OAuthOptions> configureOptions)
+ => builder.AddOAuth<OAuthOptions, OAuthHandler<OAuthOptions>>(authenticationScheme, displayName, configureOptions);
+
+ public static AuthenticationBuilder AddOAuth<TOptions, THandler>(this AuthenticationBuilder builder, string authenticationScheme, Action<TOptions> configureOptions)
+ where TOptions : OAuthOptions, new()
+ where THandler : OAuthHandler<TOptions>
+ => builder.AddOAuth<TOptions, THandler>(authenticationScheme, OAuthDefaults.DisplayName, configureOptions);
+
+ public static AuthenticationBuilder AddOAuth<TOptions, THandler>(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TOptions> configureOptions)
+ where TOptions : OAuthOptions, new()
+ where THandler : OAuthHandler<TOptions>
+ {
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, OAuthPostConfigureOptions<TOptions, THandler>>());
+ return builder.AddRemoteScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs
new file mode 100644
index 0000000000..808e0f9039
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs
@@ -0,0 +1,243 @@
+// 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.Globalization;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Security.Claims;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth
+{
+ public class OAuthHandler<TOptions> : RemoteAuthenticationHandler<TOptions> where TOptions : OAuthOptions, new()
+ {
+ protected HttpClient Backchannel => Options.Backchannel;
+
+ /// <summary>
+ /// The handler calls methods on the events which give the application control at certain points where processing is occurring.
+ /// If it is not provided a default instance is supplied which does nothing when the methods are called.
+ /// </summary>
+ protected new OAuthEvents Events
+ {
+ get { return (OAuthEvents)base.Events; }
+ set { base.Events = value; }
+ }
+
+ public OAuthHandler(IOptionsMonitor<TOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
+ : base(options, logger, encoder, clock)
+ { }
+
+ /// <summary>
+ /// Creates a new instance of the events instance.
+ /// </summary>
+ /// <returns>A new instance of the events instance.</returns>
+ protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new OAuthEvents());
+
+ protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
+ {
+ var query = Request.Query;
+
+ var state = query["state"];
+ var properties = Options.StateDataFormat.Unprotect(state);
+
+ if (properties == null)
+ {
+ return HandleRequestResult.Fail("The oauth state was missing or invalid.");
+ }
+
+ // OAuth2 10.12 CSRF
+ if (!ValidateCorrelationId(properties))
+ {
+ return HandleRequestResult.Fail("Correlation failed.", properties);
+ }
+
+ var error = query["error"];
+ if (!StringValues.IsNullOrEmpty(error))
+ {
+ var failureMessage = new StringBuilder();
+ failureMessage.Append(error);
+ var errorDescription = query["error_description"];
+ if (!StringValues.IsNullOrEmpty(errorDescription))
+ {
+ failureMessage.Append(";Description=").Append(errorDescription);
+ }
+ var errorUri = query["error_uri"];
+ if (!StringValues.IsNullOrEmpty(errorUri))
+ {
+ failureMessage.Append(";Uri=").Append(errorUri);
+ }
+
+ return HandleRequestResult.Fail(failureMessage.ToString(), properties);
+ }
+
+ var code = query["code"];
+
+ if (StringValues.IsNullOrEmpty(code))
+ {
+ return HandleRequestResult.Fail("Code was not found.", properties);
+ }
+
+ var tokens = await ExchangeCodeAsync(code, BuildRedirectUri(Options.CallbackPath));
+
+ if (tokens.Error != null)
+ {
+ return HandleRequestResult.Fail(tokens.Error, properties);
+ }
+
+ if (string.IsNullOrEmpty(tokens.AccessToken))
+ {
+ return HandleRequestResult.Fail("Failed to retrieve access token.", properties);
+ }
+
+ var identity = new ClaimsIdentity(ClaimsIssuer);
+
+ if (Options.SaveTokens)
+ {
+ var authTokens = new List<AuthenticationToken>();
+
+ authTokens.Add(new AuthenticationToken { Name = "access_token", Value = tokens.AccessToken });
+ if (!string.IsNullOrEmpty(tokens.RefreshToken))
+ {
+ authTokens.Add(new AuthenticationToken { Name = "refresh_token", Value = tokens.RefreshToken });
+ }
+
+ if (!string.IsNullOrEmpty(tokens.TokenType))
+ {
+ authTokens.Add(new AuthenticationToken { Name = "token_type", Value = tokens.TokenType });
+ }
+
+ if (!string.IsNullOrEmpty(tokens.ExpiresIn))
+ {
+ int value;
+ if (int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
+ {
+ // https://www.w3.org/TR/xmlschema-2/#dateTime
+ // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
+ var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value);
+ authTokens.Add(new AuthenticationToken
+ {
+ Name = "expires_at",
+ Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
+ });
+ }
+ }
+
+ properties.StoreTokens(authTokens);
+ }
+
+ var ticket = await CreateTicketAsync(identity, properties, tokens);
+ if (ticket != null)
+ {
+ return HandleRequestResult.Success(ticket);
+ }
+ else
+ {
+ return HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties);
+ }
+ }
+
+ protected virtual async Task<OAuthTokenResponse> ExchangeCodeAsync(string code, string redirectUri)
+ {
+ var tokenRequestParameters = new Dictionary<string, string>()
+ {
+ { "client_id", Options.ClientId },
+ { "redirect_uri", redirectUri },
+ { "client_secret", Options.ClientSecret },
+ { "code", code },
+ { "grant_type", "authorization_code" },
+ };
+
+ var requestContent = new FormUrlEncodedContent(tokenRequestParameters);
+
+ var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint);
+ requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+ requestMessage.Content = requestContent;
+ var response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted);
+ if (response.IsSuccessStatusCode)
+ {
+ var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
+ return OAuthTokenResponse.Success(payload);
+ }
+ else
+ {
+ var error = "OAuth token endpoint failure: " + await Display(response);
+ return OAuthTokenResponse.Failed(new Exception(error));
+ }
+ }
+
+ private static async Task<string> Display(HttpResponseMessage response)
+ {
+ var output = new StringBuilder();
+ output.Append("Status: " + response.StatusCode + ";");
+ output.Append("Headers: " + response.Headers.ToString() + ";");
+ output.Append("Body: " + await response.Content.ReadAsStringAsync() + ";");
+ return output.ToString();
+ }
+
+ protected virtual async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)
+ {
+ var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens);
+ await Events.CreatingTicket(context);
+ return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
+ }
+
+ protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
+ {
+ if (string.IsNullOrEmpty(properties.RedirectUri))
+ {
+ properties.RedirectUri = CurrentUri;
+ }
+
+ // OAuth2 10.12 CSRF
+ GenerateCorrelationId(properties);
+
+ var authorizationEndpoint = BuildChallengeUrl(properties, BuildRedirectUri(Options.CallbackPath));
+ var redirectContext = new RedirectContext<OAuthOptions>(
+ Context, Scheme, Options,
+ properties, authorizationEndpoint);
+ await Events.RedirectToAuthorizationEndpoint(redirectContext);
+ }
+
+ protected virtual string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
+ {
+ var scopeParameter = properties.GetParameter<ICollection<string>>(OAuthChallengeProperties.ScopeKey);
+ var scope = scopeParameter != null ? FormatScope(scopeParameter) : FormatScope();
+
+ var state = Options.StateDataFormat.Protect(properties);
+ var parameters = new Dictionary<string, string>
+ {
+ { "client_id", Options.ClientId },
+ { "scope", scope },
+ { "response_type", "code" },
+ { "redirect_uri", redirectUri },
+ { "state", state },
+ };
+ return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters);
+ }
+
+ /// <summary>
+ /// Format a list of OAuth scopes.
+ /// </summary>
+ /// <param name="scopes">List of scopes.</param>
+ /// <returns>Formatted scopes.</returns>
+ protected virtual string FormatScope(IEnumerable<string> scopes)
+ => string.Join(" ", scopes); // OAuth2 3.3 space separated
+
+ /// <summary>
+ /// Format the <see cref="OAuthOptions.Scope"/> property.
+ /// </summary>
+ /// <returns>Formatted scopes.</returns>
+ /// <remarks>Subclasses should rather override <see cref="FormatScope(IEnumerable{string})"/>.</remarks>
+ protected virtual string FormatScope()
+ => FormatScope(Options.Scope);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs
new file mode 100644
index 0000000000..3c71f055f5
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs
@@ -0,0 +1,108 @@
+// 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 Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.AspNetCore.Authentication.OAuth.Claims;
+using Microsoft.AspNetCore.Http.Authentication;
+using System.Globalization;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth
+{
+ /// <summary>
+ /// Configuration options OAuth.
+ /// </summary>
+ public class OAuthOptions : RemoteAuthenticationOptions
+ {
+ public OAuthOptions()
+ {
+ Events = new OAuthEvents();
+ }
+
+ /// <summary>
+ /// Check that the options are valid. Should throw an exception if things are not ok.
+ /// </summary>
+ public override void Validate()
+ {
+ base.Validate();
+
+ if (string.IsNullOrEmpty(ClientId))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ClientId)), nameof(ClientId));
+ }
+
+ if (string.IsNullOrEmpty(ClientSecret))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ClientSecret)), nameof(ClientSecret));
+ }
+
+ if (string.IsNullOrEmpty(AuthorizationEndpoint))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(AuthorizationEndpoint)), nameof(AuthorizationEndpoint));
+ }
+
+ if (string.IsNullOrEmpty(TokenEndpoint))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(TokenEndpoint)), nameof(TokenEndpoint));
+ }
+
+ if (!CallbackPath.HasValue)
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(CallbackPath)), nameof(CallbackPath));
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the provider-assigned client id.
+ /// </summary>
+ public string ClientId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the provider-assigned client secret.
+ /// </summary>
+ public string ClientSecret { get; set; }
+
+ /// <summary>
+ /// Gets or sets the URI where the client will be redirected to authenticate.
+ /// </summary>
+ public string AuthorizationEndpoint { get; set; }
+
+ /// <summary>
+ /// Gets or sets the URI the middleware will access to exchange the OAuth token.
+ /// </summary>
+ public string TokenEndpoint { get; set; }
+
+ /// <summary>
+ /// Gets or sets the URI the middleware will access to obtain the user information.
+ /// This value is not used in the default implementation, it is for use in custom implementations of
+ /// IOAuthAuthenticationEvents.Authenticated or OAuthAuthenticationHandler.CreateTicketAsync.
+ /// </summary>
+ public string UserInformationEndpoint { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="OAuthEvents"/> used to handle authentication events.
+ /// </summary>
+ public new OAuthEvents Events
+ {
+ get { return (OAuthEvents)base.Events; }
+ set { base.Events = value; }
+ }
+
+ /// <summary>
+ /// A collection of claim actions used to select values from the json user data and create Claims.
+ /// </summary>
+ public ClaimActionCollection ClaimActions { get; } = new ClaimActionCollection();
+
+ /// <summary>
+ /// Gets the list of permissions to request.
+ /// </summary>
+ public ICollection<string> Scope { get; } = new HashSet<string>();
+
+ /// <summary>
+ /// Gets or sets the type used to secure data handled by the middleware.
+ /// </summary>
+ public ISecureDataFormat<AuthenticationProperties> StateDataFormat { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthPostConfigureOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthPostConfigureOptions.cs
new file mode 100644
index 0000000000..e97346413c
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthPostConfigureOptions.cs
@@ -0,0 +1,45 @@
+// 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.Net.Http;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ /// <summary>
+ /// Used to setup defaults for the OAuthOptions.
+ /// </summary>
+ public class OAuthPostConfigureOptions<TOptions, THandler> : IPostConfigureOptions<TOptions>
+ where TOptions : OAuthOptions, new()
+ where THandler : OAuthHandler<TOptions>
+ {
+ private readonly IDataProtectionProvider _dp;
+
+ public OAuthPostConfigureOptions(IDataProtectionProvider dataProtection)
+ {
+ _dp = dataProtection;
+ }
+
+ public void PostConfigure(string name, TOptions options)
+ {
+ options.DataProtectionProvider = options.DataProtectionProvider ?? _dp;
+ if (options.Backchannel == null)
+ {
+ options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler());
+ options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OAuth handler");
+ options.Backchannel.Timeout = options.BackchannelTimeout;
+ options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
+ }
+
+ if (options.StateDataFormat == null)
+ {
+ var dataProtector = options.DataProtectionProvider.CreateProtector(
+ typeof(THandler).FullName, name, "v1");
+ options.StateDataFormat = new PropertiesDataFormat(dataProtector);
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthTokenResponse.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthTokenResponse.cs
new file mode 100644
index 0000000000..aa4026b009
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthTokenResponse.cs
@@ -0,0 +1,42 @@
+// 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 Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth
+{
+ public class OAuthTokenResponse
+ {
+ private OAuthTokenResponse(JObject response)
+ {
+ Response = response;
+ AccessToken = response.Value<string>("access_token");
+ TokenType = response.Value<string>("token_type");
+ RefreshToken = response.Value<string>("refresh_token");
+ ExpiresIn = response.Value<string>("expires_in");
+ }
+
+ private OAuthTokenResponse(Exception error)
+ {
+ Error = error;
+ }
+
+ public static OAuthTokenResponse Success(JObject response)
+ {
+ return new OAuthTokenResponse(response);
+ }
+
+ public static OAuthTokenResponse Failed(Exception error)
+ {
+ return new OAuthTokenResponse(error);
+ }
+
+ public JObject Response { get; set; }
+ public string AccessToken { get; set; }
+ public string TokenType { get; set; }
+ public string RefreshToken { get; set; }
+ public string ExpiresIn { get; set; }
+ public Exception Error { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..5a38ade0b9
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/Resources.Designer.cs
@@ -0,0 +1,58 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Authentication.OAuth
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Authentication.OAuth.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string Exception_OptionMustBeProvided
+ {
+ get => GetString("Exception_OptionMustBeProvided");
+ }
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string FormatException_OptionMustBeProvided(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0);
+
+ /// <summary>
+ /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.
+ /// </summary>
+ internal static string Exception_ValidatorHandlerMismatch
+ {
+ get => GetString("Exception_ValidatorHandlerMismatch");
+ }
+
+ /// <summary>
+ /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.
+ /// </summary>
+ internal static string FormatException_ValidatorHandlerMismatch()
+ => GetString("Exception_ValidatorHandlerMismatch");
+
+ 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/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Resources.resx
new file mode 100644
index 0000000000..2a19bea96a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/Resources.resx
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Exception_OptionMustBeProvided" xml:space="preserve">
+ <value>The '{0}' option must be provided.</value>
+ </data>
+ <data name="Exception_ValidatorHandlerMismatch" xml:space="preserve">
+ <value>An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json
new file mode 100644
index 0000000000..9c23947049
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json
@@ -0,0 +1,1810 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.OAuth, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.OAuthExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddOAuth",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddOAuth",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddOAuth<T0, T1>",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<T0>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions"
+ ]
+ },
+ {
+ "ParameterName": "THandler",
+ "ParameterPosition": 1,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler<T0>"
+ ]
+ }
+ ]
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddOAuth<T0, T1>",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<T0>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions"
+ ]
+ },
+ {
+ "ParameterName": "THandler",
+ "ParameterPosition": 1,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler<T0>"
+ ]
+ }
+ ]
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.OAuthPostConfigureOptions<T0, T1>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.Extensions.Options.IPostConfigureOptions<T0>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "PostConfigure",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "options",
+ "Type": "T0"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions<T0>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "dataProtection",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions"
+ ]
+ },
+ {
+ "ParameterName": "THandler",
+ "ParameterPosition": 1,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler<T0>"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.OAuthAppBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseOAuthAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseOAuthAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.ClaimActionCollectionMapExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "MapJsonKey",
+ "Parameters": [
+ {
+ "Name": "collection",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection"
+ },
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "jsonKey",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapJsonKey",
+ "Parameters": [
+ {
+ "Name": "collection",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection"
+ },
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "jsonKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "valueType",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapJsonSubKey",
+ "Parameters": [
+ {
+ "Name": "collection",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection"
+ },
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "jsonKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "subKey",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapJsonSubKey",
+ "Parameters": [
+ {
+ "Name": "collection",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection"
+ },
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "jsonKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "subKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "valueType",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapCustomJson",
+ "Parameters": [
+ {
+ "Name": "collection",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection"
+ },
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "resolver",
+ "Type": "System.Func<Newtonsoft.Json.Linq.JObject, System.String>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapCustomJson",
+ "Parameters": [
+ {
+ "Name": "collection",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection"
+ },
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "valueType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "resolver",
+ "Type": "System.Func<Newtonsoft.Json.Linq.JObject, System.String>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapAll",
+ "Parameters": [
+ {
+ "Name": "collection",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapAllExcept",
+ "Parameters": [
+ {
+ "Name": "collection",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection"
+ },
+ {
+ "Name": "exclusions",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "DeleteClaim",
+ "Parameters": [
+ {
+ "Name": "collection",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection"
+ },
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "DeleteClaims",
+ "Parameters": [
+ {
+ "Name": "collection",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection"
+ },
+ {
+ "Name": "claimTypes",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthCreatingTicketContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext<Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_User",
+ "Parameters": [],
+ "ReturnType": "Newtonsoft.Json.Linq.JObject",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TokenResponse",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AccessToken",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TokenType",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RefreshToken",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ExpiresIn",
+ "Parameters": [],
+ "ReturnType": "System.Nullable<System.TimeSpan>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Backchannel",
+ "Parameters": [],
+ "ReturnType": "System.Net.Http.HttpClient",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Identity",
+ "Parameters": [],
+ "ReturnType": "System.Security.Claims.ClaimsIdentity",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RunClaimActions",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RunClaimActions",
+ "Parameters": [
+ {
+ "Name": "userData",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "principal",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions"
+ },
+ {
+ "Name": "backchannel",
+ "Type": "System.Net.Http.HttpClient"
+ },
+ {
+ "Name": "tokens",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "principal",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions"
+ },
+ {
+ "Name": "backchannel",
+ "Type": "System.Net.Http.HttpClient"
+ },
+ {
+ "Name": "tokens",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse"
+ },
+ {
+ "Name": "user",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_OnCreatingTicket",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.OAuth.OAuthCreatingTicketContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnCreatingTicket",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.OAuth.OAuthCreatingTicketContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnRedirectToAuthorizationEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions>, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnRedirectToAuthorizationEndpoint",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions>, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreatingTicket",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthCreatingTicketContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RedirectToAuthorizationEndpoint",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions>"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthChallengeProperties",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Scope",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.ICollection<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Scope",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Collections.Generic.ICollection<System.String>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SetScope",
+ "Parameters": [
+ {
+ "Name": "scopes",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "items",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.String>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "items",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.String>"
+ },
+ {
+ "Name": "parameters",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "ScopeKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthDefaults",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "DisplayName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler<T0>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Backchannel",
+ "Parameters": [],
+ "ReturnType": "System.Net.Http.HttpClient",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateEventsAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Object>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleRemoteAuthenticateAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.HandleRequestResult>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ExchangeCodeAsync",
+ "Parameters": [
+ {
+ "Name": "code",
+ "Type": "System.String"
+ },
+ {
+ "Name": "redirectUri",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse>",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateTicketAsync",
+ "Parameters": [
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "tokens",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationTicket>",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleChallengeAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "BuildChallengeUrl",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "redirectUri",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "FormatScope",
+ "Parameters": [
+ {
+ "Name": "scopes",
+ "Type": "System.Collections.Generic.IEnumerable<System.String>"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "FormatScope",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<T0>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Validate",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ClientId",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ClientId",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ClientSecret",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ClientSecret",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AuthorizationEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AuthorizationEndpoint",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TokenEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_TokenEndpoint",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_UserInformationEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_UserInformationEndpoint",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ClaimActions",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Scope",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.ICollection<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_StateDataFormat",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationProperties>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_StateDataFormat",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationProperties>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Success",
+ "Parameters": [
+ {
+ "Name": "response",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Failed",
+ "Parameters": [
+ {
+ "Name": "error",
+ "Type": "System.Exception"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Response",
+ "Parameters": [],
+ "ReturnType": "Newtonsoft.Json.Linq.JObject",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Response",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AccessToken",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AccessToken",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TokenType",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_TokenType",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RefreshToken",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RefreshToken",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ExpiresIn",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ExpiresIn",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Error",
+ "Parameters": [],
+ "ReturnType": "System.Exception",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Error",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Exception"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ClaimType",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ValueType",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Run",
+ "Parameters": [
+ {
+ "Name": "userData",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ },
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ },
+ {
+ "Name": "issuer",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Abstract": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "valueType",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Clear",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Remove",
+ "Parameters": [
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Add",
+ "Parameters": [
+ {
+ "Name": "action",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetEnumerator",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IEnumerator<Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.CustomJsonClaimAction",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Resolver",
+ "Parameters": [],
+ "ReturnType": "System.Func<Newtonsoft.Json.Linq.JObject, System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Run",
+ "Parameters": [
+ {
+ "Name": "userData",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ },
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ },
+ {
+ "Name": "issuer",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "valueType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "resolver",
+ "Type": "System.Func<Newtonsoft.Json.Linq.JObject, System.String>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.DeleteClaimAction",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Run",
+ "Parameters": [
+ {
+ "Name": "userData",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ },
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ },
+ {
+ "Name": "issuer",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.JsonKeyClaimAction",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_JsonKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Run",
+ "Parameters": [
+ {
+ "Name": "userData",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ },
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ },
+ {
+ "Name": "issuer",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "valueType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "jsonKey",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.JsonSubKeyClaimAction",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.JsonKeyClaimAction",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_SubKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Run",
+ "Parameters": [
+ {
+ "Name": "userData",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ },
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ },
+ {
+ "Name": "issuer",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "valueType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "jsonKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "subKey",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.MapAllClaimsAction",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Run",
+ "Parameters": [
+ {
+ "Name": "userData",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ },
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ },
+ {
+ "Name": "issuer",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/ClaimActionCollectionUniqueExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/ClaimActionCollectionUniqueExtensions.cs
new file mode 100644
index 0000000000..4e349579f3
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/ClaimActionCollectionUniqueExtensions.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 System.Security.Claims;
+using Microsoft.AspNetCore.Authentication.OAuth.Claims;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect.Claims;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public static class ClaimActionCollectionUniqueExtensions
+ {
+ /// <summary>
+ /// Selects a top level value from the json user data with the given key name and adds it as a Claim.
+ /// This no-ops if the ClaimsIdentity already contains a Claim with the given ClaimType.
+ /// This no-ops if the key is not found or the value is empty.
+ /// </summary>
+ /// <param name="collection"></param>
+ /// <param name="claimType">The value to use for Claim.Type when creating a Claim.</param>
+ /// <param name="jsonKey">The top level key to look for in the json user data.</param>
+ public static void MapUniqueJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey)
+ {
+ collection.MapUniqueJsonKey(claimType, jsonKey, ClaimValueTypes.String);
+ }
+
+ /// <summary>
+ /// Selects a top level value from the json user data with the given key name and adds it as a Claim.
+ /// This no-ops if the ClaimsIdentity already contains a Claim with the given ClaimType.
+ /// This no-ops if the key is not found or the value is empty.
+ /// </summary>
+ /// <param name="collection"></param>
+ /// <param name="claimType">The value to use for Claim.Type when creating a Claim.</param>
+ /// <param name="jsonKey">The top level key to look for in the json user data.</param>
+ /// <param name="valueType">The value to use for Claim.ValueType when creating a Claim.</param>
+ public static void MapUniqueJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey, string valueType)
+ {
+ collection.Add(new UniqueJsonKeyClaimAction(claimType, valueType, jsonKey));
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/UniqueJsonKeyClaimAction.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/UniqueJsonKeyClaimAction.cs
new file mode 100644
index 0000000000..132885b3ca
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/UniqueJsonKeyClaimAction.cs
@@ -0,0 +1,61 @@
+// 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.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Authentication.OAuth.Claims;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect.Claims
+{
+ /// <summary>
+ /// A ClaimAction that selects a top level value from the json user data with the given key name and adds it as a Claim.
+ /// This no-ops if the ClaimsIdentity already contains a Claim with the given ClaimType.
+ /// This no-ops if the key is not found or the value is empty.
+ /// </summary>
+ public class UniqueJsonKeyClaimAction : JsonKeyClaimAction
+ {
+ /// <summary>
+ /// Creates a new UniqueJsonKeyClaimAction.
+ /// </summary>
+ /// <param name="claimType">The value to use for Claim.Type when creating a Claim.</param>
+ /// <param name="valueType">The value to use for Claim.ValueType when creating a Claim.</param>
+ /// <param name="jsonKey">The top level key to look for in the json user data.</param>
+ public UniqueJsonKeyClaimAction(string claimType, string valueType, string jsonKey)
+ : base(claimType, valueType, jsonKey)
+ {
+ }
+
+ /// <inheritdoc />
+ public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
+ {
+ var value = userData?.Value<string>(JsonKey);
+ if (string.IsNullOrEmpty(value))
+ {
+ // Not found
+ return;
+ }
+
+ var claim = identity.FindFirst(c => string.Equals(c.Type, JsonKey, System.StringComparison.OrdinalIgnoreCase));
+ if (claim != null && string.Equals(claim.Value, value, System.StringComparison.Ordinal))
+ {
+ // Duplicate
+ return;
+ }
+
+ claim = identity.FindFirst(c =>
+ {
+ // If this claimType is mapped by the JwtSeurityTokenHandler, then this property will be set
+ return c.Properties.TryGetValue(JwtSecurityTokenHandler.ShortClaimTypeProperty, out var shortType)
+ && string.Equals(shortType, JsonKey, System.StringComparison.OrdinalIgnoreCase);
+ });
+ if (claim != null && string.Equals(claim.Value, value, System.StringComparison.Ordinal))
+ {
+ // Duplicate with an alternate name.
+ return;
+ }
+
+ identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer));
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs
new file mode 100644
index 0000000000..203da93c53
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs
@@ -0,0 +1,20 @@
+// 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;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ public class AuthenticationFailedContext : RemoteAuthenticationContext<OpenIdConnectOptions>
+ {
+ public AuthenticationFailedContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options)
+ : base(context, scheme, options, new AuthenticationProperties())
+ { }
+
+ public OpenIdConnectMessage ProtocolMessage { get; set; }
+
+ public Exception Exception { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs
new file mode 100644
index 0000000000..bdf6e4a7ff
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs
@@ -0,0 +1,93 @@
+// 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.IdentityModel.Tokens.Jwt;
+using System.Net.Http;
+using Microsoft.AspNetCore.Http;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ /// <summary>
+ /// This Context can be used to be informed when an 'AuthorizationCode' is received over the OpenIdConnect protocol.
+ /// </summary>
+ public class AuthorizationCodeReceivedContext : RemoteAuthenticationContext<OpenIdConnectOptions>
+ {
+ /// <summary>
+ /// Creates a <see cref="AuthorizationCodeReceivedContext"/>
+ /// </summary>
+ public AuthorizationCodeReceivedContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ OpenIdConnectOptions options,
+ AuthenticationProperties properties)
+ : base(context, scheme, options, properties) { }
+
+ public OpenIdConnectMessage ProtocolMessage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="JwtSecurityToken"/> that was received in the authentication response, if any.
+ /// </summary>
+ public JwtSecurityToken JwtSecurityToken { get; set; }
+
+ /// <summary>
+ /// The request that will be sent to the token endpoint and is available for customization.
+ /// </summary>
+ public OpenIdConnectMessage TokenEndpointRequest { get; set; }
+
+ /// <summary>
+ /// The configured communication channel to the identity provider for use when making custom requests to the token endpoint.
+ /// </summary>
+ public HttpClient Backchannel { get; internal set; }
+
+ /// <summary>
+ /// If the developer chooses to redeem the code themselves then they can provide the resulting tokens here. This is the
+ /// same as calling HandleCodeRedemption. If set then the handler will not attempt to redeem the code. An IdToken
+ /// is required if one had not been previously received in the authorization response. An access token is optional
+ /// if the handler is to contact the user-info endpoint.
+ /// </summary>
+ public OpenIdConnectMessage TokenEndpointResponse { get; set; }
+
+ /// <summary>
+ /// Indicates if the developer choose to handle (or skip) the code redemption. If true then the handler will not attempt
+ /// to redeem the code. See HandleCodeRedemption and TokenEndpointResponse.
+ /// </summary>
+ public bool HandledCodeRedemption => TokenEndpointResponse != null;
+
+ /// <summary>
+ /// Tells the handler to skip the code redemption process. The developer may have redeemed the code themselves, or
+ /// decided that the redemption was not required. If tokens were retrieved that are needed for further processing then
+ /// call one of the overloads that allows providing tokens. An IdToken is required if one had not been previously received
+ /// in the authorization response. An access token can optionally be provided for the handler to contact the
+ /// user-info endpoint. Calling this is the same as setting TokenEndpointResponse.
+ /// </summary>
+ public void HandleCodeRedemption()
+ {
+ TokenEndpointResponse = new OpenIdConnectMessage();
+ }
+
+ /// <summary>
+ /// Tells the handler to skip the code redemption process. The developer may have redeemed the code themselves, or
+ /// decided that the redemption was not required. If tokens were retrieved that are needed for further processing then
+ /// call one of the overloads that allows providing tokens. An IdToken is required if one had not been previously received
+ /// in the authorization response. An access token can optionally be provided for the handler to contact the
+ /// user-info endpoint. Calling this is the same as setting TokenEndpointResponse.
+ /// </summary>
+ public void HandleCodeRedemption(string accessToken, string idToken)
+ {
+ TokenEndpointResponse = new OpenIdConnectMessage() { AccessToken = accessToken, IdToken = idToken };
+ }
+
+ /// <summary>
+ /// Tells the handler to skip the code redemption process. The developer may have redeemed the code themselves, or
+ /// decided that the redemption was not required. If tokens were retrieved that are needed for further processing then
+ /// call one of the overloads that allows providing tokens. An IdToken is required if one had not been previously received
+ /// in the authorization response. An access token can optionally be provided for the handler to contact the
+ /// user-info endpoint. Calling this is the same as setting TokenEndpointResponse.
+ /// </summary>
+ public void HandleCodeRedemption(OpenIdConnectMessage tokenEndpointResponse)
+ {
+ TokenEndpointResponse = tokenEndpointResponse;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs
new file mode 100644
index 0000000000..7d06e44799
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/MessageReceivedContext.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 Microsoft.AspNetCore.Http;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ public class MessageReceivedContext : RemoteAuthenticationContext<OpenIdConnectOptions>
+ {
+ public MessageReceivedContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ OpenIdConnectOptions options,
+ AuthenticationProperties properties)
+ : base(context, scheme, options, properties) { }
+
+ public OpenIdConnectMessage ProtocolMessage { get; set; }
+
+ /// <summary>
+ /// Bearer Token. This will give the application an opportunity to retrieve a token from an alternative location.
+ /// </summary>
+ public string Token { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs
new file mode 100644
index 0000000000..2a48d250bb
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs
@@ -0,0 +1,86 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ /// <summary>
+ /// Specifies events which the <see cref="OpenIdConnectHandler" />invokes to enable developer control over the authentication process.
+ /// </summary>
+ public class OpenIdConnectEvents : RemoteAuthenticationEvents
+ {
+ /// <summary>
+ /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
+ /// </summary>
+ public Func<AuthenticationFailedContext, Task> OnAuthenticationFailed { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked after security token validation if an authorization code is present in the protocol message.
+ /// </summary>
+ public Func<AuthorizationCodeReceivedContext, Task> OnAuthorizationCodeReceived { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked when a protocol message is first received.
+ /// </summary>
+ public Func<MessageReceivedContext, Task> OnMessageReceived { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked before redirecting to the identity provider to authenticate. This can be used to set ProtocolMessage.State
+ /// that will be persisted through the authentication process. The ProtocolMessage can also be used to add or customize
+ /// parameters sent to the identity provider.
+ /// </summary>
+ public Func<RedirectContext, Task> OnRedirectToIdentityProvider { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked before redirecting to the identity provider to sign out.
+ /// </summary>
+ public Func<RedirectContext, Task> OnRedirectToIdentityProviderForSignOut { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked before redirecting to the <see cref="OpenIdConnectOptions.SignedOutRedirectUri"/> at the end of a remote sign-out flow.
+ /// </summary>
+ public Func<RemoteSignOutContext, Task> OnSignedOutCallbackRedirect { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked when a request is received on the RemoteSignOutPath.
+ /// </summary>
+ public Func<RemoteSignOutContext, Task> OnRemoteSignOut { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked after "authorization code" is redeemed for tokens at the token endpoint.
+ /// </summary>
+ public Func<TokenResponseReceivedContext, Task> OnTokenResponseReceived { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked when an IdToken has been validated and produced an AuthenticationTicket.
+ /// </summary>
+ public Func<TokenValidatedContext, Task> OnTokenValidated { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked when user information is retrieved from the UserInfoEndpoint.
+ /// </summary>
+ public Func<UserInformationReceivedContext, Task> OnUserInformationReceived { get; set; } = context => Task.CompletedTask;
+
+ public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context);
+
+ public virtual Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context) => OnAuthorizationCodeReceived(context);
+
+ public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context);
+
+ public virtual Task RedirectToIdentityProvider(RedirectContext context) => OnRedirectToIdentityProvider(context);
+
+ public virtual Task RedirectToIdentityProviderForSignOut(RedirectContext context) => OnRedirectToIdentityProviderForSignOut(context);
+
+ public virtual Task SignedOutCallbackRedirect(RemoteSignOutContext context) => OnSignedOutCallbackRedirect(context);
+
+ public virtual Task RemoteSignOut(RemoteSignOutContext context) => OnRemoteSignOut(context);
+
+ public virtual Task TokenResponseReceived(TokenResponseReceivedContext context) => OnTokenResponseReceived(context);
+
+ public virtual Task TokenValidated(TokenValidatedContext context) => OnTokenValidated(context);
+
+ public virtual Task UserInformationReceived(UserInformationReceivedContext context) => OnUserInformationReceived(context);
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RedirectContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RedirectContext.cs
new file mode 100644
index 0000000000..9961c237d4
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RedirectContext.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 Microsoft.AspNetCore.Http;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ /// <summary>
+ /// When a user configures the <see cref="OpenIdConnectHandler"/> to be notified prior to redirecting to an IdentityProvider
+ /// an instance of <see cref="RedirectContext"/> is passed to the 'RedirectToAuthenticationEndpoint' or 'RedirectToEndSessionEndpoint' events.
+ /// </summary>
+ public class RedirectContext : PropertiesContext<OpenIdConnectOptions>
+ {
+ public RedirectContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ OpenIdConnectOptions options,
+ AuthenticationProperties properties)
+ : base(context, scheme, options, properties) { }
+
+ public OpenIdConnectMessage ProtocolMessage { get; set; }
+
+ /// <summary>
+ /// If true, will skip any default logic for this redirect.
+ /// </summary>
+ public bool Handled { get; private set; }
+
+ /// <summary>
+ /// Skips any default logic for this redirect.
+ /// </summary>
+ public void HandleResponse() => Handled = true;
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs
new file mode 100644
index 0000000000..26720a58f8
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs
@@ -0,0 +1,17 @@
+// 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;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ public class RemoteSignOutContext : RemoteAuthenticationContext<OpenIdConnectOptions>
+ {
+ public RemoteSignOutContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, OpenIdConnectMessage message)
+ : base(context, scheme, options, new AuthenticationProperties())
+ => ProtocolMessage = message;
+
+ public OpenIdConnectMessage ProtocolMessage { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs
new file mode 100644
index 0000000000..2bebdb8dc5
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs
@@ -0,0 +1,29 @@
+// 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.Claims;
+using Microsoft.AspNetCore.Http;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ /// <summary>
+ /// This Context can be used to be informed when an 'AuthorizationCode' is redeemed for tokens at the token endpoint.
+ /// </summary>
+ public class TokenResponseReceivedContext : RemoteAuthenticationContext<OpenIdConnectOptions>
+ {
+ /// <summary>
+ /// Creates a <see cref="TokenResponseReceivedContext"/>
+ /// </summary>
+ public TokenResponseReceivedContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, ClaimsPrincipal user, AuthenticationProperties properties)
+ : base(context, scheme, options, properties)
+ => Principal = user;
+
+ public OpenIdConnectMessage ProtocolMessage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="OpenIdConnectMessage"/> that contains the tokens received after redeeming the code at the token endpoint.
+ /// </summary>
+ public OpenIdConnectMessage TokenEndpointResponse { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenValidatedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenValidatedContext.cs
new file mode 100644
index 0000000000..853857dc7b
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenValidatedContext.cs
@@ -0,0 +1,28 @@
+// 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.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Http;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ public class TokenValidatedContext : RemoteAuthenticationContext<OpenIdConnectOptions>
+ {
+ /// <summary>
+ /// Creates a <see cref="TokenValidatedContext"/>
+ /// </summary>
+ public TokenValidatedContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, ClaimsPrincipal principal, AuthenticationProperties properties)
+ : base(context, scheme, options, properties)
+ => Principal = principal;
+
+ public OpenIdConnectMessage ProtocolMessage { get; set; }
+
+ public JwtSecurityToken SecurityToken { get; set; }
+
+ public OpenIdConnectMessage TokenEndpointResponse { get; set; }
+
+ public string Nonce { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs
new file mode 100644
index 0000000000..0b855eaf39
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.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.Security.Claims;
+using Microsoft.AspNetCore.Http;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ public class UserInformationReceivedContext : RemoteAuthenticationContext<OpenIdConnectOptions>
+ {
+ public UserInformationReceivedContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, ClaimsPrincipal principal, AuthenticationProperties properties)
+ : base(context, scheme, options, properties)
+ => Principal = principal;
+
+ public OpenIdConnectMessage ProtocolMessage { get; set; }
+
+ public JObject User { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs
new file mode 100644
index 0000000000..224af87b6f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs
@@ -0,0 +1,508 @@
+// 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.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static Action<ILogger, Exception> _redirectToIdentityProviderForSignOutHandledResponse;
+ private static Action<ILogger, Exception> _redirectToIdentityProviderHandledResponse;
+ private static Action<ILogger, Exception> _signoutCallbackRedirectHandledResponse;
+ private static Action<ILogger, Exception> _signoutCallbackRedirectSkipped;
+ private static Action<ILogger, Exception> _updatingConfiguration;
+ private static Action<ILogger, Exception> _receivedIdToken;
+ private static Action<ILogger, Exception> _redeemingCodeForTokens;
+ private static Action<ILogger, string, Exception> _enteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync;
+ private static Action<ILogger, string, Exception> _enteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync;
+ private static Action<ILogger, string, Exception> _enteringOpenIdAuthenticationHandlerHandleSignOutAsync;
+ private static Action<ILogger, string, Exception> _messageReceived;
+ private static Action<ILogger, Exception> _messageReceivedContextHandledResponse;
+ private static Action<ILogger, Exception> _messageReceivedContextSkipped;
+ private static Action<ILogger, Exception> _authorizationCodeReceived;
+ private static Action<ILogger, Exception> _configurationManagerRequestRefreshCalled;
+ private static Action<ILogger, Exception> _tokenResponseReceived;
+ private static Action<ILogger, Exception> _tokenValidatedHandledResponse;
+ private static Action<ILogger, Exception> _tokenValidatedSkipped;
+ private static Action<ILogger, Exception> _authenticationFailedContextHandledResponse;
+ private static Action<ILogger, Exception> _authenticationFailedContextSkipped;
+ private static Action<ILogger, Exception> _authorizationCodeReceivedContextHandledResponse;
+ private static Action<ILogger, Exception> _authorizationCodeReceivedContextSkipped;
+ private static Action<ILogger, Exception> _tokenResponseReceivedHandledResponse;
+ private static Action<ILogger, Exception> _tokenResponseReceivedSkipped;
+ private static Action<ILogger, string, Exception> _userInformationReceived;
+ private static Action<ILogger, Exception> _userInformationReceivedHandledResponse;
+ private static Action<ILogger, Exception> _userInformationReceivedSkipped;
+ private static Action<ILogger, string, Exception> _invalidLogoutQueryStringRedirectUrl;
+ private static Action<ILogger, Exception> _nullOrEmptyAuthorizationResponseState;
+ private static Action<ILogger, Exception> _unableToReadAuthorizationResponseState;
+ private static Action<ILogger, string, string, string, Exception> _responseError;
+ private static Action<ILogger, string, string, string, int, Exception> _responseErrorWithStatusCode;
+ private static Action<ILogger, Exception> _exceptionProcessingMessage;
+ private static Action<ILogger, Exception> _accessTokenNotAvailable;
+ private static Action<ILogger, Exception> _retrievingClaims;
+ private static Action<ILogger, Exception> _userInfoEndpointNotSet;
+ private static Action<ILogger, Exception> _unableToProtectNonceCookie;
+ private static Action<ILogger, string, Exception> _invalidAuthenticationRequestUrl;
+ private static Action<ILogger, string, Exception> _unableToReadIdToken;
+ private static Action<ILogger, string, Exception> _invalidSecurityTokenType;
+ private static Action<ILogger, string, Exception> _unableToValidateIdToken;
+ private static Action<ILogger, string, Exception> _postAuthenticationLocalRedirect;
+ private static Action<ILogger, string, Exception> _postSignOutRedirect;
+ private static Action<ILogger, Exception> _remoteSignOutHandledResponse;
+ private static Action<ILogger, Exception> _remoteSignOutSkipped;
+ private static Action<ILogger, Exception> _remoteSignOut;
+ private static Action<ILogger, Exception> _remoteSignOutSessionIdMissing;
+ private static Action<ILogger, Exception> _remoteSignOutSessionIdInvalid;
+ private static Action<ILogger, string, Exception> _signOut;
+
+ static LoggingExtensions()
+ {
+ // Final
+ _redirectToIdentityProviderForSignOutHandledResponse = LoggerMessage.Define(
+ eventId: 1,
+ logLevel: LogLevel.Debug,
+ formatString: "RedirectToIdentityProviderForSignOut.HandledResponse");
+ _invalidLogoutQueryStringRedirectUrl = LoggerMessage.Define<string>(
+ eventId: 3,
+ logLevel: LogLevel.Warning,
+ formatString: "The query string for Logout is not a well-formed URI. Redirect URI: '{RedirectUrl}'.");
+ _enteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync = LoggerMessage.Define<string>(
+ eventId: 4,
+ logLevel: LogLevel.Trace,
+ formatString: "Entering {OpenIdConnectHandlerType}'s HandleUnauthorizedAsync.");
+ _enteringOpenIdAuthenticationHandlerHandleSignOutAsync = LoggerMessage.Define<string>(
+ eventId: 14,
+ logLevel: LogLevel.Trace,
+ formatString: "Entering {OpenIdConnectHandlerType}'s HandleSignOutAsync.");
+ _postAuthenticationLocalRedirect = LoggerMessage.Define<string>(
+ eventId: 5,
+ logLevel: LogLevel.Trace,
+ formatString: "Using properties.RedirectUri for 'local redirect' post authentication: '{RedirectUri}'.");
+ _redirectToIdentityProviderHandledResponse = LoggerMessage.Define(
+ eventId: 6,
+ logLevel: LogLevel.Debug,
+ formatString: "RedirectToIdentityProvider.HandledResponse");
+ _invalidAuthenticationRequestUrl = LoggerMessage.Define<string>(
+ eventId: 8,
+ logLevel: LogLevel.Warning,
+ formatString: "The redirect URI is not well-formed. The URI is: '{AuthenticationRequestUrl}'.");
+ _enteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync = LoggerMessage.Define<string>(
+ eventId: 9,
+ logLevel: LogLevel.Trace,
+ formatString: "Entering {OpenIdConnectHandlerType}'s HandleRemoteAuthenticateAsync.");
+ _nullOrEmptyAuthorizationResponseState = LoggerMessage.Define(
+ eventId: 10,
+ logLevel: LogLevel.Debug,
+ formatString: "message.State is null or empty.");
+ _unableToReadAuthorizationResponseState = LoggerMessage.Define(
+ eventId: 11,
+ logLevel: LogLevel.Debug,
+ formatString: "Unable to read the message.State.");
+ _responseError = LoggerMessage.Define<string, string, string>(
+ eventId: 12,
+ logLevel: LogLevel.Error,
+ formatString: "Message contains error: '{Error}', error_description: '{ErrorDescription}', error_uri: '{ErrorUri}'.");
+ _responseErrorWithStatusCode = LoggerMessage.Define<string, string, string, int>(
+ eventId: 49,
+ logLevel: LogLevel.Error,
+ formatString: "Message contains error: '{Error}', error_description: '{ErrorDescription}', error_uri: '{ErrorUri}', status code '{StatusCode}'.");
+ _updatingConfiguration = LoggerMessage.Define(
+ eventId: 13,
+ logLevel: LogLevel.Debug,
+ formatString: "Updating configuration");
+ _tokenValidatedHandledResponse = LoggerMessage.Define(
+ eventId: 15,
+ logLevel: LogLevel.Debug,
+ formatString: "TokenValidated.HandledResponse");
+ _tokenValidatedSkipped = LoggerMessage.Define(
+ eventId: 16,
+ logLevel: LogLevel.Debug,
+ formatString: "TokenValidated.Skipped");
+ _exceptionProcessingMessage = LoggerMessage.Define(
+ eventId: 17,
+ logLevel: LogLevel.Error,
+ formatString: "Exception occurred while processing message.");
+ _configurationManagerRequestRefreshCalled = LoggerMessage.Define(
+ eventId: 18,
+ logLevel: LogLevel.Debug,
+ formatString: "Exception of type 'SecurityTokenSignatureKeyNotFoundException' thrown, Options.ConfigurationManager.RequestRefresh() called.");
+ _redeemingCodeForTokens = LoggerMessage.Define(
+ eventId: 19,
+ logLevel: LogLevel.Debug,
+ formatString: "Redeeming code for tokens.");
+ _retrievingClaims = LoggerMessage.Define(
+ eventId: 20,
+ logLevel: LogLevel.Trace,
+ formatString: "Retrieving claims from the user info endpoint.");
+ _receivedIdToken = LoggerMessage.Define(
+ eventId: 21,
+ logLevel: LogLevel.Debug,
+ formatString: "Received 'id_token'");
+ _userInfoEndpointNotSet = LoggerMessage.Define(
+ eventId: 22,
+ logLevel: LogLevel.Debug,
+ formatString: "UserInfoEndpoint is not set. Claims cannot be retrieved.");
+ _unableToProtectNonceCookie = LoggerMessage.Define(
+ eventId: 23,
+ logLevel: LogLevel.Warning,
+ formatString: "Failed to un-protect the nonce cookie.");
+ _messageReceived = LoggerMessage.Define<string>(
+ eventId: 24,
+ logLevel: LogLevel.Trace,
+ formatString: "MessageReceived: '{RedirectUrl}'.");
+ _messageReceivedContextHandledResponse = LoggerMessage.Define(
+ eventId: 25,
+ logLevel: LogLevel.Debug,
+ formatString: "MessageReceivedContext.HandledResponse");
+ _messageReceivedContextSkipped = LoggerMessage.Define(
+ eventId: 26,
+ logLevel: LogLevel.Debug,
+ formatString: "MessageReceivedContext.Skipped");
+ _authorizationCodeReceived = LoggerMessage.Define(
+ eventId: 27,
+ logLevel: LogLevel.Trace,
+ formatString: "Authorization code received.");
+ _authorizationCodeReceivedContextHandledResponse = LoggerMessage.Define(
+ eventId: 28,
+ logLevel: LogLevel.Debug,
+ formatString: "AuthorizationCodeReceivedContext.HandledResponse");
+ _authorizationCodeReceivedContextSkipped = LoggerMessage.Define(
+ eventId: 29,
+ logLevel: LogLevel.Debug,
+ formatString: "AuthorizationCodeReceivedContext.Skipped");
+ _tokenResponseReceived = LoggerMessage.Define(
+ eventId: 30,
+ logLevel: LogLevel.Trace,
+ formatString: "Token response received.");
+ _tokenResponseReceivedHandledResponse = LoggerMessage.Define(
+ eventId: 31,
+ logLevel: LogLevel.Debug,
+ formatString: "TokenResponseReceived.HandledResponse");
+ _tokenResponseReceivedSkipped = LoggerMessage.Define(
+ eventId: 32,
+ logLevel: LogLevel.Debug,
+ formatString: "TokenResponseReceived.Skipped");
+ _postSignOutRedirect = LoggerMessage.Define<string>(
+ eventId: 33,
+ logLevel: LogLevel.Trace,
+ formatString: "Using properties.RedirectUri for redirect post authentication: '{RedirectUri}'.");
+ _userInformationReceived = LoggerMessage.Define<string>(
+ eventId: 35,
+ logLevel: LogLevel.Trace,
+ formatString: "User information received: {User}");
+ _userInformationReceivedHandledResponse = LoggerMessage.Define(
+ eventId: 36,
+ logLevel: LogLevel.Debug,
+ formatString: "The UserInformationReceived event returned Handled.");
+ _userInformationReceivedSkipped = LoggerMessage.Define(
+ eventId: 37,
+ logLevel: LogLevel.Debug,
+ formatString: "The UserInformationReceived event returned Skipped.");
+ _authenticationFailedContextHandledResponse = LoggerMessage.Define(
+ eventId: 38,
+ logLevel: LogLevel.Debug,
+ formatString: "AuthenticationFailedContext.HandledResponse");
+ _authenticationFailedContextSkipped = LoggerMessage.Define(
+ eventId: 39,
+ logLevel: LogLevel.Debug,
+ formatString: "AuthenticationFailedContext.Skipped");
+ _invalidSecurityTokenType = LoggerMessage.Define<string>(
+ eventId: 40,
+ logLevel: LogLevel.Error,
+ formatString: "The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{SecurityTokenType}'");
+ _unableToValidateIdToken = LoggerMessage.Define<string>(
+ eventId: 41,
+ logLevel: LogLevel.Error,
+ formatString: "Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{IdToken}'.");
+ _accessTokenNotAvailable = LoggerMessage.Define(
+ eventId: 42,
+ logLevel: LogLevel.Debug,
+ formatString: "The access_token is not available. Claims cannot be retrieved.");
+ _unableToReadIdToken = LoggerMessage.Define<string>(
+ eventId: 43,
+ logLevel: LogLevel.Error,
+ formatString: "Unable to read the 'id_token', no suitable ISecurityTokenValidator was found for: '{IdToken}'.");
+ _remoteSignOutHandledResponse = LoggerMessage.Define(
+ eventId: 44,
+ logLevel: LogLevel.Debug,
+ formatString: "RemoteSignOutContext.HandledResponse");
+ _remoteSignOutSkipped = LoggerMessage.Define(
+ eventId: 45,
+ logLevel: LogLevel.Debug,
+ formatString: "RemoteSignOutContext.Skipped");
+ _remoteSignOut = LoggerMessage.Define(
+ eventId: 46,
+ logLevel: LogLevel.Information,
+ formatString: "Remote signout request processed.");
+ _remoteSignOutSessionIdMissing = LoggerMessage.Define(
+ eventId: 47,
+ logLevel: LogLevel.Error,
+ formatString: "The remote signout request was ignored because the 'sid' parameter " +
+ "was missing, which may indicate an unsolicited logout.");
+ _remoteSignOutSessionIdInvalid = LoggerMessage.Define(
+ eventId: 48,
+ logLevel: LogLevel.Error,
+ formatString: "The remote signout request was ignored because the 'sid' parameter didn't match " +
+ "the expected value, which may indicate an unsolicited logout.");
+ _signOut = LoggerMessage.Define<string>(
+ eventId: 49,
+ logLevel: LogLevel.Information,
+ formatString: "AuthenticationScheme: {AuthenticationScheme} signed out.");
+ _signoutCallbackRedirectHandledResponse = LoggerMessage.Define(
+ eventId: 50,
+ logLevel: LogLevel.Debug,
+ formatString: "RedirectToSignedOutRedirectUri.HandledResponse");
+ _signoutCallbackRedirectSkipped = LoggerMessage.Define(
+ eventId: 51,
+ logLevel: LogLevel.Debug,
+ formatString: "RedirectToSignedOutRedirectUri.Skipped");
+ }
+
+ public static void UpdatingConfiguration(this ILogger logger)
+ {
+ _updatingConfiguration(logger, null);
+ }
+
+ public static void ConfigurationManagerRequestRefreshCalled(this ILogger logger)
+ {
+ _configurationManagerRequestRefreshCalled(logger, null);
+ }
+
+ public static void AuthorizationCodeReceived(this ILogger logger)
+ {
+ _authorizationCodeReceived(logger, null);
+ }
+
+ public static void TokenResponseReceived(this ILogger logger)
+ {
+ _tokenResponseReceived(logger, null);
+ }
+
+ public static void ReceivedIdToken(this ILogger logger)
+ {
+ _receivedIdToken(logger, null);
+ }
+
+ public static void RedeemingCodeForTokens(this ILogger logger)
+ {
+ _redeemingCodeForTokens(logger, null);
+ }
+
+ public static void TokenValidatedHandledResponse(this ILogger logger)
+ {
+ _tokenValidatedHandledResponse(logger, null);
+ }
+
+ public static void TokenValidatedSkipped(this ILogger logger)
+ {
+ _tokenValidatedSkipped(logger, null);
+ }
+
+ public static void AuthorizationCodeReceivedContextHandledResponse(this ILogger logger)
+ {
+ _authorizationCodeReceivedContextHandledResponse(logger, null);
+ }
+
+ public static void AuthorizationCodeReceivedContextSkipped(this ILogger logger)
+ {
+ _authorizationCodeReceivedContextSkipped(logger, null);
+ }
+
+ public static void TokenResponseReceivedHandledResponse(this ILogger logger)
+ {
+ _tokenResponseReceivedHandledResponse(logger, null);
+ }
+
+ public static void TokenResponseReceivedSkipped(this ILogger logger)
+ {
+ _tokenResponseReceivedSkipped(logger, null);
+ }
+
+ public static void AuthenticationFailedContextHandledResponse(this ILogger logger)
+ {
+ _authenticationFailedContextHandledResponse(logger, null);
+ }
+
+ public static void AuthenticationFailedContextSkipped(this ILogger logger)
+ {
+ _authenticationFailedContextSkipped(logger, null);
+ }
+
+ public static void MessageReceived(this ILogger logger, string redirectUrl)
+ {
+ _messageReceived(logger, redirectUrl, null);
+ }
+
+ public static void MessageReceivedContextHandledResponse(this ILogger logger)
+ {
+ _messageReceivedContextHandledResponse(logger, null);
+ }
+
+ public static void MessageReceivedContextSkipped(this ILogger logger)
+ {
+ _messageReceivedContextSkipped(logger, null);
+ }
+
+ public static void RedirectToIdentityProviderForSignOutHandledResponse(this ILogger logger)
+ {
+ _redirectToIdentityProviderForSignOutHandledResponse(logger, null);
+ }
+
+ public static void RedirectToIdentityProviderHandledResponse(this ILogger logger)
+ {
+ _redirectToIdentityProviderHandledResponse(logger, null);
+ }
+
+ public static void SignoutCallbackRedirectHandledResponse(this ILogger logger)
+ {
+ _signoutCallbackRedirectHandledResponse(logger, null);
+ }
+
+ public static void SignoutCallbackRedirectSkipped(this ILogger logger)
+ {
+ _signoutCallbackRedirectSkipped(logger, null);
+ }
+
+ public static void UserInformationReceivedHandledResponse(this ILogger logger)
+ {
+ _userInformationReceivedHandledResponse(logger, null);
+ }
+
+ public static void UserInformationReceivedSkipped(this ILogger logger)
+ {
+ _userInformationReceivedSkipped(logger, null);
+ }
+
+ public static void InvalidLogoutQueryStringRedirectUrl(this ILogger logger, string redirectUrl)
+ {
+ _invalidLogoutQueryStringRedirectUrl(logger, redirectUrl, null);
+ }
+
+ public static void NullOrEmptyAuthorizationResponseState(this ILogger logger)
+ {
+ _nullOrEmptyAuthorizationResponseState(logger, null);
+ }
+
+ public static void UnableToReadAuthorizationResponseState(this ILogger logger)
+ {
+ _unableToReadAuthorizationResponseState(logger, null);
+ }
+
+ public static void ResponseError(this ILogger logger, string error, string errorDescription, string errorUri)
+ {
+ _responseError(logger, error, errorDescription, errorUri, null);
+ }
+
+ public static void ResponseErrorWithStatusCode(this ILogger logger, string error, string errorDescription, string errorUri, int statusCode)
+ {
+ _responseErrorWithStatusCode(logger, error, errorDescription, errorUri, statusCode, null);
+ }
+
+ public static void ExceptionProcessingMessage(this ILogger logger, Exception ex)
+ {
+ _exceptionProcessingMessage(logger, ex);
+ }
+
+ public static void AccessTokenNotAvailable(this ILogger logger)
+ {
+ _accessTokenNotAvailable(logger, null);
+ }
+
+ public static void RetrievingClaims(this ILogger logger)
+ {
+ _retrievingClaims(logger, null);
+ }
+
+ public static void UserInfoEndpointNotSet(this ILogger logger)
+ {
+ _userInfoEndpointNotSet(logger, null);
+ }
+
+ public static void UnableToProtectNonceCookie(this ILogger logger, Exception ex)
+ {
+ _unableToProtectNonceCookie(logger, ex);
+ }
+
+ public static void InvalidAuthenticationRequestUrl(this ILogger logger, string redirectUri)
+ {
+ _invalidAuthenticationRequestUrl(logger, redirectUri, null);
+ }
+
+ public static void UnableToReadIdToken(this ILogger logger, string idToken)
+ {
+ _unableToReadIdToken(logger, idToken, null);
+ }
+
+ public static void InvalidSecurityTokenType(this ILogger logger, string tokenType)
+ {
+ _invalidSecurityTokenType(logger, tokenType, null);
+ }
+
+ public static void UnableToValidateIdToken(this ILogger logger, string idToken)
+ {
+ _unableToValidateIdToken(logger, idToken, null);
+ }
+
+ public static void EnteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync(this ILogger logger, string openIdConnectHandlerTypeName)
+ {
+ _enteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync(logger, openIdConnectHandlerTypeName, null);
+ }
+
+ public static void EnteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(this ILogger logger, string openIdConnectHandlerTypeName)
+ {
+ _enteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(logger, openIdConnectHandlerTypeName, null);
+ }
+
+ public static void EnteringOpenIdAuthenticationHandlerHandleSignOutAsync(this ILogger logger, string openIdConnectHandlerTypeName)
+ {
+ _enteringOpenIdAuthenticationHandlerHandleSignOutAsync(logger, openIdConnectHandlerTypeName, null);
+ }
+
+ public static void UserInformationReceived(this ILogger logger, string user)
+ {
+ _userInformationReceived(logger, user, null);
+ }
+
+ public static void PostAuthenticationLocalRedirect(this ILogger logger, string redirectUri)
+ {
+ _postAuthenticationLocalRedirect(logger, redirectUri, null);
+ }
+
+ public static void PostSignOutRedirect(this ILogger logger, string redirectUri)
+ {
+ _postSignOutRedirect(logger, redirectUri, null);
+ }
+
+ public static void RemoteSignOutHandledResponse(this ILogger logger)
+ {
+ _remoteSignOutHandledResponse(logger, null);
+ }
+
+ public static void RemoteSignOutSkipped(this ILogger logger)
+ {
+ _remoteSignOutSkipped(logger, null);
+ }
+
+ public static void RemoteSignOut(this ILogger logger)
+ {
+ _remoteSignOut(logger, null);
+ }
+
+ public static void RemoteSignOutSessionIdMissing(this ILogger logger)
+ {
+ _remoteSignOutSessionIdMissing(logger, null);
+ }
+
+ public static void RemoteSignOutSessionIdInvalid(this ILogger logger)
+ {
+ _remoteSignOutSessionIdInvalid(logger, null);
+ }
+
+ public static void SignedOut(this ILogger logger, string authenticationScheme)
+ {
+ _signOut(logger, authenticationScheme, null);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj
new file mode 100644
index 0000000000..b7f4c1704a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core middleware that enables an application to support the OpenID Connect authentication workflow.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;authentication;security</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.Authentication.OAuth\Microsoft.AspNetCore.Authentication.OAuth.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="$(MicrosoftIdentityModelProtocolsOpenIdConnectPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectAppBuilderExtensions.cs
new file mode 100644
index 0000000000..0746ae3fdb
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectAppBuilderExtensions.cs
@@ -0,0 +1,37 @@
+// 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.Authentication.OpenIdConnect;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Extension methods to add OpenID Connect authentication capabilities to an HTTP application pipeline.
+ /// </summary>
+ public static class OpenIdConnectAppBuilderExtensions
+ {
+ /// <summary>
+ /// UseOpenIdConnectAuthentication is obsolete. Configure OpenIdConnect authentication with AddAuthentication().AddOpenIdConnect in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseOpenIdConnectAuthentication is obsolete. Configure OpenIdConnect authentication with AddAuthentication().AddOpenIdConnect in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseOpenIdConnectAuthentication(this IApplicationBuilder app)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+
+ /// <summary>
+ /// UseOpenIdConnectAuthentication is obsolete. Configure OpenIdConnect authentication with AddAuthentication().AddOpenIdConnect in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <param name="options">A <see cref="OpenIdConnectOptions"/> that specifies options for the handler.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseOpenIdConnectAuthentication is obsolete. Configure OpenIdConnect authentication with AddAuthentication().AddOpenIdConnect in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseOpenIdConnectAuthentication(this IApplicationBuilder app, OpenIdConnectOptions options)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectChallengeProperties.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectChallengeProperties.cs
new file mode 100644
index 0000000000..0ced488deb
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectChallengeProperties.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ public class OpenIdConnectChallengeProperties : OAuthChallengeProperties
+ {
+ /// <summary>
+ /// The parameter key for the "max_age" argument being used for a challenge request.
+ /// </summary>
+ public static readonly string MaxAgeKey = OpenIdConnectParameterNames.MaxAge;
+
+ /// <summary>
+ /// The parameter key for the "prompt" argument being used for a challenge request.
+ /// </summary>
+ public static readonly string PromptKey = OpenIdConnectParameterNames.Prompt;
+
+ public OpenIdConnectChallengeProperties()
+ { }
+
+ public OpenIdConnectChallengeProperties(IDictionary<string, string> items)
+ : base(items)
+ { }
+
+ public OpenIdConnectChallengeProperties(IDictionary<string, string> items, IDictionary<string, object> parameters)
+ : base(items, parameters)
+ { }
+
+ /// <summary>
+ /// The "max_age" parameter value being used for a challenge request.
+ /// </summary>
+ public TimeSpan? MaxAge
+ {
+ get => GetParameter<TimeSpan?>(MaxAgeKey);
+ set => SetParameter(MaxAgeKey, value);
+ }
+
+ /// <summary>
+ /// The "prompt" parameter value being used for a challenge request.
+ /// </summary>
+ public string Prompt
+ {
+ get => GetParameter<string>(PromptKey);
+ set => SetParameter(PromptKey, value);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectDefaults.cs
new file mode 100644
index 0000000000..f98ba87e02
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectDefaults.cs
@@ -0,0 +1,41 @@
+// 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.Authentication.OpenIdConnect
+{
+ /// <summary>
+ /// Default values related to OpenIdConnect authentication handler
+ /// </summary>
+ public static class OpenIdConnectDefaults
+ {
+ /// <summary>
+ /// Constant used to identify state in openIdConnect protocol message.
+ /// </summary>
+ public static readonly string AuthenticationPropertiesKey = "OpenIdConnect.AuthenticationProperties";
+
+ /// <summary>
+ /// The default value used for OpenIdConnectOptions.AuthenticationScheme.
+ /// </summary>
+ public const string AuthenticationScheme = "OpenIdConnect";
+
+ /// <summary>
+ /// The default value for the display name.
+ /// </summary>
+ public static readonly string DisplayName = "OpenIdConnect";
+
+ /// <summary>
+ /// The prefix used to for the nonce in the cookie.
+ /// </summary>
+ public static readonly string CookieNoncePrefix = ".AspNetCore.OpenIdConnect.Nonce.";
+
+ /// <summary>
+ /// The property for the RedirectUri that was used when asking for a 'authorizationCode'.
+ /// </summary>
+ public static readonly string RedirectUriForCodePropertiesKey = "OpenIdConnect.Code.RedirectUri";
+
+ /// <summary>
+ /// Constant used to identify userstate inside AuthenticationProperties that have been serialized in the 'state' parameter.
+ /// </summary>
+ public static readonly string UserstatePropertiesKey = "OpenIdConnect.Userstate";
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectExtensions.cs
new file mode 100644
index 0000000000..f427bebaff
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectExtensions.cs
@@ -0,0 +1,29 @@
+// 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.Authentication;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class OpenIdConnectExtensions
+ {
+ public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder)
+ => builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, _ => { });
+
+ public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, Action<OpenIdConnectOptions> configureOptions)
+ => builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, configureOptions);
+
+ public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, Action<OpenIdConnectOptions> configureOptions)
+ => builder.AddOpenIdConnect(authenticationScheme, OpenIdConnectDefaults.DisplayName, configureOptions);
+
+ public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OpenIdConnectOptions> configureOptions)
+ {
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIdConnectOptions>, OpenIdConnectPostConfigureOptions>());
+ return builder.AddRemoteScheme<OpenIdConnectOptions, OpenIdConnectHandler>(authenticationScheme, displayName, configureOptions);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs
new file mode 100644
index 0000000000..029cf541b7
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs
@@ -0,0 +1,1240 @@
+// 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.Globalization;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Security.Claims;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+using Microsoft.IdentityModel.Tokens;
+using Microsoft.Net.Http.Headers;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ /// <summary>
+ /// A per-request authentication handler for the OpenIdConnectAuthenticationMiddleware.
+ /// </summary>
+ public class OpenIdConnectHandler : RemoteAuthenticationHandler<OpenIdConnectOptions>, IAuthenticationSignOutHandler
+ {
+ private const string NonceProperty = "N";
+
+ private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT";
+
+ private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
+
+ private OpenIdConnectConfiguration _configuration;
+
+ protected HttpClient Backchannel => Options.Backchannel;
+
+ protected HtmlEncoder HtmlEncoder { get; }
+
+ public OpenIdConnectHandler(IOptionsMonitor<OpenIdConnectOptions> options, ILoggerFactory logger, HtmlEncoder htmlEncoder, UrlEncoder encoder, ISystemClock clock)
+ : base(options, logger, encoder, clock)
+ {
+ HtmlEncoder = htmlEncoder;
+ }
+
+ /// <summary>
+ /// The handler calls methods on the events which give the application control at certain points where processing is occurring.
+ /// If it is not provided a default instance is supplied which does nothing when the methods are called.
+ /// </summary>
+ protected new OpenIdConnectEvents Events
+ {
+ get { return (OpenIdConnectEvents)base.Events; }
+ set { base.Events = value; }
+ }
+
+ protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new OpenIdConnectEvents());
+
+ public override Task<bool> HandleRequestAsync()
+ {
+ if (Options.RemoteSignOutPath.HasValue && Options.RemoteSignOutPath == Request.Path)
+ {
+ return HandleRemoteSignOutAsync();
+ }
+ else if (Options.SignedOutCallbackPath.HasValue && Options.SignedOutCallbackPath == Request.Path)
+ {
+ return HandleSignOutCallbackAsync();
+ }
+
+ return base.HandleRequestAsync();
+ }
+
+ protected virtual async Task<bool> HandleRemoteSignOutAsync()
+ {
+ OpenIdConnectMessage message = null;
+
+ if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
+ {
+ message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
+ }
+
+ // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small.
+ else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)
+ && !string.IsNullOrEmpty(Request.ContentType)
+ // May have media/type; charset=utf-8, allow partial match.
+ && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)
+ && Request.Body.CanRead)
+ {
+ var form = await Request.ReadFormAsync();
+ message = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
+ }
+
+ var remoteSignOutContext = new RemoteSignOutContext(Context, Scheme, Options, message);
+ await Events.RemoteSignOut(remoteSignOutContext);
+
+ if (remoteSignOutContext.Result != null)
+ {
+ if (remoteSignOutContext.Result.Handled)
+ {
+ Logger.RemoteSignOutHandledResponse();
+ return true;
+ }
+ if (remoteSignOutContext.Result.Skipped)
+ {
+ Logger.RemoteSignOutSkipped();
+ return false;
+ }
+ if (remoteSignOutContext.Result.Failure != null)
+ {
+ throw new InvalidOperationException("An error was returned from the RemoteSignOut event.", remoteSignOutContext.Result.Failure);
+ }
+ }
+
+ if (message == null)
+ {
+ return false;
+ }
+
+ // Try to extract the session identifier from the authentication ticket persisted by the sign-in handler.
+ // If the identifier cannot be found, bypass the session identifier checks: this may indicate that the
+ // authentication cookie was already cleared, that the session identifier was lost because of a lossy
+ // external/application cookie conversion or that the identity provider doesn't support sessions.
+ var sid = (await Context.AuthenticateAsync(Options.SignOutScheme))
+ ?.Principal
+ ?.FindFirst(JwtRegisteredClaimNames.Sid)
+ ?.Value;
+ if (!string.IsNullOrEmpty(sid))
+ {
+ // Ensure a 'sid' parameter was sent by the identity provider.
+ if (string.IsNullOrEmpty(message.Sid))
+ {
+ Logger.RemoteSignOutSessionIdMissing();
+ return true;
+ }
+ // Ensure the 'sid' parameter corresponds to the 'sid' stored in the authentication ticket.
+ if (!string.Equals(sid, message.Sid, StringComparison.Ordinal))
+ {
+ Logger.RemoteSignOutSessionIdInvalid();
+ return true;
+ }
+ }
+
+ Logger.RemoteSignOut();
+
+ // We've received a remote sign-out request
+ await Context.SignOutAsync(Options.SignOutScheme);
+ return true;
+ }
+
+ /// <summary>
+ /// Redirect user to the identity provider for sign out
+ /// </summary>
+ /// <returns>A task executing the sign out procedure</returns>
+ public async virtual Task SignOutAsync(AuthenticationProperties properties)
+ {
+ var target = ResolveTarget(Options.ForwardSignOut);
+ if (target != null)
+ {
+ await Context.SignOutAsync(target, properties);
+ return;
+ }
+
+ properties = properties ?? new AuthenticationProperties();
+
+ Logger.EnteringOpenIdAuthenticationHandlerHandleSignOutAsync(GetType().FullName);
+
+ if (_configuration == null && Options.ConfigurationManager != null)
+ {
+ _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
+ }
+
+ var message = new OpenIdConnectMessage()
+ {
+ EnableTelemetryParameters = !Options.DisableTelemetry,
+ IssuerAddress = _configuration?.EndSessionEndpoint ?? string.Empty,
+
+ // Redirect back to SigneOutCallbackPath first before user agent is redirected to actual post logout redirect uri
+ PostLogoutRedirectUri = BuildRedirectUriIfRelative(Options.SignedOutCallbackPath)
+ };
+
+ // Get the post redirect URI.
+ if (string.IsNullOrEmpty(properties.RedirectUri))
+ {
+ properties.RedirectUri = BuildRedirectUriIfRelative(Options.SignedOutRedirectUri);
+ if (string.IsNullOrWhiteSpace(properties.RedirectUri))
+ {
+ properties.RedirectUri = CurrentUri;
+ }
+ }
+ Logger.PostSignOutRedirect(properties.RedirectUri);
+
+ // Attach the identity token to the logout request when possible.
+ message.IdTokenHint = await Context.GetTokenAsync(Options.SignOutScheme, OpenIdConnectParameterNames.IdToken);
+
+ var redirectContext = new RedirectContext(Context, Scheme, Options, properties)
+ {
+ ProtocolMessage = message
+ };
+
+ await Events.RedirectToIdentityProviderForSignOut(redirectContext);
+ if (redirectContext.Handled)
+ {
+ Logger.RedirectToIdentityProviderForSignOutHandledResponse();
+ return;
+ }
+
+ message = redirectContext.ProtocolMessage;
+
+ if (!string.IsNullOrEmpty(message.State))
+ {
+ properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State;
+ }
+
+ message.State = Options.StateDataFormat.Protect(properties);
+
+ if (string.IsNullOrEmpty(message.IssuerAddress))
+ {
+ throw new InvalidOperationException("Cannot redirect to the end session endpoint, the configuration may be missing or invalid.");
+ }
+
+ if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
+ {
+ var redirectUri = message.CreateLogoutRequestUrl();
+ if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
+ {
+ Logger.InvalidLogoutQueryStringRedirectUrl(redirectUri);
+ }
+
+ Response.Redirect(redirectUri);
+ }
+ else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
+ {
+ var content = message.BuildFormPost();
+ var buffer = Encoding.UTF8.GetBytes(content);
+
+ Response.ContentLength = buffer.Length;
+ Response.ContentType = "text/html;charset=UTF-8";
+
+ // Emit Cache-Control=no-cache to prevent client caching.
+ Response.Headers[HeaderNames.CacheControl] = "no-cache";
+ Response.Headers[HeaderNames.Pragma] = "no-cache";
+ Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate;
+
+ await Response.Body.WriteAsync(buffer, 0, buffer.Length);
+ }
+ else
+ {
+ throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}");
+ }
+
+ Logger.SignedOut(Scheme.Name);
+ }
+
+ /// <summary>
+ /// Response to the callback from OpenId provider after session ended.
+ /// </summary>
+ /// <returns>A task executing the callback procedure</returns>
+ protected async virtual Task<bool> HandleSignOutCallbackAsync()
+ {
+ var message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
+ AuthenticationProperties properties = null;
+ if (!string.IsNullOrEmpty(message.State))
+ {
+ properties = Options.StateDataFormat.Unprotect(message.State);
+ }
+
+ var signOut = new RemoteSignOutContext(Context, Scheme, Options, message)
+ {
+ Properties = properties,
+ };
+
+ await Events.SignedOutCallbackRedirect(signOut);
+ if (signOut.Result != null)
+ {
+ if (signOut.Result.Handled)
+ {
+ Logger.SignoutCallbackRedirectHandledResponse();
+ return true;
+ }
+ if (signOut.Result.Skipped)
+ {
+ Logger.SignoutCallbackRedirectSkipped();
+ return false;
+ }
+ if (signOut.Result.Failure != null)
+ {
+ throw new InvalidOperationException("An error was returned from the SignedOutCallbackRedirect event.", signOut.Result.Failure);
+ }
+ }
+
+ properties = signOut.Properties;
+ if (!string.IsNullOrEmpty(properties?.RedirectUri))
+ {
+ Response.Redirect(properties.RedirectUri);
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Responds to a 401 Challenge. Sends an OpenIdConnect message to the 'identity authority' to obtain an identity.
+ /// </summary>
+ /// <returns></returns>
+ protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
+ {
+ Logger.EnteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(GetType().FullName);
+
+ // order for local RedirectUri
+ // 1. challenge.Properties.RedirectUri
+ // 2. CurrentUri if RedirectUri is not set)
+ if (string.IsNullOrEmpty(properties.RedirectUri))
+ {
+ properties.RedirectUri = CurrentUri;
+ }
+ Logger.PostAuthenticationLocalRedirect(properties.RedirectUri);
+
+ if (_configuration == null && Options.ConfigurationManager != null)
+ {
+ _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
+ }
+
+ var message = new OpenIdConnectMessage
+ {
+ ClientId = Options.ClientId,
+ EnableTelemetryParameters = !Options.DisableTelemetry,
+ IssuerAddress = _configuration?.AuthorizationEndpoint ?? string.Empty,
+ RedirectUri = BuildRedirectUri(Options.CallbackPath),
+ Resource = Options.Resource,
+ ResponseType = Options.ResponseType,
+ Prompt = properties.GetParameter<string>(OpenIdConnectParameterNames.Prompt) ?? Options.Prompt,
+ Scope = string.Join(" ", properties.GetParameter<ICollection<string>>(OpenIdConnectParameterNames.Scope) ?? Options.Scope),
+ };
+
+ // Add the 'max_age' parameter to the authentication request if MaxAge is not null.
+ // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
+ var maxAge = properties.GetParameter<TimeSpan?>(OpenIdConnectParameterNames.MaxAge) ?? Options.MaxAge;
+ if (maxAge.HasValue)
+ {
+ message.MaxAge = Convert.ToInt64(Math.Floor((maxAge.Value).TotalSeconds))
+ .ToString(CultureInfo.InvariantCulture);
+ }
+
+ // Omitting the response_mode parameter when it already corresponds to the default
+ // response_mode used for the specified response_type is recommended by the specifications.
+ // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
+ if (!string.Equals(Options.ResponseType, OpenIdConnectResponseType.Code, StringComparison.Ordinal) ||
+ !string.Equals(Options.ResponseMode, OpenIdConnectResponseMode.Query, StringComparison.Ordinal))
+ {
+ message.ResponseMode = Options.ResponseMode;
+ }
+
+ if (Options.ProtocolValidator.RequireNonce)
+ {
+ message.Nonce = Options.ProtocolValidator.GenerateNonce();
+ WriteNonceCookie(message.Nonce);
+ }
+
+ GenerateCorrelationId(properties);
+
+ var redirectContext = new RedirectContext(Context, Scheme, Options, properties)
+ {
+ ProtocolMessage = message
+ };
+
+ await Events.RedirectToIdentityProvider(redirectContext);
+ if (redirectContext.Handled)
+ {
+ Logger.RedirectToIdentityProviderHandledResponse();
+ return;
+ }
+
+ message = redirectContext.ProtocolMessage;
+
+ if (!string.IsNullOrEmpty(message.State))
+ {
+ properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State;
+ }
+
+ // When redeeming a 'code' for an AccessToken, this value is needed
+ properties.Items.Add(OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, message.RedirectUri);
+
+ message.State = Options.StateDataFormat.Protect(properties);
+
+ if (string.IsNullOrEmpty(message.IssuerAddress))
+ {
+ throw new InvalidOperationException(
+ "Cannot redirect to the authorization endpoint, the configuration may be missing or invalid.");
+ }
+
+ if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
+ {
+ var redirectUri = message.CreateAuthenticationRequestUrl();
+ if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
+ {
+ Logger.InvalidAuthenticationRequestUrl(redirectUri);
+ }
+
+ Response.Redirect(redirectUri);
+ return;
+ }
+ else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
+ {
+ var content = message.BuildFormPost();
+ var buffer = Encoding.UTF8.GetBytes(content);
+
+ Response.ContentLength = buffer.Length;
+ Response.ContentType = "text/html;charset=UTF-8";
+
+ // Emit Cache-Control=no-cache to prevent client caching.
+ Response.Headers[HeaderNames.CacheControl] = "no-cache";
+ Response.Headers[HeaderNames.Pragma] = "no-cache";
+ Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate;
+
+ await Response.Body.WriteAsync(buffer, 0, buffer.Length);
+ return;
+ }
+
+ throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}");
+ }
+
+ /// <summary>
+ /// Invoked to process incoming OpenIdConnect messages.
+ /// </summary>
+ /// <returns>An <see cref="HandleRequestResult"/>.</returns>
+ protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
+ {
+ Logger.EnteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync(GetType().FullName);
+
+ OpenIdConnectMessage authorizationResponse = null;
+
+ if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
+ {
+ authorizationResponse = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
+
+ // response_mode=query (explicit or not) and a response_type containing id_token
+ // or token are not considered as a safe combination and MUST be rejected.
+ // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security
+ if (!string.IsNullOrEmpty(authorizationResponse.IdToken) || !string.IsNullOrEmpty(authorizationResponse.AccessToken))
+ {
+ if (Options.SkipUnrecognizedRequests)
+ {
+ // Not for us?
+ return HandleRequestResult.SkipHandler();
+ }
+ return HandleRequestResult.Fail("An OpenID Connect response cannot contain an " +
+ "identity token or an access token when using response_mode=query");
+ }
+ }
+ // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small.
+ else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)
+ && !string.IsNullOrEmpty(Request.ContentType)
+ // May have media/type; charset=utf-8, allow partial match.
+ && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)
+ && Request.Body.CanRead)
+ {
+ var form = await Request.ReadFormAsync();
+ authorizationResponse = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
+ }
+
+ if (authorizationResponse == null)
+ {
+ if (Options.SkipUnrecognizedRequests)
+ {
+ // Not for us?
+ return HandleRequestResult.SkipHandler();
+ }
+ return HandleRequestResult.Fail("No message.");
+ }
+
+ AuthenticationProperties properties = null;
+ try
+ {
+ properties = ReadPropertiesAndClearState(authorizationResponse);
+
+ var messageReceivedContext = await RunMessageReceivedEventAsync(authorizationResponse, properties);
+ if (messageReceivedContext.Result != null)
+ {
+ return messageReceivedContext.Result;
+ }
+ authorizationResponse = messageReceivedContext.ProtocolMessage;
+ properties = messageReceivedContext.Properties;
+
+ if (properties == null)
+ {
+ // Fail if state is missing, it's required for the correlation id.
+ if (string.IsNullOrEmpty(authorizationResponse.State))
+ {
+ // This wasn't a valid OIDC message, it may not have been intended for us.
+ Logger.NullOrEmptyAuthorizationResponseState();
+ if (Options.SkipUnrecognizedRequests)
+ {
+ return HandleRequestResult.SkipHandler();
+ }
+ return HandleRequestResult.Fail(Resources.MessageStateIsNullOrEmpty);
+ }
+
+ properties = ReadPropertiesAndClearState(authorizationResponse);
+ }
+
+ if (properties == null)
+ {
+ Logger.UnableToReadAuthorizationResponseState();
+ if (Options.SkipUnrecognizedRequests)
+ {
+ // Not for us?
+ return HandleRequestResult.SkipHandler();
+ }
+
+ // if state exists and we failed to 'unprotect' this is not a message we should process.
+ return HandleRequestResult.Fail(Resources.MessageStateIsInvalid);
+ }
+
+ if (!ValidateCorrelationId(properties))
+ {
+ return HandleRequestResult.Fail("Correlation failed.", properties);
+ }
+
+ // if any of the error fields are set, throw error null
+ if (!string.IsNullOrEmpty(authorizationResponse.Error))
+ {
+ return HandleRequestResult.Fail(CreateOpenIdConnectProtocolException(authorizationResponse, response: null), properties);
+ }
+
+ if (_configuration == null && Options.ConfigurationManager != null)
+ {
+ Logger.UpdatingConfiguration();
+ _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
+ }
+
+ PopulateSessionProperties(authorizationResponse, properties);
+
+ ClaimsPrincipal user = null;
+ JwtSecurityToken jwt = null;
+ string nonce = null;
+ var validationParameters = Options.TokenValidationParameters.Clone();
+
+ // Hybrid or Implicit flow
+ if (!string.IsNullOrEmpty(authorizationResponse.IdToken))
+ {
+ Logger.ReceivedIdToken();
+ user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt);
+
+ nonce = jwt.Payload.Nonce;
+ if (!string.IsNullOrEmpty(nonce))
+ {
+ nonce = ReadNonceCookie(nonce);
+ }
+
+ var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, null, user, properties, jwt, nonce);
+ if (tokenValidatedContext.Result != null)
+ {
+ return tokenValidatedContext.Result;
+ }
+ authorizationResponse = tokenValidatedContext.ProtocolMessage;
+ user = tokenValidatedContext.Principal;
+ properties = tokenValidatedContext.Properties;
+ jwt = tokenValidatedContext.SecurityToken;
+ nonce = tokenValidatedContext.Nonce;
+ }
+
+ Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext()
+ {
+ ClientId = Options.ClientId,
+ ProtocolMessage = authorizationResponse,
+ ValidatedIdToken = jwt,
+ Nonce = nonce
+ });
+
+ OpenIdConnectMessage tokenEndpointResponse = null;
+
+ // Authorization Code or Hybrid flow
+ if (!string.IsNullOrEmpty(authorizationResponse.Code))
+ {
+ var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(authorizationResponse, user, properties, jwt);
+ if (authorizationCodeReceivedContext.Result != null)
+ {
+ return authorizationCodeReceivedContext.Result;
+ }
+ authorizationResponse = authorizationCodeReceivedContext.ProtocolMessage;
+ user = authorizationCodeReceivedContext.Principal;
+ properties = authorizationCodeReceivedContext.Properties;
+ var tokenEndpointRequest = authorizationCodeReceivedContext.TokenEndpointRequest;
+ // If the developer redeemed the code themselves...
+ tokenEndpointResponse = authorizationCodeReceivedContext.TokenEndpointResponse;
+ jwt = authorizationCodeReceivedContext.JwtSecurityToken;
+
+ if (!authorizationCodeReceivedContext.HandledCodeRedemption)
+ {
+ tokenEndpointResponse = await RedeemAuthorizationCodeAsync(tokenEndpointRequest);
+ }
+
+ var tokenResponseReceivedContext = await RunTokenResponseReceivedEventAsync(authorizationResponse, tokenEndpointResponse, user, properties);
+ if (tokenResponseReceivedContext.Result != null)
+ {
+ return tokenResponseReceivedContext.Result;
+ }
+
+ authorizationResponse = tokenResponseReceivedContext.ProtocolMessage;
+ tokenEndpointResponse = tokenResponseReceivedContext.TokenEndpointResponse;
+ user = tokenResponseReceivedContext.Principal;
+ properties = tokenResponseReceivedContext.Properties;
+
+ // no need to validate signature when token is received using "code flow" as per spec
+ // [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation].
+ validationParameters.RequireSignedTokens = false;
+
+ // At least a cursory validation is required on the new IdToken, even if we've already validated the one from the authorization response.
+ // And we'll want to validate the new JWT in ValidateTokenResponse.
+ var tokenEndpointUser = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out var tokenEndpointJwt);
+
+ // Avoid reading & deleting the nonce cookie, running the event, etc, if it was already done as part of the authorization response validation.
+ if (user == null)
+ {
+ nonce = tokenEndpointJwt.Payload.Nonce;
+ if (!string.IsNullOrEmpty(nonce))
+ {
+ nonce = ReadNonceCookie(nonce);
+ }
+
+ var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, tokenEndpointResponse, tokenEndpointUser, properties, tokenEndpointJwt, nonce);
+ if (tokenValidatedContext.Result != null)
+ {
+ return tokenValidatedContext.Result;
+ }
+ authorizationResponse = tokenValidatedContext.ProtocolMessage;
+ tokenEndpointResponse = tokenValidatedContext.TokenEndpointResponse;
+ user = tokenValidatedContext.Principal;
+ properties = tokenValidatedContext.Properties;
+ jwt = tokenValidatedContext.SecurityToken;
+ nonce = tokenValidatedContext.Nonce;
+ }
+ else
+ {
+ if (!string.Equals(jwt.Subject, tokenEndpointJwt.Subject, StringComparison.Ordinal))
+ {
+ throw new SecurityTokenException("The sub claim does not match in the id_token's from the authorization and token endpoints.");
+ }
+
+ jwt = tokenEndpointJwt;
+ }
+
+ // Validate the token response if it wasn't provided manually
+ if (!authorizationCodeReceivedContext.HandledCodeRedemption)
+ {
+ Options.ProtocolValidator.ValidateTokenResponse(new OpenIdConnectProtocolValidationContext()
+ {
+ ClientId = Options.ClientId,
+ ProtocolMessage = tokenEndpointResponse,
+ ValidatedIdToken = jwt,
+ Nonce = nonce
+ });
+ }
+ }
+
+ if (Options.SaveTokens)
+ {
+ SaveTokens(properties, tokenEndpointResponse ?? authorizationResponse);
+ }
+
+ if (Options.GetClaimsFromUserInfoEndpoint)
+ {
+ return await GetUserInformationAsync(tokenEndpointResponse ?? authorizationResponse, jwt, user, properties);
+ }
+ else
+ {
+ var identity = (ClaimsIdentity)user.Identity;
+ foreach (var action in Options.ClaimActions)
+ {
+ action.Run(null, identity, ClaimsIssuer);
+ }
+ }
+
+ return HandleRequestResult.Success(new AuthenticationTicket(user, properties, Scheme.Name));
+ }
+ catch (Exception exception)
+ {
+ Logger.ExceptionProcessingMessage(exception);
+
+ // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
+ if (Options.RefreshOnIssuerKeyNotFound && exception is SecurityTokenSignatureKeyNotFoundException)
+ {
+ if (Options.ConfigurationManager != null)
+ {
+ Logger.ConfigurationManagerRequestRefreshCalled();
+ Options.ConfigurationManager.RequestRefresh();
+ }
+ }
+
+ var authenticationFailedContext = await RunAuthenticationFailedEventAsync(authorizationResponse, exception);
+ if (authenticationFailedContext.Result != null)
+ {
+ return authenticationFailedContext.Result;
+ }
+
+ return HandleRequestResult.Fail(exception, properties);
+ }
+ }
+
+ private AuthenticationProperties ReadPropertiesAndClearState(OpenIdConnectMessage message)
+ {
+ AuthenticationProperties properties = null;
+ if (!string.IsNullOrEmpty(message.State))
+ {
+ properties = Options.StateDataFormat.Unprotect(message.State);
+
+ if (properties != null)
+ {
+ // If properties can be decoded from state, clear the message state.
+ properties.Items.TryGetValue(OpenIdConnectDefaults.UserstatePropertiesKey, out var userstate);
+ message.State = userstate;
+ }
+ }
+ return properties;
+ }
+
+ private void PopulateSessionProperties(OpenIdConnectMessage message, AuthenticationProperties properties)
+ {
+ if (!string.IsNullOrEmpty(message.SessionState))
+ {
+ properties.Items[OpenIdConnectSessionProperties.SessionState] = message.SessionState;
+ }
+
+ if (!string.IsNullOrEmpty(_configuration.CheckSessionIframe))
+ {
+ properties.Items[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe;
+ }
+ }
+
+ /// <summary>
+ /// Redeems the authorization code for tokens at the token endpoint.
+ /// </summary>
+ /// <param name="tokenEndpointRequest">The request that will be sent to the token endpoint and is available for customization.</param>
+ /// <returns>OpenIdConnect message that has tokens inside it.</returns>
+ protected virtual async Task<OpenIdConnectMessage> RedeemAuthorizationCodeAsync(OpenIdConnectMessage tokenEndpointRequest)
+ {
+ Logger.RedeemingCodeForTokens();
+
+ var requestMessage = new HttpRequestMessage(HttpMethod.Post, _configuration.TokenEndpoint);
+ requestMessage.Content = new FormUrlEncodedContent(tokenEndpointRequest.Parameters);
+
+ var responseMessage = await Backchannel.SendAsync(requestMessage);
+
+ var contentMediaType = responseMessage.Content.Headers.ContentType?.MediaType;
+ if (string.IsNullOrEmpty(contentMediaType))
+ {
+ Logger.LogDebug($"Unexpected token response format. Status Code: {(int)responseMessage.StatusCode}. Content-Type header is missing.");
+ }
+ else if (!string.Equals(contentMediaType, "application/json", StringComparison.OrdinalIgnoreCase))
+ {
+ Logger.LogDebug($"Unexpected token response format. Status Code: {(int)responseMessage.StatusCode}. Content-Type {responseMessage.Content.Headers.ContentType}.");
+ }
+
+ // Error handling:
+ // 1. If the response body can't be parsed as json, throws.
+ // 2. If the response's status code is not in 2XX range, throw OpenIdConnectProtocolException. If the body is correct parsed,
+ // pass the error information from body to the exception.
+ OpenIdConnectMessage message;
+ try
+ {
+ var responseContent = await responseMessage.Content.ReadAsStringAsync();
+ message = new OpenIdConnectMessage(responseContent);
+ }
+ catch (Exception ex)
+ {
+ throw new OpenIdConnectProtocolException($"Failed to parse token response body as JSON. Status Code: {(int)responseMessage.StatusCode}. Content-Type: {responseMessage.Content.Headers.ContentType}", ex);
+ }
+
+ if (!responseMessage.IsSuccessStatusCode)
+ {
+ throw CreateOpenIdConnectProtocolException(message, responseMessage);
+ }
+
+ return message;
+ }
+
+ /// <summary>
+ /// Goes to UserInfo endpoint to retrieve additional claims and add any unique claims to the given identity.
+ /// </summary>
+ /// <param name="message">message that is being processed</param>
+ /// <param name="jwt">The <see cref="JwtSecurityToken"/>.</param>
+ /// <param name="principal">The claims principal and identities.</param>
+ /// <param name="properties">The authentication properties.</param>
+ /// <returns><see cref="HandleRequestResult"/> which is used to determine if the remote authentication was successful.</returns>
+ protected virtual async Task<HandleRequestResult> GetUserInformationAsync(
+ OpenIdConnectMessage message, JwtSecurityToken jwt,
+ ClaimsPrincipal principal, AuthenticationProperties properties)
+ {
+ var userInfoEndpoint = _configuration?.UserInfoEndpoint;
+
+ if (string.IsNullOrEmpty(userInfoEndpoint))
+ {
+ Logger.UserInfoEndpointNotSet();
+ return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name));
+ }
+ if (string.IsNullOrEmpty(message.AccessToken))
+ {
+ Logger.AccessTokenNotAvailable();
+ return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name));
+ }
+ Logger.RetrievingClaims();
+ var requestMessage = new HttpRequestMessage(HttpMethod.Get, userInfoEndpoint);
+ requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", message.AccessToken);
+ var responseMessage = await Backchannel.SendAsync(requestMessage);
+ responseMessage.EnsureSuccessStatusCode();
+ var userInfoResponse = await responseMessage.Content.ReadAsStringAsync();
+
+ JObject user;
+ var contentType = responseMessage.Content.Headers.ContentType;
+ if (contentType.MediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase))
+ {
+ user = JObject.Parse(userInfoResponse);
+ }
+ else if (contentType.MediaType.Equals("application/jwt", StringComparison.OrdinalIgnoreCase))
+ {
+ var userInfoEndpointJwt = new JwtSecurityToken(userInfoResponse);
+ user = JObject.FromObject(userInfoEndpointJwt.Payload);
+ }
+ else
+ {
+ return HandleRequestResult.Fail("Unknown response type: " + contentType.MediaType, properties);
+ }
+
+ var userInformationReceivedContext = await RunUserInformationReceivedEventAsync(principal, properties, message, user);
+ if (userInformationReceivedContext.Result != null)
+ {
+ return userInformationReceivedContext.Result;
+ }
+ principal = userInformationReceivedContext.Principal;
+ properties = userInformationReceivedContext.Properties;
+ user = userInformationReceivedContext.User;
+
+ Options.ProtocolValidator.ValidateUserInfoResponse(new OpenIdConnectProtocolValidationContext()
+ {
+ UserInfoEndpointResponse = userInfoResponse,
+ ValidatedIdToken = jwt,
+ });
+
+ var identity = (ClaimsIdentity)principal.Identity;
+
+ foreach (var action in Options.ClaimActions)
+ {
+ action.Run(user, identity, ClaimsIssuer);
+ }
+
+ return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name));
+ }
+
+ /// <summary>
+ /// Save the tokens contained in the <see cref="OpenIdConnectMessage"/> in the <see cref="ClaimsPrincipal"/>.
+ /// </summary>
+ /// <param name="properties">The <see cref="AuthenticationProperties"/> in which tokens are saved.</param>
+ /// <param name="message">The OpenID Connect response.</param>
+ private void SaveTokens(AuthenticationProperties properties, OpenIdConnectMessage message)
+ {
+ var tokens = new List<AuthenticationToken>();
+
+ if (!string.IsNullOrEmpty(message.AccessToken))
+ {
+ tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = message.AccessToken });
+ }
+
+ if (!string.IsNullOrEmpty(message.IdToken))
+ {
+ tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.IdToken, Value = message.IdToken });
+ }
+
+ if (!string.IsNullOrEmpty(message.RefreshToken))
+ {
+ tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.RefreshToken, Value = message.RefreshToken });
+ }
+
+ if (!string.IsNullOrEmpty(message.TokenType))
+ {
+ tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.TokenType, Value = message.TokenType });
+ }
+
+ if (!string.IsNullOrEmpty(message.ExpiresIn))
+ {
+ if (int.TryParse(message.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
+ {
+ var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value);
+ // https://www.w3.org/TR/xmlschema-2/#dateTime
+ // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
+ tokens.Add(new AuthenticationToken { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) });
+ }
+ }
+
+ properties.StoreTokens(tokens);
+ }
+
+ /// <summary>
+ /// Adds the nonce to <see cref="HttpResponse.Cookies"/>.
+ /// </summary>
+ /// <param name="nonce">the nonce to remember.</param>
+ /// <remarks><see cref="M:IResponseCookies.Append"/> of <see cref="HttpResponse.Cookies"/> is called to add a cookie with the name: 'OpenIdConnectAuthenticationDefaults.Nonce + <see cref="M:ISecureDataFormat{TData}.Protect"/>(nonce)' of <see cref="OpenIdConnectOptions.StringDataFormat"/>.
+ /// The value of the cookie is: "N".</remarks>
+ private void WriteNonceCookie(string nonce)
+ {
+ if (string.IsNullOrEmpty(nonce))
+ {
+ throw new ArgumentNullException(nameof(nonce));
+ }
+
+ var cookieOptions = Options.NonceCookie.Build(Context, Clock.UtcNow);
+
+ Response.Cookies.Append(
+ Options.NonceCookie.Name + Options.StringDataFormat.Protect(nonce),
+ NonceProperty,
+ cookieOptions);
+ }
+
+ /// <summary>
+ /// Searches <see cref="HttpRequest.Cookies"/> for a matching nonce.
+ /// </summary>
+ /// <param name="nonce">the nonce that we are looking for.</param>
+ /// <returns>echos 'nonce' if a cookie is found that matches, null otherwise.</returns>
+ /// <remarks>Examine <see cref="IRequestCookieCollection.Keys"/> of <see cref="HttpRequest.Cookies"/> that start with the prefix: 'OpenIdConnectAuthenticationDefaults.Nonce'.
+ /// <see cref="M:ISecureDataFormat{TData}.Unprotect"/> of <see cref="OpenIdConnectOptions.StringDataFormat"/> is used to obtain the actual 'nonce'. If the nonce is found, then <see cref="M:IResponseCookies.Delete"/> of <see cref="HttpResponse.Cookies"/> is called.</remarks>
+ private string ReadNonceCookie(string nonce)
+ {
+ if (nonce == null)
+ {
+ return null;
+ }
+
+ foreach (var nonceKey in Request.Cookies.Keys)
+ {
+ if (nonceKey.StartsWith(Options.NonceCookie.Name))
+ {
+ try
+ {
+ var nonceDecodedValue = Options.StringDataFormat.Unprotect(nonceKey.Substring(Options.NonceCookie.Name.Length, nonceKey.Length - Options.NonceCookie.Name.Length));
+ if (nonceDecodedValue == nonce)
+ {
+ var cookieOptions = Options.NonceCookie.Build(Context, Clock.UtcNow);
+ Response.Cookies.Delete(nonceKey, cookieOptions);
+ return nonce;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.UnableToProtectNonceCookie(ex);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private AuthenticationProperties GetPropertiesFromState(string state)
+ {
+ // assume a well formed query string: <a=b&>OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey=kasjd;fljasldkjflksdj<&c=d>
+ var startIndex = 0;
+ if (string.IsNullOrEmpty(state) || (startIndex = state.IndexOf(OpenIdConnectDefaults.AuthenticationPropertiesKey, StringComparison.Ordinal)) == -1)
+ {
+ return null;
+ }
+
+ var authenticationIndex = startIndex + OpenIdConnectDefaults.AuthenticationPropertiesKey.Length;
+ if (authenticationIndex == -1 || authenticationIndex == state.Length || state[authenticationIndex] != '=')
+ {
+ return null;
+ }
+
+ // scan rest of string looking for '&'
+ authenticationIndex++;
+ var endIndex = state.Substring(authenticationIndex, state.Length - authenticationIndex).IndexOf("&", StringComparison.Ordinal);
+
+ // -1 => no other parameters are after the AuthenticationPropertiesKey
+ if (endIndex == -1)
+ {
+ return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex).Replace('+', ' ')));
+ }
+ else
+ {
+ return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex, endIndex).Replace('+', ' ')));
+ }
+ }
+
+ private async Task<MessageReceivedContext> RunMessageReceivedEventAsync(OpenIdConnectMessage message, AuthenticationProperties properties)
+ {
+ Logger.MessageReceived(message.BuildRedirectUrl());
+ var context = new MessageReceivedContext(Context, Scheme, Options, properties)
+ {
+ ProtocolMessage = message,
+ };
+
+ await Events.MessageReceived(context);
+ if (context.Result != null)
+ {
+ if (context.Result.Handled)
+ {
+ Logger.MessageReceivedContextHandledResponse();
+ }
+ else if (context.Result.Skipped)
+ {
+ Logger.MessageReceivedContextSkipped();
+ }
+ }
+
+ return context;
+ }
+
+ private async Task<TokenValidatedContext> RunTokenValidatedEventAsync(OpenIdConnectMessage authorizationResponse, OpenIdConnectMessage tokenEndpointResponse, ClaimsPrincipal user, AuthenticationProperties properties, JwtSecurityToken jwt, string nonce)
+ {
+ var context = new TokenValidatedContext(Context, Scheme, Options, user, properties)
+ {
+ ProtocolMessage = authorizationResponse,
+ TokenEndpointResponse = tokenEndpointResponse,
+ SecurityToken = jwt,
+ Nonce = nonce,
+ };
+
+ await Events.TokenValidated(context);
+ if (context.Result != null)
+ {
+ if (context.Result.Handled)
+ {
+ Logger.TokenValidatedHandledResponse();
+ }
+ else if (context.Result.Skipped)
+ {
+ Logger.TokenValidatedSkipped();
+ }
+ }
+
+ return context;
+ }
+
+ private async Task<AuthorizationCodeReceivedContext> RunAuthorizationCodeReceivedEventAsync(OpenIdConnectMessage authorizationResponse, ClaimsPrincipal user, AuthenticationProperties properties, JwtSecurityToken jwt)
+ {
+ Logger.AuthorizationCodeReceived();
+
+ var tokenEndpointRequest = new OpenIdConnectMessage()
+ {
+ ClientId = Options.ClientId,
+ ClientSecret = Options.ClientSecret,
+ Code = authorizationResponse.Code,
+ GrantType = OpenIdConnectGrantTypes.AuthorizationCode,
+ EnableTelemetryParameters = !Options.DisableTelemetry,
+ RedirectUri = properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey]
+ };
+
+ var context = new AuthorizationCodeReceivedContext(Context, Scheme, Options, properties)
+ {
+ ProtocolMessage = authorizationResponse,
+ TokenEndpointRequest = tokenEndpointRequest,
+ Principal = user,
+ JwtSecurityToken = jwt,
+ Backchannel = Backchannel
+ };
+
+ await Events.AuthorizationCodeReceived(context);
+ if (context.Result != null)
+ {
+ if (context.Result.Handled)
+ {
+ Logger.AuthorizationCodeReceivedContextHandledResponse();
+ }
+ else if (context.Result.Skipped)
+ {
+ Logger.AuthorizationCodeReceivedContextSkipped();
+ }
+ }
+
+ return context;
+ }
+
+ private async Task<TokenResponseReceivedContext> RunTokenResponseReceivedEventAsync(
+ OpenIdConnectMessage message,
+ OpenIdConnectMessage tokenEndpointResponse,
+ ClaimsPrincipal user,
+ AuthenticationProperties properties)
+ {
+ Logger.TokenResponseReceived();
+ var context = new TokenResponseReceivedContext(Context, Scheme, Options, user, properties)
+ {
+ ProtocolMessage = message,
+ TokenEndpointResponse = tokenEndpointResponse,
+ };
+
+ await Events.TokenResponseReceived(context);
+ if (context.Result != null)
+ {
+ if (context.Result.Handled)
+ {
+ Logger.TokenResponseReceivedHandledResponse();
+ }
+ else if (context.Result.Skipped)
+ {
+ Logger.TokenResponseReceivedSkipped();
+ }
+ }
+
+ return context;
+ }
+
+ private async Task<UserInformationReceivedContext> RunUserInformationReceivedEventAsync(ClaimsPrincipal principal, AuthenticationProperties properties, OpenIdConnectMessage message, JObject user)
+ {
+ Logger.UserInformationReceived(user.ToString());
+
+ var context = new UserInformationReceivedContext(Context, Scheme, Options, principal, properties)
+ {
+ ProtocolMessage = message,
+ User = user,
+ };
+
+ await Events.UserInformationReceived(context);
+ if (context.Result != null)
+ {
+ if (context.Result.Handled)
+ {
+ Logger.UserInformationReceivedHandledResponse();
+ }
+ else if (context.Result.Skipped)
+ {
+ Logger.UserInformationReceivedSkipped();
+ }
+ }
+
+ return context;
+ }
+
+ private async Task<AuthenticationFailedContext> RunAuthenticationFailedEventAsync(OpenIdConnectMessage message, Exception exception)
+ {
+ var context = new AuthenticationFailedContext(Context, Scheme, Options)
+ {
+ ProtocolMessage = message,
+ Exception = exception
+ };
+
+ await Events.AuthenticationFailed(context);
+ if (context.Result != null)
+ {
+ if (context.Result.Handled)
+ {
+ Logger.AuthenticationFailedContextHandledResponse();
+ }
+ else if (context.Result.Skipped)
+ {
+ Logger.AuthenticationFailedContextSkipped();
+ }
+ }
+
+ return context;
+ }
+
+ // Note this modifies properties if Options.UseTokenLifetime
+ private ClaimsPrincipal ValidateToken(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters, out JwtSecurityToken jwt)
+ {
+ if (!Options.SecurityTokenValidator.CanReadToken(idToken))
+ {
+ Logger.UnableToReadIdToken(idToken);
+ throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken));
+ }
+
+ if (_configuration != null)
+ {
+ var issuer = new[] { _configuration.Issuer };
+ validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuer) ?? issuer;
+
+ validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys)
+ ?? _configuration.SigningKeys;
+ }
+
+ var principal = Options.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out SecurityToken validatedToken);
+ jwt = validatedToken as JwtSecurityToken;
+ if (jwt == null)
+ {
+ Logger.InvalidSecurityTokenType(validatedToken?.GetType().ToString());
+ throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.ValidatedSecurityTokenNotJwt, validatedToken?.GetType()));
+ }
+
+ if (validatedToken == null)
+ {
+ Logger.UnableToValidateIdToken(idToken);
+ throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken));
+ }
+
+ if (Options.UseTokenLifetime)
+ {
+ var issued = validatedToken.ValidFrom;
+ if (issued != DateTime.MinValue)
+ {
+ properties.IssuedUtc = issued;
+ }
+
+ var expires = validatedToken.ValidTo;
+ if (expires != DateTime.MinValue)
+ {
+ properties.ExpiresUtc = expires;
+ }
+ }
+
+ return principal;
+ }
+
+ /// <summary>
+ /// Build a redirect path if the given path is a relative path.
+ /// </summary>
+ private string BuildRedirectUriIfRelative(string uri)
+ {
+ if (string.IsNullOrEmpty(uri))
+ {
+ return uri;
+ }
+
+ if (!uri.StartsWith("/", StringComparison.Ordinal))
+ {
+ return uri;
+ }
+
+ return BuildRedirectUri(uri);
+ }
+
+ private OpenIdConnectProtocolException CreateOpenIdConnectProtocolException(OpenIdConnectMessage message, HttpResponseMessage response)
+ {
+ var description = message.ErrorDescription ?? "error_description is null";
+ var errorUri = message.ErrorUri ?? "error_uri is null";
+
+ if (response != null)
+ {
+ Logger.ResponseErrorWithStatusCode(message.Error, description, errorUri, (int)response.StatusCode);
+ }
+ else
+ {
+ Logger.ResponseError(message.Error, description, errorUri);
+ }
+
+ return new OpenIdConnectProtocolException(string.Format(
+ CultureInfo.InvariantCulture,
+ Resources.MessageContainsError,
+ message.Error,
+ description,
+ errorUri));
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs
new file mode 100644
index 0000000000..cbf6e8eab6
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs
@@ -0,0 +1,326 @@
+// 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.IdentityModel.Tokens.Jwt;
+using Microsoft.AspNetCore.Authentication.Internal;
+using Microsoft.AspNetCore.Authentication.OAuth.Claims;
+using Microsoft.AspNetCore.Http;
+using Microsoft.IdentityModel.Protocols;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ /// <summary>
+ /// Configuration options for <see cref="OpenIdConnectHandler"/>
+ /// </summary>
+ public class OpenIdConnectOptions : RemoteAuthenticationOptions
+ {
+ private CookieBuilder _nonceCookieBuilder;
+
+ /// <summary>
+ /// Initializes a new <see cref="OpenIdConnectOptions"/>
+ /// </summary>
+ /// <remarks>
+ /// Defaults:
+ /// <para>AddNonceToRequest: true.</para>
+ /// <para>BackchannelTimeout: 1 minute.</para>
+ /// <para>ProtocolValidator: new <see cref="OpenIdConnectProtocolValidator"/>.</para>
+ /// <para>RefreshOnIssuerKeyNotFound: true</para>
+ /// <para>ResponseType: <see cref="OpenIdConnectResponseType.CodeIdToken"/></para>
+ /// <para>Scope: <see cref="OpenIdConnectScope.OpenIdProfile"/>.</para>
+ /// <para>TokenValidationParameters: new <see cref="TokenValidationParameters"/> with AuthenticationScheme = authenticationScheme.</para>
+ /// <para>UseTokenLifetime: false.</para>
+ /// </remarks>
+ public OpenIdConnectOptions()
+ {
+ CallbackPath = new PathString("/signin-oidc");
+ SignedOutCallbackPath = new PathString("/signout-callback-oidc");
+ RemoteSignOutPath = new PathString("/signout-oidc");
+
+ Events = new OpenIdConnectEvents();
+ Scope.Add("openid");
+ Scope.Add("profile");
+
+ ClaimActions.DeleteClaim("nonce");
+ ClaimActions.DeleteClaim("aud");
+ ClaimActions.DeleteClaim("azp");
+ ClaimActions.DeleteClaim("acr");
+ ClaimActions.DeleteClaim("amr");
+ ClaimActions.DeleteClaim("iss");
+ ClaimActions.DeleteClaim("iat");
+ ClaimActions.DeleteClaim("nbf");
+ ClaimActions.DeleteClaim("exp");
+ ClaimActions.DeleteClaim("at_hash");
+ ClaimActions.DeleteClaim("c_hash");
+ ClaimActions.DeleteClaim("auth_time");
+ ClaimActions.DeleteClaim("ipaddr");
+ ClaimActions.DeleteClaim("platf");
+ ClaimActions.DeleteClaim("ver");
+
+ // http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
+ ClaimActions.MapUniqueJsonKey("sub", "sub");
+ ClaimActions.MapUniqueJsonKey("name", "name");
+ ClaimActions.MapUniqueJsonKey("given_name", "given_name");
+ ClaimActions.MapUniqueJsonKey("family_name", "family_name");
+ ClaimActions.MapUniqueJsonKey("profile", "profile");
+ ClaimActions.MapUniqueJsonKey("email", "email");
+
+ _nonceCookieBuilder = new OpenIdConnectNonceCookieBuilder(this)
+ {
+ Name = OpenIdConnectDefaults.CookieNoncePrefix,
+ HttpOnly = true,
+ SameSite = SameSiteMode.None,
+ SecurePolicy = CookieSecurePolicy.SameAsRequest,
+ IsEssential = true,
+ };
+ }
+
+ /// <summary>
+ /// Check that the options are valid. Should throw an exception if things are not ok.
+ /// </summary>
+ public override void Validate()
+ {
+ base.Validate();
+
+ if (MaxAge.HasValue && MaxAge.Value < TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(nameof(MaxAge), MaxAge.Value, "The value must not be a negative TimeSpan.");
+ }
+
+ if (string.IsNullOrEmpty(ClientId))
+ {
+ throw new ArgumentException("Options.ClientId must be provided", nameof(ClientId));
+ }
+
+ if (!CallbackPath.HasValue)
+ {
+ throw new ArgumentException("Options.CallbackPath must be provided.", nameof(CallbackPath));
+ }
+
+ if (ConfigurationManager == null)
+ {
+ throw new InvalidOperationException($"Provide {nameof(Authority)}, {nameof(MetadataAddress)}, "
+ + $"{nameof(Configuration)}, or {nameof(ConfigurationManager)} to {nameof(OpenIdConnectOptions)}");
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the Authority to use when making OpenIdConnect calls.
+ /// </summary>
+ public string Authority { get; set; }
+
+ /// <summary>
+ /// Gets or sets the 'client_id'.
+ /// </summary>
+ public string ClientId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the 'client_secret'.
+ /// </summary>
+ public string ClientSecret { get; set; }
+
+ /// <summary>
+ /// Configuration provided directly by the developer. If provided, then MetadataAddress and the Backchannel properties
+ /// will not be used. This information should not be updated during request processing.
+ /// </summary>
+ public OpenIdConnectConfiguration Configuration { get; set; }
+
+ /// <summary>
+ /// Responsible for retrieving, caching, and refreshing the configuration from metadata.
+ /// If not provided, then one will be created using the MetadataAddress and Backchannel properties.
+ /// </summary>
+ public IConfigurationManager<OpenIdConnectConfiguration> ConfigurationManager { get; set; }
+
+ /// <summary>
+ /// Boolean to set whether the handler should go to user info endpoint to retrieve additional claims or not after creating an identity from id_token received from token endpoint.
+ /// The default is 'false'.
+ /// </summary>
+ public bool GetClaimsFromUserInfoEndpoint { get; set; }
+
+ /// <summary>
+ /// A collection of claim actions used to select values from the json user data and create Claims.
+ /// </summary>
+ public ClaimActionCollection ClaimActions { get; } = new ClaimActionCollection();
+
+ /// <summary>
+ /// Gets or sets if HTTPS is required for the metadata address or authority.
+ /// The default is true. This should be disabled only in development environments.
+ /// </summary>
+ public bool RequireHttpsMetadata { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the discovery endpoint for obtaining metadata
+ /// </summary>
+ public string MetadataAddress { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="OpenIdConnectEvents"/> to notify when processing OpenIdConnect messages.
+ /// </summary>
+ public new OpenIdConnectEvents Events
+ {
+ get => (OpenIdConnectEvents)base.Events;
+ set => base.Events = value;
+ }
+
+ /// <summary>
+ /// Gets or sets the 'max_age'. If set the 'max_age' parameter will be sent with the authentication request. If the identity
+ /// provider has not actively authenticated the user within the length of time specified, the user will be prompted to
+ /// re-authenticate. By default no max_age is specified.
+ /// </summary>
+ public TimeSpan? MaxAge { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="OpenIdConnectProtocolValidator"/> that is used to ensure that the 'id_token' received
+ /// is valid per: http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
+ /// </summary>
+ /// <exception cref="ArgumentNullException">if 'value' is null.</exception>
+ public OpenIdConnectProtocolValidator ProtocolValidator { get; set; } = new OpenIdConnectProtocolValidator()
+ {
+ RequireStateValidation = false,
+ NonceLifetime = TimeSpan.FromMinutes(15)
+ };
+
+ /// <summary>
+ /// The request path within the application's base path where the user agent will be returned after sign out from the identity provider.
+ /// See post_logout_redirect_uri from http://openid.net/specs/openid-connect-session-1_0.html#RedirectionAfterLogout.
+ /// </summary>
+ public PathString SignedOutCallbackPath { get; set; }
+
+ /// <summary>
+ /// The uri where the user agent will be redirected to after application is signed out from the identity provider.
+ /// The redirect will happen after the SignedOutCallbackPath is invoked.
+ /// </summary>
+ /// <remarks>This URI can be out of the application's domain. By default it points to the root.</remarks>
+ public string SignedOutRedirectUri { get; set; } = "/";
+
+ /// <summary>
+ /// Gets or sets if a metadata refresh should be attempted after a SecurityTokenSignatureKeyNotFoundException. This allows for automatic
+ /// recovery in the event of a signature key rollover. This is enabled by default.
+ /// </summary>
+ public bool RefreshOnIssuerKeyNotFound { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the method used to redirect the user agent to the identity provider.
+ /// </summary>
+ public OpenIdConnectRedirectBehavior AuthenticationMethod { get; set; } = OpenIdConnectRedirectBehavior.RedirectGet;
+
+ /// <summary>
+ /// Gets or sets the 'resource'.
+ /// </summary>
+ public string Resource { get; set; }
+
+ /// <summary>
+ /// Gets or sets the 'response_mode'.
+ /// </summary>
+ public string ResponseMode { get; set; } = OpenIdConnectResponseMode.FormPost;
+
+ /// <summary>
+ /// Gets or sets the 'response_type'.
+ /// </summary>
+ public string ResponseType { get; set; } = OpenIdConnectResponseType.IdToken;
+
+ /// <summary>
+ /// Gets or sets the 'prompt'.
+ /// </summary>
+ public string Prompt { get; set; }
+
+ /// <summary>
+ /// Gets the list of permissions to request.
+ /// </summary>
+ public ICollection<string> Scope { get; } = new HashSet<string>();
+
+ /// <summary>
+ /// Requests received on this path will cause the handler to invoke SignOut using the SignInScheme.
+ /// </summary>
+ public PathString RemoteSignOutPath { get; set; }
+
+ /// <summary>
+ /// The Authentication Scheme to use with SignOut on the SignOutPath. SignInScheme will be used if this
+ /// is not set.
+ /// </summary>
+ public string SignOutScheme { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type used to secure data handled by the handler.
+ /// </summary>
+ public ISecureDataFormat<AuthenticationProperties> StateDataFormat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type used to secure strings used by the handler.
+ /// </summary>
+ public ISecureDataFormat<string> StringDataFormat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="ISecurityTokenValidator"/> used to validate identity tokens.
+ /// </summary>
+ public ISecurityTokenValidator SecurityTokenValidator { get; set; } = new JwtSecurityTokenHandler();
+
+ /// <summary>
+ /// Gets or sets the parameters used to validate identity tokens.
+ /// </summary>
+ /// <remarks>Contains the types and definitions required for validating a token.</remarks>
+ public TokenValidationParameters TokenValidationParameters { get; set; } = new TokenValidationParameters();
+
+ /// <summary>
+ /// Indicates that the authentication session lifetime (e.g. cookies) should match that of the authentication token.
+ /// If the token does not provide lifetime information then normal session lifetimes will be used.
+ /// This is disabled by default.
+ /// </summary>
+ public bool UseTokenLifetime { get; set; }
+
+ /// <summary>
+ /// Indicates if requests to the CallbackPath may also be for other components. If enabled the handler will pass
+ /// requests through that do not contain OpenIdConnect authentication responses. Disabling this and setting the
+ /// CallbackPath to a dedicated endpoint may provide better error handling.
+ /// This is disabled by default.
+ /// </summary>
+ public bool SkipUnrecognizedRequests { get; set; } = false;
+
+ /// <summary>
+ /// Indicates whether telemetry should be disabled. When this feature is enabled,
+ /// the assembly version of the Microsoft IdentityModel packages is sent to the
+ /// remote OpenID Connect provider as an authorization/logout request parameter.
+ /// </summary>
+ public bool DisableTelemetry { get; set; }
+
+ /// <summary>
+ /// Determines the settings used to create the nonce cookie before the
+ /// cookie gets added to the response.
+ /// </summary>
+ /// <remarks>
+ /// The value of <see cref="CookieBuilder.Name"/> is treated as the prefix to the cookie name, and defaults to <seealso cref="OpenIdConnectDefaults.CookieNoncePrefix"/>.
+ /// </remarks>
+ public CookieBuilder NonceCookie
+ {
+ get => _nonceCookieBuilder;
+ set => _nonceCookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ private class OpenIdConnectNonceCookieBuilder : RequestPathBaseCookieBuilder
+ {
+ private readonly OpenIdConnectOptions _options;
+
+ public OpenIdConnectNonceCookieBuilder(OpenIdConnectOptions oidcOptions)
+ {
+ _options = oidcOptions;
+ }
+
+ protected override string AdditionalPath => _options.CallbackPath;
+
+ public override CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom)
+ {
+ var cookieOptions = base.Build(context, expiresFrom);
+
+ if (!Expiration.HasValue || !cookieOptions.Expires.HasValue)
+ {
+ cookieOptions.Expires = expiresFrom.Add(_options.ProtocolValidator.NonceLifetime);
+ }
+
+ return cookieOptions;
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectPostConfigureOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectPostConfigureOptions.cs
new file mode 100644
index 0000000000..b79f1d1edf
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectPostConfigureOptions.cs
@@ -0,0 +1,114 @@
+// 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.Net.Http;
+using System.Text;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Protocols;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ /// <summary>
+ /// Used to setup defaults for all <see cref="OpenIdConnectOptions"/>.
+ /// </summary>
+ public class OpenIdConnectPostConfigureOptions : IPostConfigureOptions<OpenIdConnectOptions>
+ {
+ private readonly IDataProtectionProvider _dp;
+
+ public OpenIdConnectPostConfigureOptions(IDataProtectionProvider dataProtection)
+ {
+ _dp = dataProtection;
+ }
+
+ /// <summary>
+ /// Invoked to post configure a TOptions instance.
+ /// </summary>
+ /// <param name="name">The name of the options instance being configured.</param>
+ /// <param name="options">The options instance to configure.</param>
+ public void PostConfigure(string name, OpenIdConnectOptions options)
+ {
+ options.DataProtectionProvider = options.DataProtectionProvider ?? _dp;
+
+ if (string.IsNullOrEmpty(options.SignOutScheme))
+ {
+ options.SignOutScheme = options.SignInScheme;
+ }
+
+ if (options.StateDataFormat == null)
+ {
+ var dataProtector = options.DataProtectionProvider.CreateProtector(
+ typeof(OpenIdConnectHandler).FullName, name, "v1");
+ options.StateDataFormat = new PropertiesDataFormat(dataProtector);
+ }
+
+ if (options.StringDataFormat == null)
+ {
+ var dataProtector = options.DataProtectionProvider.CreateProtector(
+ typeof(OpenIdConnectHandler).FullName,
+ typeof(string).FullName,
+ name,
+ "v1");
+
+ options.StringDataFormat = new SecureDataFormat<string>(new StringSerializer(), dataProtector);
+ }
+
+ if (string.IsNullOrEmpty(options.TokenValidationParameters.ValidAudience) && !string.IsNullOrEmpty(options.ClientId))
+ {
+ options.TokenValidationParameters.ValidAudience = options.ClientId;
+ }
+
+ if (options.Backchannel == null)
+ {
+ options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler());
+ options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OpenIdConnect handler");
+ options.Backchannel.Timeout = options.BackchannelTimeout;
+ options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
+ }
+
+ if (options.ConfigurationManager == null)
+ {
+ if (options.Configuration != null)
+ {
+ options.ConfigurationManager = new StaticConfigurationManager<OpenIdConnectConfiguration>(options.Configuration);
+ }
+ else if (!(string.IsNullOrEmpty(options.MetadataAddress) && string.IsNullOrEmpty(options.Authority)))
+ {
+ if (string.IsNullOrEmpty(options.MetadataAddress) && !string.IsNullOrEmpty(options.Authority))
+ {
+ options.MetadataAddress = options.Authority;
+ if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal))
+ {
+ options.MetadataAddress += "/";
+ }
+
+ options.MetadataAddress += ".well-known/openid-configuration";
+ }
+
+ if (options.RequireHttpsMetadata && !options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false.");
+ }
+
+ options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.MetadataAddress, new OpenIdConnectConfigurationRetriever(),
+ new HttpDocumentRetriever(options.Backchannel) { RequireHttps = options.RequireHttpsMetadata });
+ }
+ }
+ }
+
+ private class StringSerializer : IDataSerializer<string>
+ {
+ public string Deserialize(byte[] data)
+ {
+ return Encoding.UTF8.GetString(data);
+ }
+
+ public byte[] Serialize(string model)
+ {
+ return Encoding.UTF8.GetBytes(model);
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectRedirectBehavior.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectRedirectBehavior.cs
new file mode 100644
index 0000000000..2f419df18a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectRedirectBehavior.cs
@@ -0,0 +1,21 @@
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ /// <summary>
+ /// Lists the different authentication methods used to
+ /// redirect the user agent to the identity provider.
+ /// </summary>
+ public enum OpenIdConnectRedirectBehavior
+ {
+ /// <summary>
+ /// Emits a 302 response to redirect the user agent to
+ /// the OpenID Connect provider using a GET request.
+ /// </summary>
+ RedirectGet = 0,
+
+ /// <summary>
+ /// Emits an HTML form to redirect the user agent to
+ /// the OpenID Connect provider using a POST request.
+ /// </summary>
+ FormPost = 1
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..753373ece4
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/Resources.Designer.cs
@@ -0,0 +1,114 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Authentication.OpenIdConnect.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// OpenIdConnectAuthenticationHandler: message.State is null or empty.
+ /// </summary>
+ internal static string MessageStateIsNullOrEmpty
+ {
+ get => GetString("MessageStateIsNullOrEmpty");
+ }
+
+ /// <summary>
+ /// OpenIdConnectAuthenticationHandler: message.State is null or empty.
+ /// </summary>
+ internal static string FormatMessageStateIsNullOrEmpty()
+ => GetString("MessageStateIsNullOrEmpty");
+
+ /// <summary>
+ /// Unable to unprotect the message.State.
+ /// </summary>
+ internal static string MessageStateIsInvalid
+ {
+ get => GetString("MessageStateIsInvalid");
+ }
+
+ /// <summary>
+ /// Unable to unprotect the message.State.
+ /// </summary>
+ internal static string FormatMessageStateIsInvalid()
+ => GetString("MessageStateIsInvalid");
+
+ /// <summary>
+ /// Message contains error: '{0}', error_description: '{1}', error_uri: '{2}'.
+ /// </summary>
+ internal static string MessageContainsError
+ {
+ get => GetString("MessageContainsError");
+ }
+
+ /// <summary>
+ /// Message contains error: '{0}', error_description: '{1}', error_uri: '{2}'.
+ /// </summary>
+ internal static string FormatMessageContainsError(object p0, object p1, object p2)
+ => string.Format(CultureInfo.CurrentCulture, GetString("MessageContainsError"), p0, p1, p2);
+
+ /// <summary>
+ /// The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{0}'.
+ /// </summary>
+ internal static string ValidatedSecurityTokenNotJwt
+ {
+ get => GetString("ValidatedSecurityTokenNotJwt");
+ }
+
+ /// <summary>
+ /// The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{0}'.
+ /// </summary>
+ internal static string FormatValidatedSecurityTokenNotJwt(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("ValidatedSecurityTokenNotJwt"), p0);
+
+ /// <summary>
+ /// Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{0}'."
+ /// </summary>
+ internal static string UnableToValidateToken
+ {
+ get => GetString("UnableToValidateToken");
+ }
+
+ /// <summary>
+ /// Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{0}'."
+ /// </summary>
+ internal static string FormatUnableToValidateToken(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("UnableToValidateToken"), p0);
+
+ /// <summary>
+ /// Cannot process the message. Both id_token and code are missing.
+ /// </summary>
+ internal static string IdTokenCodeMissing
+ {
+ get => GetString("IdTokenCodeMissing");
+ }
+
+ /// <summary>
+ /// Cannot process the message. Both id_token and code are missing.
+ /// </summary>
+ internal static string FormatIdTokenCodeMissing()
+ => GetString("IdTokenCodeMissing");
+
+ 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/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Resources.resx
new file mode 100644
index 0000000000..7f790fef43
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Resources.resx
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="MessageStateIsNullOrEmpty" xml:space="preserve">
+ <value>OpenIdConnectAuthenticationHandler: message.State is null or empty.</value>
+ </data>
+ <data name="MessageStateIsInvalid" xml:space="preserve">
+ <value>Unable to unprotect the message.State.</value>
+ </data>
+ <data name="MessageContainsError" xml:space="preserve">
+ <value>Message contains error: '{0}', error_description: '{1}', error_uri: '{2}'.</value>
+ </data>
+ <data name="ValidatedSecurityTokenNotJwt" xml:space="preserve">
+ <value>The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{0}'.</value>
+ </data>
+ <data name="UnableToValidateToken" xml:space="preserve">
+ <value>Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{0}'."</value>
+ </data>
+ <data name="IdTokenCodeMissing" xml:space="preserve">
+ <value>Cannot process the message. Both id_token and code are missing.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json
new file mode 100644
index 0000000000..d5c10d18db
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json
@@ -0,0 +1,2452 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.OpenIdConnect, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.OpenIdConnectExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddOpenIdConnect",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddOpenIdConnect",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddOpenIdConnect",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddOpenIdConnect",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.OpenIdConnectAppBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseOpenIdConnectAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseOpenIdConnectAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.ClaimActionCollectionUniqueExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "MapUniqueJsonKey",
+ "Parameters": [
+ {
+ "Name": "collection",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection"
+ },
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "jsonKey",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MapUniqueJsonKey",
+ "Parameters": [
+ {
+ "Name": "collection",
+ "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection"
+ },
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "jsonKey",
+ "Type": "System.String"
+ },
+ {
+ "Name": "valueType",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthenticationFailedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Exception",
+ "Parameters": [],
+ "ReturnType": "System.Exception",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Exception",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Exception"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthorizationCodeReceivedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_JwtSecurityToken",
+ "Parameters": [],
+ "ReturnType": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_JwtSecurityToken",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TokenEndpointRequest",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_TokenEndpointRequest",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Backchannel",
+ "Parameters": [],
+ "ReturnType": "System.Net.Http.HttpClient",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TokenEndpointResponse",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_TokenEndpointResponse",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_HandledCodeRedemption",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleCodeRedemption",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleCodeRedemption",
+ "Parameters": [
+ {
+ "Name": "accessToken",
+ "Type": "System.String"
+ },
+ {
+ "Name": "idToken",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleCodeRedemption",
+ "Parameters": [
+ {
+ "Name": "tokenEndpointResponse",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.MessageReceivedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Token",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Token",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_OnAuthenticationFailed",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthenticationFailedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnAuthenticationFailed",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthenticationFailedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnAuthorizationCodeReceived",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthorizationCodeReceivedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnAuthorizationCodeReceived",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthorizationCodeReceivedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnMessageReceived",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.MessageReceivedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnMessageReceived",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.MessageReceivedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnRedirectToIdentityProvider",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.RedirectContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnRedirectToIdentityProvider",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.RedirectContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnRedirectToIdentityProviderForSignOut",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.RedirectContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnRedirectToIdentityProviderForSignOut",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.RedirectContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnSignedOutCallbackRedirect",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.RemoteSignOutContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnSignedOutCallbackRedirect",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.RemoteSignOutContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnRemoteSignOut",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.RemoteSignOutContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnRemoteSignOut",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.RemoteSignOutContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnTokenResponseReceived",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenResponseReceivedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnTokenResponseReceived",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenResponseReceivedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnTokenValidated",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnTokenValidated",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnUserInformationReceived",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.UserInformationReceivedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnUserInformationReceived",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.OpenIdConnect.UserInformationReceivedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AuthenticationFailed",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthenticationFailedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AuthorizationCodeReceived",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthorizationCodeReceivedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MessageReceived",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.MessageReceivedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RedirectToIdentityProvider",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RedirectContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RedirectToIdentityProviderForSignOut",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RedirectContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SignedOutCallbackRedirect",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RemoteSignOutContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RemoteSignOut",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RemoteSignOutContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "TokenResponseReceived",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenResponseReceivedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "TokenValidated",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UserInformationReceived",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.UserInformationReceivedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RedirectContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Handled",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleResponse",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RemoteSignOutContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions"
+ },
+ {
+ "Name": "message",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenResponseReceivedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TokenEndpointResponse",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_TokenEndpointResponse",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions"
+ },
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SecurityToken",
+ "Parameters": [],
+ "ReturnType": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SecurityToken",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TokenEndpointResponse",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_TokenEndpointResponse",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Nonce",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Nonce",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions"
+ },
+ {
+ "Name": "principal",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.UserInformationReceivedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_User",
+ "Parameters": [],
+ "ReturnType": "Newtonsoft.Json.Linq.JObject",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_User",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions"
+ },
+ {
+ "Name": "principal",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectChallengeProperties",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthChallengeProperties",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_MaxAge",
+ "Parameters": [],
+ "ReturnType": "System.Nullable<System.TimeSpan>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_MaxAge",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Nullable<System.TimeSpan>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Prompt",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Prompt",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "items",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.String>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "items",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.String>"
+ },
+ {
+ "Name": "parameters",
+ "Type": "System.Collections.Generic.IDictionary<System.String, System.Object>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "MaxAgeKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "PromptKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "AuthenticationPropertiesKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "DisplayName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "CookieNoncePrefix",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "RedirectUriForCodePropertiesKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "UserstatePropertiesKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AuthenticationScheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": [],
+ "Constant": true,
+ "Literal": "\"OpenIdConnect\""
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "HandleRequestAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Boolean>",
+ "Virtual": true,
+ "Override": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Backchannel",
+ "Parameters": [],
+ "ReturnType": "System.Net.Http.HttpClient",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_HtmlEncoder",
+ "Parameters": [],
+ "ReturnType": "System.Text.Encodings.Web.HtmlEncoder",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateEventsAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Object>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleRemoteSignOutAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Boolean>",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SignOutAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleSignOutCallbackAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Boolean>",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleChallengeAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleRemoteAuthenticateAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.HandleRequestResult>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RedeemAuthorizationCodeAsync",
+ "Parameters": [
+ {
+ "Name": "tokenEndpointRequest",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage>",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetUserInformationAsync",
+ "Parameters": [
+ {
+ "Name": "message",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage"
+ },
+ {
+ "Name": "jwt",
+ "Type": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken"
+ },
+ {
+ "Name": "principal",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.HandleRequestResult>",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "htmlEncoder",
+ "Type": "System.Text.Encodings.Web.HtmlEncoder"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Validate",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Authority",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Authority",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ClientId",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ClientId",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ClientSecret",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ClientSecret",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Configuration",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Configuration",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ConfigurationManager",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.IConfigurationManager<Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ConfigurationManager",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.IConfigurationManager<Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_GetClaimsFromUserInfoEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_GetClaimsFromUserInfoEndpoint",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ClaimActions",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RequireHttpsMetadata",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RequireHttpsMetadata",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_MetadataAddress",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_MetadataAddress",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_MaxAge",
+ "Parameters": [],
+ "ReturnType": "System.Nullable<System.TimeSpan>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_MaxAge",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Nullable<System.TimeSpan>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolValidator",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolValidator",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolValidator",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolValidator"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SignedOutCallbackPath",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SignedOutCallbackPath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.PathString"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SignedOutRedirectUri",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SignedOutRedirectUri",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RefreshOnIssuerKeyNotFound",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RefreshOnIssuerKeyNotFound",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AuthenticationMethod",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectRedirectBehavior",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AuthenticationMethod",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectRedirectBehavior"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Resource",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Resource",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ResponseMode",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ResponseMode",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ResponseType",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ResponseType",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Prompt",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Prompt",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Scope",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.ICollection<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RemoteSignOutPath",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RemoteSignOutPath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.PathString"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SignOutScheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SignOutScheme",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_StateDataFormat",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationProperties>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_StateDataFormat",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationProperties>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_StringDataFormat",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_StringDataFormat",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<System.String>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SecurityTokenValidator",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Tokens.ISecurityTokenValidator",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SecurityTokenValidator",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Tokens.ISecurityTokenValidator"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TokenValidationParameters",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Tokens.TokenValidationParameters",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_TokenValidationParameters",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Tokens.TokenValidationParameters"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_UseTokenLifetime",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_UseTokenLifetime",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SkipUnrecognizedRequests",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SkipUnrecognizedRequests",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_DisableTelemetry",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_DisableTelemetry",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_NonceCookie",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_NonceCookie",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.CookieBuilder"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectPostConfigureOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.Extensions.Options.IPostConfigureOptions<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "PostConfigure",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "dataProtection",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectRedirectBehavior",
+ "Visibility": "Public",
+ "Kind": "Enumeration",
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "RedirectGet",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "0"
+ },
+ {
+ "Kind": "Field",
+ "Name": "FormPost",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "1"
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.Claims.UniqueJsonKeyClaimAction",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.JsonKeyClaimAction",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Run",
+ "Parameters": [
+ {
+ "Name": "userData",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ },
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ },
+ {
+ "Name": "issuer",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "valueType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "jsonKey",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs
new file mode 100644
index 0000000000..67f28d5297
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs
@@ -0,0 +1,77 @@
+// 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 Microsoft.AspNetCore.Http;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.Twitter
+{
+ /// <summary>
+ /// Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.
+ /// </summary>
+ public class TwitterCreatingTicketContext : ResultContext<TwitterOptions>
+ {
+ /// <summary>
+ /// Initializes a <see cref="TwitterCreatingTicketContext"/>
+ /// </summary>
+ /// <param name="context">The HTTP environment</param>
+ /// <param name="scheme">The scheme data</param>
+ /// <param name="options">The options for Twitter</param>
+ /// <param name="principal">The <see cref="ClaimsPrincipal"/>.</param>
+ /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
+ /// <param name="userId">Twitter user ID</param>
+ /// <param name="screenName">Twitter screen name</param>
+ /// <param name="accessToken">Twitter access token</param>
+ /// <param name="accessTokenSecret">Twitter access token secret</param>
+ /// <param name="user">User details</param>
+ public TwitterCreatingTicketContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ TwitterOptions options,
+ ClaimsPrincipal principal,
+ AuthenticationProperties properties,
+ string userId,
+ string screenName,
+ string accessToken,
+ string accessTokenSecret,
+ JObject user)
+ : base(context, scheme, options)
+ {
+ UserId = userId;
+ ScreenName = screenName;
+ AccessToken = accessToken;
+ AccessTokenSecret = accessTokenSecret;
+ User = user ?? new JObject();
+ Principal = principal;
+ Properties = properties;
+ }
+
+ /// <summary>
+ /// Gets the Twitter user ID
+ /// </summary>
+ public string UserId { get; }
+
+ /// <summary>
+ /// Gets the Twitter screen name
+ /// </summary>
+ public string ScreenName { get; }
+
+ /// <summary>
+ /// Gets the Twitter access token
+ /// </summary>
+ public string AccessToken { get; }
+
+ /// <summary>
+ /// Gets the Twitter access token secret
+ /// </summary>
+ public string AccessTokenSecret { get; }
+
+ /// <summary>
+ /// Gets the JSON-serialized user or an empty
+ /// <see cref="JObject"/> if it is not available.
+ /// </summary>
+ public JObject User { get; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterEvents.cs
new file mode 100644
index 0000000000..744c48c5fc
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterEvents.cs
@@ -0,0 +1,41 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authentication.Twitter
+{
+ /// <summary>
+ /// Default <see cref="TwitterEvents"/> implementation.
+ /// </summary>
+ public class TwitterEvents : RemoteAuthenticationEvents
+ {
+ /// <summary>
+ /// Gets or sets the function that is invoked when the Authenticated method is invoked.
+ /// </summary>
+ public Func<TwitterCreatingTicketContext, Task> OnCreatingTicket { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked.
+ /// </summary>
+ public Func<RedirectContext<TwitterOptions>, Task> OnRedirectToAuthorizationEndpoint { get; set; } = context =>
+ {
+ context.Response.Redirect(context.RedirectUri);
+ return Task.CompletedTask;
+ };
+
+ /// <summary>
+ /// Invoked whenever Twitter successfully authenticates a user
+ /// </summary>
+ /// <param name="context">Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.</param>
+ /// <returns>A <see cref="Task"/> representing the completed operation.</returns>
+ public virtual Task CreatingTicket(TwitterCreatingTicketContext context) => OnCreatingTicket(context);
+
+ /// <summary>
+ /// Called when a Challenge causes a redirect to authorize endpoint in the Twitter handler
+ /// </summary>
+ /// <param name="context">Contains redirect URI and <see cref="Http.Authentication.AuthenticationProperties"/> of the challenge </param>
+ public virtual Task RedirectToAuthorizationEndpoint(RedirectContext<TwitterOptions> context) => OnRedirectToAuthorizationEndpoint(context);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/LoggingExtensions.cs
new file mode 100644
index 0000000000..2a2cd5da79
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/LoggingExtensions.cs
@@ -0,0 +1,46 @@
+// 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.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static Action<ILogger, Exception> _obtainRequestToken;
+ private static Action<ILogger, Exception> _obtainAccessToken;
+ private static Action<ILogger, Exception> _retrieveUserDetails;
+
+ static LoggingExtensions()
+ {
+ _obtainRequestToken = LoggerMessage.Define(
+ eventId: 1,
+ logLevel: LogLevel.Debug,
+ formatString: "ObtainRequestToken");
+ _obtainAccessToken = LoggerMessage.Define(
+ eventId: 2,
+ logLevel: LogLevel.Debug,
+ formatString: "ObtainAccessToken");
+ _retrieveUserDetails = LoggerMessage.Define(
+ eventId: 3,
+ logLevel: LogLevel.Debug,
+ formatString: "RetrieveUserDetails");
+
+ }
+
+ public static void ObtainAccessToken(this ILogger logger)
+ {
+ _obtainAccessToken(logger, null);
+ }
+
+ public static void ObtainRequestToken(this ILogger logger)
+ {
+ _obtainRequestToken(logger, null);
+ }
+
+ public static void RetrieveUserDetails(this ILogger logger)
+ {
+ _retrieveUserDetails(logger, null);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/AccessToken.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/AccessToken.cs
new file mode 100644
index 0000000000..550163bec8
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/AccessToken.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.
+
+namespace Microsoft.AspNetCore.Authentication.Twitter
+{
+ /// <summary>
+ /// The Twitter access token retrieved from the access token endpoint.
+ /// </summary>
+ public class AccessToken : RequestToken
+ {
+ /// <summary>
+ /// Gets or sets the Twitter User ID.
+ /// </summary>
+ public string UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Twitter screen name.
+ /// </summary>
+ public string ScreenName { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestToken.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestToken.cs
new file mode 100644
index 0000000000..04c334e3d3
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestToken.cs
@@ -0,0 +1,30 @@
+// 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.Authentication;
+
+namespace Microsoft.AspNetCore.Authentication.Twitter
+{
+ /// <summary>
+ /// The Twitter request token obtained from the request token endpoint.
+ /// </summary>
+ public class RequestToken
+ {
+ /// <summary>
+ /// Gets or sets the Twitter request token.
+ /// </summary>
+ public string Token { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Twitter token secret.
+ /// </summary>
+ public string TokenSecret { get; set; }
+
+ public bool CallbackConfirmed { get; set; }
+
+ /// <summary>
+ /// Gets or sets a property bag for common authentication properties.
+ /// </summary>
+ public AuthenticationProperties Properties { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestTokenSerializer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestTokenSerializer.cs
new file mode 100644
index 0000000000..88b10d3d60
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Messages/RequestTokenSerializer.cs
@@ -0,0 +1,104 @@
+// 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.Http.Authentication;
+
+namespace Microsoft.AspNetCore.Authentication.Twitter
+{
+ /// <summary>
+ /// Serializes and deserializes Twitter request and access tokens so that they can be used by other application components.
+ /// </summary>
+ public class RequestTokenSerializer : IDataSerializer<RequestToken>
+ {
+ private const int FormatVersion = 1;
+
+ /// <summary>
+ /// Serialize a request token.
+ /// </summary>
+ /// <param name="model">The token to serialize</param>
+ /// <returns>A byte array containing the serialized token</returns>
+ public virtual byte[] Serialize(RequestToken model)
+ {
+ using (var memory = new MemoryStream())
+ {
+ using (var writer = new BinaryWriter(memory))
+ {
+ Write(writer, model);
+ writer.Flush();
+ return memory.ToArray();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Deserializes a request token.
+ /// </summary>
+ /// <param name="data">A byte array containing the serialized token</param>
+ /// <returns>The Twitter request token</returns>
+ public virtual RequestToken Deserialize(byte[] data)
+ {
+ using (var memory = new MemoryStream(data))
+ {
+ using (var reader = new BinaryReader(memory))
+ {
+ return Read(reader);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Writes a Twitter request token as a series of bytes. Used by the <see cref="Serialize"/> method.
+ /// </summary>
+ /// <param name="writer">The writer to use in writing the token</param>
+ /// <param name="token">The token to write</param>
+ public static void Write(BinaryWriter writer, RequestToken token)
+ {
+ if (writer == null)
+ {
+ throw new ArgumentNullException(nameof(writer));
+ }
+
+ if (token == null)
+ {
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ writer.Write(FormatVersion);
+ writer.Write(token.Token);
+ writer.Write(token.TokenSecret);
+ writer.Write(token.CallbackConfirmed);
+ PropertiesSerializer.Default.Write(writer, token.Properties);
+ }
+
+ /// <summary>
+ /// Reads a Twitter request token from a series of bytes. Used by the <see cref="Deserialize"/> method.
+ /// </summary>
+ /// <param name="reader">The reader to use in reading the token bytes</param>
+ /// <returns>The token</returns>
+ public static RequestToken Read(BinaryReader reader)
+ {
+ if (reader == null)
+ {
+ throw new ArgumentNullException(nameof(reader));
+ }
+
+ if (reader.ReadInt32() != FormatVersion)
+ {
+ return null;
+ }
+
+ string token = reader.ReadString();
+ string tokenSecret = reader.ReadString();
+ bool callbackConfirmed = reader.ReadBoolean();
+ AuthenticationProperties properties = PropertiesSerializer.Default.Read(reader);
+ if (properties == null)
+ {
+ return null;
+ }
+
+ return new RequestToken { Token = token, TokenSecret = tokenSecret, CallbackConfirmed = callbackConfirmed, Properties = properties };
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj
new file mode 100644
index 0000000000..f720d08f04
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core middleware that enables an application to support Twitter's OAuth 1.0 authentication workflow.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;authentication;security</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.Authentication.OAuth\Microsoft.AspNetCore.Authentication.OAuth.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..2eabfff298
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/Resources.Designer.cs
@@ -0,0 +1,58 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Authentication.Twitter
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Authentication.Twitter.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string Exception_OptionMustBeProvided
+ {
+ get => GetString("Exception_OptionMustBeProvided");
+ }
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string FormatException_OptionMustBeProvided(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0);
+
+ /// <summary>
+ /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.
+ /// </summary>
+ internal static string Exception_ValidatorHandlerMismatch
+ {
+ get => GetString("Exception_ValidatorHandlerMismatch");
+ }
+
+ /// <summary>
+ /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.
+ /// </summary>
+ internal static string FormatException_ValidatorHandlerMismatch()
+ => GetString("Exception_ValidatorHandlerMismatch");
+
+ 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/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Resources.resx
new file mode 100644
index 0000000000..2a19bea96a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/Resources.resx
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Exception_OptionMustBeProvided" xml:space="preserve">
+ <value>The '{0}' option must be provided.</value>
+ </data>
+ <data name="Exception_ValidatorHandlerMismatch" xml:space="preserve">
+ <value>An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterAppBuilderExtensions.cs
new file mode 100644
index 0000000000..36e1111da6
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterAppBuilderExtensions.cs
@@ -0,0 +1,37 @@
+// 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.Authentication.Twitter;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Extension methods to add Twitter authentication capabilities to an HTTP application pipeline.
+ /// </summary>
+ public static class TwitterAppBuilderExtensions
+ {
+ /// <summary>
+ /// UseTwitterAuthentication is obsolete. Configure Twitter authentication with AddAuthentication().AddTwitter in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseTwitterAuthentication is obsolete. Configure Twitter authentication with AddAuthentication().AddTwitter in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseTwitterAuthentication(this IApplicationBuilder app)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+
+ /// <summary>
+ /// UseTwitterAuthentication is obsolete. Configure Twitter authentication with AddAuthentication().AddTwitter in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <param name="options">An action delegate to configure the provided <see cref="TwitterOptions"/>.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ [Obsolete("UseTwitterAuthentication is obsolete. Configure Twitter authentication with AddAuthentication().AddTwitter in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)]
+ public static IApplicationBuilder UseTwitterAuthentication(this IApplicationBuilder app, TwitterOptions options)
+ {
+ throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs
new file mode 100644
index 0000000000..a39a3f0367
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.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.Authentication.Twitter
+{
+ public static class TwitterDefaults
+ {
+ public const string AuthenticationScheme = "Twitter";
+
+ public static readonly string DisplayName = "Twitter";
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterExtensions.cs
new file mode 100644
index 0000000000..7243805692
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterExtensions.cs
@@ -0,0 +1,29 @@
+// 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.Authentication;
+using Microsoft.AspNetCore.Authentication.Twitter;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class TwitterExtensions
+ {
+ public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder)
+ => builder.AddTwitter(TwitterDefaults.AuthenticationScheme, _ => { });
+
+ public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, Action<TwitterOptions> configureOptions)
+ => builder.AddTwitter(TwitterDefaults.AuthenticationScheme, configureOptions);
+
+ public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, Action<TwitterOptions> configureOptions)
+ => builder.AddTwitter(authenticationScheme, TwitterDefaults.DisplayName, configureOptions);
+
+ public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TwitterOptions> configureOptions)
+ {
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TwitterOptions>, TwitterPostConfigureOptions>());
+ return builder.AddRemoteScheme<TwitterOptions, TwitterHandler>(authenticationScheme, displayName, configureOptions);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs
new file mode 100644
index 0000000000..670e76f7e3
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs
@@ -0,0 +1,371 @@
+// 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.Globalization;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.Twitter
+{
+ public class TwitterHandler : RemoteAuthenticationHandler<TwitterOptions>
+ {
+ private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ private const string RequestTokenEndpoint = "https://api.twitter.com/oauth/request_token";
+ private const string AuthenticationEndpoint = "https://api.twitter.com/oauth/authenticate?oauth_token=";
+ private const string AccessTokenEndpoint = "https://api.twitter.com/oauth/access_token";
+
+ private HttpClient Backchannel => Options.Backchannel;
+
+ /// <summary>
+ /// The handler calls methods on the events which give the application control at certain points where processing is occurring.
+ /// If it is not provided a default instance is supplied which does nothing when the methods are called.
+ /// </summary>
+ protected new TwitterEvents Events
+ {
+ get { return (TwitterEvents)base.Events; }
+ set { base.Events = value; }
+ }
+
+ public TwitterHandler(IOptionsMonitor<TwitterOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
+ : base(options, logger, encoder, clock)
+ { }
+
+ protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new TwitterEvents());
+
+ protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
+ {
+ var query = Request.Query;
+ var protectedRequestToken = Request.Cookies[Options.StateCookie.Name];
+
+ var requestToken = Options.StateDataFormat.Unprotect(protectedRequestToken);
+
+ if (requestToken == null)
+ {
+ return HandleRequestResult.Fail("Invalid state cookie.");
+ }
+
+ var properties = requestToken.Properties;
+
+ // REVIEW: see which of these are really errors
+
+ var denied = query["denied"];
+ if (!StringValues.IsNullOrEmpty(denied))
+ {
+ return HandleRequestResult.Fail("The user denied permissions.", properties);
+ }
+
+ var returnedToken = query["oauth_token"];
+ if (StringValues.IsNullOrEmpty(returnedToken))
+ {
+ return HandleRequestResult.Fail("Missing oauth_token", properties);
+ }
+
+ if (!string.Equals(returnedToken, requestToken.Token, StringComparison.Ordinal))
+ {
+ return HandleRequestResult.Fail("Unmatched token", properties);
+ }
+
+ var oauthVerifier = query["oauth_verifier"];
+ if (StringValues.IsNullOrEmpty(oauthVerifier))
+ {
+ return HandleRequestResult.Fail("Missing or blank oauth_verifier", properties);
+ }
+
+ var cookieOptions = Options.StateCookie.Build(Context, Clock.UtcNow);
+
+ Response.Cookies.Delete(Options.StateCookie.Name, cookieOptions);
+
+ var accessToken = await ObtainAccessTokenAsync(requestToken, oauthVerifier);
+
+ var identity = new ClaimsIdentity(new[]
+ {
+ new Claim(ClaimTypes.NameIdentifier, accessToken.UserId, ClaimValueTypes.String, ClaimsIssuer),
+ new Claim(ClaimTypes.Name, accessToken.ScreenName, ClaimValueTypes.String, ClaimsIssuer),
+ new Claim("urn:twitter:userid", accessToken.UserId, ClaimValueTypes.String, ClaimsIssuer),
+ new Claim("urn:twitter:screenname", accessToken.ScreenName, ClaimValueTypes.String, ClaimsIssuer)
+ },
+ ClaimsIssuer);
+
+ JObject user = null;
+ if (Options.RetrieveUserDetails)
+ {
+ user = await RetrieveUserDetailsAsync(accessToken, identity);
+ }
+
+ if (Options.SaveTokens)
+ {
+ properties.StoreTokens(new [] {
+ new AuthenticationToken { Name = "access_token", Value = accessToken.Token },
+ new AuthenticationToken { Name = "access_token_secret", Value = accessToken.TokenSecret }
+ });
+ }
+
+ return HandleRequestResult.Success(await CreateTicketAsync(identity, properties, accessToken, user));
+ }
+
+ protected virtual async Task<AuthenticationTicket> CreateTicketAsync(
+ ClaimsIdentity identity, AuthenticationProperties properties, AccessToken token, JObject user)
+ {
+ foreach (var action in Options.ClaimActions)
+ {
+ action.Run(user, identity, ClaimsIssuer);
+ }
+
+ var context = new TwitterCreatingTicketContext(Context, Scheme, Options, new ClaimsPrincipal(identity), properties, token.UserId, token.ScreenName, token.Token, token.TokenSecret, user);
+ await Events.CreatingTicket(context);
+
+ return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
+ }
+
+ protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
+ {
+ if (string.IsNullOrEmpty(properties.RedirectUri))
+ {
+ properties.RedirectUri = CurrentUri;
+ }
+
+ // If CallbackConfirmed is false, this will throw
+ var requestToken = await ObtainRequestTokenAsync(BuildRedirectUri(Options.CallbackPath), properties);
+ var twitterAuthenticationEndpoint = AuthenticationEndpoint + requestToken.Token;
+
+ var cookieOptions = Options.StateCookie.Build(Context, Clock.UtcNow);
+
+ Response.Cookies.Append(Options.StateCookie.Name, Options.StateDataFormat.Protect(requestToken), cookieOptions);
+
+ var redirectContext = new RedirectContext<TwitterOptions>(Context, Scheme, Options, properties, twitterAuthenticationEndpoint);
+ await Events.RedirectToAuthorizationEndpoint(redirectContext);
+ }
+
+ private async Task<RequestToken> ObtainRequestTokenAsync(string callBackUri, AuthenticationProperties properties)
+ {
+ Logger.ObtainRequestToken();
+
+ var nonce = Guid.NewGuid().ToString("N");
+
+ var authorizationParts = new SortedDictionary<string, string>
+ {
+ { "oauth_callback", callBackUri },
+ { "oauth_consumer_key", Options.ConsumerKey },
+ { "oauth_nonce", nonce },
+ { "oauth_signature_method", "HMAC-SHA1" },
+ { "oauth_timestamp", GenerateTimeStamp() },
+ { "oauth_version", "1.0" }
+ };
+
+ var parameterBuilder = new StringBuilder();
+ foreach (var authorizationKey in authorizationParts)
+ {
+ parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.Encode(authorizationKey.Key), UrlEncoder.Encode(authorizationKey.Value));
+ }
+ parameterBuilder.Length--;
+ var parameterString = parameterBuilder.ToString();
+
+ var canonicalizedRequestBuilder = new StringBuilder();
+ canonicalizedRequestBuilder.Append(HttpMethod.Post.Method);
+ canonicalizedRequestBuilder.Append("&");
+ canonicalizedRequestBuilder.Append(UrlEncoder.Encode(RequestTokenEndpoint));
+ canonicalizedRequestBuilder.Append("&");
+ canonicalizedRequestBuilder.Append(UrlEncoder.Encode(parameterString));
+
+ var signature = ComputeSignature(Options.ConsumerSecret, null, canonicalizedRequestBuilder.ToString());
+ authorizationParts.Add("oauth_signature", signature);
+
+ var authorizationHeaderBuilder = new StringBuilder();
+ authorizationHeaderBuilder.Append("OAuth ");
+ foreach (var authorizationPart in authorizationParts)
+ {
+ authorizationHeaderBuilder.AppendFormat(
+ "{0}=\"{1}\", ", authorizationPart.Key, UrlEncoder.Encode(authorizationPart.Value));
+ }
+ authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2;
+
+ var request = new HttpRequestMessage(HttpMethod.Post, RequestTokenEndpoint);
+ request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString());
+
+ var response = await Backchannel.SendAsync(request, Context.RequestAborted);
+ response.EnsureSuccessStatusCode();
+ var responseText = await response.Content.ReadAsStringAsync();
+
+ var responseParameters = new FormCollection(new FormReader(responseText).ReadForm());
+ if (!string.Equals(responseParameters["oauth_callback_confirmed"], "true", StringComparison.Ordinal))
+ {
+ throw new Exception("Twitter oauth_callback_confirmed is not true.");
+ }
+
+ return new RequestToken { Token = Uri.UnescapeDataString(responseParameters["oauth_token"]), TokenSecret = Uri.UnescapeDataString(responseParameters["oauth_token_secret"]), CallbackConfirmed = true, Properties = properties };
+ }
+
+ private async Task<AccessToken> ObtainAccessTokenAsync(RequestToken token, string verifier)
+ {
+ // https://dev.twitter.com/docs/api/1/post/oauth/access_token
+
+ Logger.ObtainAccessToken();
+
+ var nonce = Guid.NewGuid().ToString("N");
+
+ var authorizationParts = new SortedDictionary<string, string>
+ {
+ { "oauth_consumer_key", Options.ConsumerKey },
+ { "oauth_nonce", nonce },
+ { "oauth_signature_method", "HMAC-SHA1" },
+ { "oauth_token", token.Token },
+ { "oauth_timestamp", GenerateTimeStamp() },
+ { "oauth_verifier", verifier },
+ { "oauth_version", "1.0" },
+ };
+
+ var parameterBuilder = new StringBuilder();
+ foreach (var authorizationKey in authorizationParts)
+ {
+ parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.Encode(authorizationKey.Key), UrlEncoder.Encode(authorizationKey.Value));
+ }
+ parameterBuilder.Length--;
+ var parameterString = parameterBuilder.ToString();
+
+ var canonicalizedRequestBuilder = new StringBuilder();
+ canonicalizedRequestBuilder.Append(HttpMethod.Post.Method);
+ canonicalizedRequestBuilder.Append("&");
+ canonicalizedRequestBuilder.Append(UrlEncoder.Encode(AccessTokenEndpoint));
+ canonicalizedRequestBuilder.Append("&");
+ canonicalizedRequestBuilder.Append(UrlEncoder.Encode(parameterString));
+
+ var signature = ComputeSignature(Options.ConsumerSecret, token.TokenSecret, canonicalizedRequestBuilder.ToString());
+ authorizationParts.Add("oauth_signature", signature);
+ authorizationParts.Remove("oauth_verifier");
+
+ var authorizationHeaderBuilder = new StringBuilder();
+ authorizationHeaderBuilder.Append("OAuth ");
+ foreach (var authorizationPart in authorizationParts)
+ {
+ authorizationHeaderBuilder.AppendFormat(
+ "{0}=\"{1}\", ", authorizationPart.Key, UrlEncoder.Encode(authorizationPart.Value));
+ }
+ authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2;
+
+ var request = new HttpRequestMessage(HttpMethod.Post, AccessTokenEndpoint);
+ request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString());
+
+ var formPairs = new Dictionary<string, string>()
+ {
+ { "oauth_verifier", verifier },
+ };
+
+ request.Content = new FormUrlEncodedContent(formPairs);
+
+ var response = await Backchannel.SendAsync(request, Context.RequestAborted);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ Logger.LogError("AccessToken request failed with a status code of " + response.StatusCode);
+ response.EnsureSuccessStatusCode(); // throw
+ }
+
+ var responseText = await response.Content.ReadAsStringAsync();
+ var responseParameters = new FormCollection(new FormReader(responseText).ReadForm());
+
+ return new AccessToken
+ {
+ Token = Uri.UnescapeDataString(responseParameters["oauth_token"]),
+ TokenSecret = Uri.UnescapeDataString(responseParameters["oauth_token_secret"]),
+ UserId = Uri.UnescapeDataString(responseParameters["user_id"]),
+ ScreenName = Uri.UnescapeDataString(responseParameters["screen_name"])
+ };
+ }
+
+ // https://dev.twitter.com/rest/reference/get/account/verify_credentials
+ private async Task<JObject> RetrieveUserDetailsAsync(AccessToken accessToken, ClaimsIdentity identity)
+ {
+ Logger.RetrieveUserDetails();
+
+ var nonce = Guid.NewGuid().ToString("N");
+
+ var authorizationParts = new SortedDictionary<string, string>
+ {
+ { "oauth_consumer_key", Options.ConsumerKey },
+ { "oauth_nonce", nonce },
+ { "oauth_signature_method", "HMAC-SHA1" },
+ { "oauth_timestamp", GenerateTimeStamp() },
+ { "oauth_token", accessToken.Token },
+ { "oauth_version", "1.0" }
+ };
+
+ var parameterBuilder = new StringBuilder();
+ foreach (var authorizationKey in authorizationParts)
+ {
+ parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.Encode(authorizationKey.Key), UrlEncoder.Encode(authorizationKey.Value));
+ }
+ parameterBuilder.Length--;
+ var parameterString = parameterBuilder.ToString();
+
+ var resource_url = "https://api.twitter.com/1.1/account/verify_credentials.json";
+ var resource_query = "include_email=true";
+ var canonicalizedRequestBuilder = new StringBuilder();
+ canonicalizedRequestBuilder.Append(HttpMethod.Get.Method);
+ canonicalizedRequestBuilder.Append("&");
+ canonicalizedRequestBuilder.Append(UrlEncoder.Encode(resource_url));
+ canonicalizedRequestBuilder.Append("&");
+ canonicalizedRequestBuilder.Append(UrlEncoder.Encode(resource_query));
+ canonicalizedRequestBuilder.Append("%26");
+ canonicalizedRequestBuilder.Append(UrlEncoder.Encode(parameterString));
+
+ var signature = ComputeSignature(Options.ConsumerSecret, accessToken.TokenSecret, canonicalizedRequestBuilder.ToString());
+ authorizationParts.Add("oauth_signature", signature);
+
+ var authorizationHeaderBuilder = new StringBuilder();
+ authorizationHeaderBuilder.Append("OAuth ");
+ foreach (var authorizationPart in authorizationParts)
+ {
+ authorizationHeaderBuilder.AppendFormat(
+ "{0}=\"{1}\", ", authorizationPart.Key, UrlEncoder.Encode(authorizationPart.Value));
+ }
+ authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2;
+
+ var request = new HttpRequestMessage(HttpMethod.Get, resource_url + "?include_email=true");
+ request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString());
+
+ var response = await Backchannel.SendAsync(request, Context.RequestAborted);
+ if (!response.IsSuccessStatusCode)
+ {
+ Logger.LogError("Email request failed with a status code of " + response.StatusCode);
+ response.EnsureSuccessStatusCode(); // throw
+ }
+ var responseText = await response.Content.ReadAsStringAsync();
+
+ var result = JObject.Parse(responseText);
+
+ return result;
+ }
+
+ private static string GenerateTimeStamp()
+ {
+ var secondsSinceUnixEpocStart = DateTime.UtcNow - Epoch;
+ return Convert.ToInt64(secondsSinceUnixEpocStart.TotalSeconds).ToString(CultureInfo.InvariantCulture);
+ }
+
+ private string ComputeSignature(string consumerSecret, string tokenSecret, string signatureData)
+ {
+ using (var algorithm = new HMACSHA1())
+ {
+ algorithm.Key = Encoding.ASCII.GetBytes(
+ string.Format(CultureInfo.InvariantCulture,
+ "{0}&{1}",
+ UrlEncoder.Encode(consumerSecret),
+ string.IsNullOrEmpty(tokenSecret) ? string.Empty : UrlEncoder.Encode(tokenSecret)));
+ var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(signatureData));
+ return Convert.ToBase64String(hash);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs
new file mode 100644
index 0000000000..03396807ee
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs
@@ -0,0 +1,128 @@
+// 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.Globalization;
+using Microsoft.AspNetCore.Authentication.OAuth.Claims;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.Twitter
+{
+ /// <summary>
+ /// Options for the Twitter authentication handler.
+ /// </summary>
+ public class TwitterOptions : RemoteAuthenticationOptions
+ {
+ private const string DefaultStateCookieName = "__TwitterState";
+
+ private CookieBuilder _stateCookieBuilder;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TwitterOptions"/> class.
+ /// </summary>
+ public TwitterOptions()
+ {
+ CallbackPath = new PathString("/signin-twitter");
+ BackchannelTimeout = TimeSpan.FromSeconds(60);
+ Events = new TwitterEvents();
+
+ ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email);
+
+ _stateCookieBuilder = new TwitterCookieBuilder(this)
+ {
+ Name = DefaultStateCookieName,
+ SecurePolicy = CookieSecurePolicy.SameAsRequest,
+ HttpOnly = true,
+ SameSite = SameSiteMode.Lax,
+ IsEssential = true,
+ };
+ }
+
+ /// <summary>
+ /// Gets or sets the consumer key used to communicate with Twitter.
+ /// </summary>
+ /// <value>The consumer key used to communicate with Twitter.</value>
+ public string ConsumerKey { get; set; }
+
+ /// <summary>
+ /// Gets or sets the consumer secret used to sign requests to Twitter.
+ /// </summary>
+ /// <value>The consumer secret used to sign requests to Twitter.</value>
+ public string ConsumerSecret { get; set; }
+
+ /// <summary>
+ /// Enables the retrieval user details during the authentication process, including
+ /// e-mail addresses. Retrieving e-mail addresses requires special permissions
+ /// from Twitter Support on a per application basis. The default is false.
+ /// See https://dev.twitter.com/rest/reference/get/account/verify_credentials
+ /// </summary>
+ public bool RetrieveUserDetails { get; set; }
+
+ /// <summary>
+ /// A collection of claim actions used to select values from the json user data and create Claims.
+ /// </summary>
+ public ClaimActionCollection ClaimActions { get; } = new ClaimActionCollection();
+
+ /// <summary>
+ /// Gets or sets the type used to secure data handled by the handler.
+ /// </summary>
+ public ISecureDataFormat<RequestToken> StateDataFormat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="TwitterEvents"/> used to handle authentication events.
+ /// </summary>
+ public new TwitterEvents Events
+ {
+ get => (TwitterEvents)base.Events;
+ set => base.Events = value;
+ }
+
+ /// <summary>
+ /// Determines the settings used to create the state cookie before the
+ /// cookie gets added to the response.
+ /// </summary>
+ public CookieBuilder StateCookie
+ {
+ get => _stateCookieBuilder;
+ set => _stateCookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ /// <summary>
+ /// Added the validate method to ensure that the customer key and customer secret values are not not empty for the twitter authentication middleware
+ /// </summary>
+ public override void Validate()
+ {
+ base.Validate();
+ if (string.IsNullOrEmpty(ConsumerKey))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ConsumerKey)), nameof(ConsumerKey));
+ }
+
+ if (string.IsNullOrEmpty(ConsumerSecret))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ConsumerSecret)), nameof(ConsumerSecret));
+ }
+ }
+
+ private class TwitterCookieBuilder : CookieBuilder
+ {
+ private readonly TwitterOptions _twitterOptions;
+
+ public TwitterCookieBuilder(TwitterOptions twitterOptions)
+ {
+ _twitterOptions = twitterOptions;
+ }
+
+ public override CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom)
+ {
+ var options = base.Build(context, expiresFrom);
+ if (!Expiration.HasValue)
+ {
+ options.Expires = expiresFrom.Add(_twitterOptions.RemoteAuthenticationTimeout);
+ }
+ return options;
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterPostConfigureOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterPostConfigureOptions.cs
new file mode 100644
index 0000000000..09db5699f9
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterPostConfigureOptions.cs
@@ -0,0 +1,51 @@
+// 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.Net.Http;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Authentication.Twitter
+{
+ /// <summary>
+ /// Used to setup defaults for all <see cref="TwitterOptions"/>.
+ /// </summary>
+ public class TwitterPostConfigureOptions : IPostConfigureOptions<TwitterOptions>
+ {
+ private readonly IDataProtectionProvider _dp;
+
+ public TwitterPostConfigureOptions(IDataProtectionProvider dataProtection)
+ {
+ _dp = dataProtection;
+ }
+
+ /// <summary>
+ /// Invoked to post configure a TOptions instance.
+ /// </summary>
+ /// <param name="name">The name of the options instance being configured.</param>
+ /// <param name="options">The options instance to configure.</param>
+ public void PostConfigure(string name, TwitterOptions options)
+ {
+ options.DataProtectionProvider = options.DataProtectionProvider ?? _dp;
+
+ if (options.StateDataFormat == null)
+ {
+ var dataProtector = options.DataProtectionProvider.CreateProtector(
+ typeof(TwitterHandler).FullName, name, "v1");
+ options.StateDataFormat = new SecureDataFormat<RequestToken>(
+ new RequestTokenSerializer(),
+ dataProtector);
+ }
+
+ if (options.Backchannel == null)
+ {
+ options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler());
+ options.Backchannel.Timeout = options.BackchannelTimeout;
+ options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
+ options.Backchannel.DefaultRequestHeaders.Accept.ParseAdd("*/*");
+ options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core Twitter handler");
+ options.Backchannel.DefaultRequestHeaders.ExpectContinue = false;
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json
new file mode 100644
index 0000000000..03ee645623
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json
@@ -0,0 +1,924 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Twitter, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.TwitterExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddTwitter",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddTwitter",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddTwitter",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddTwitter",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.TwitterAppBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseTwitterAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseTwitterAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterCreatingTicketContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext<Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_UserId",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ScreenName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AccessToken",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AccessTokenSecret",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_User",
+ "Parameters": [],
+ "ReturnType": "Newtonsoft.Json.Linq.JObject",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions"
+ },
+ {
+ "Name": "principal",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "userId",
+ "Type": "System.String"
+ },
+ {
+ "Name": "screenName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "accessToken",
+ "Type": "System.String"
+ },
+ {
+ "Name": "accessTokenSecret",
+ "Type": "System.String"
+ },
+ {
+ "Name": "user",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_OnCreatingTicket",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.Twitter.TwitterCreatingTicketContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnCreatingTicket",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.Twitter.TwitterCreatingTicketContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnRedirectToAuthorizationEndpoint",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions>, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnRedirectToAuthorizationEndpoint",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions>, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreatingTicket",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterCreatingTicketContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RedirectToAuthorizationEndpoint",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.RedirectContext<Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions>"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Twitter.AccessToken",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_UserId",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_UserId",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ScreenName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ScreenName",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Token",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Token",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TokenSecret",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_TokenSecret",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CallbackConfirmed",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CallbackConfirmed",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Properties",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Properties",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Twitter.RequestTokenSerializer",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authentication.IDataSerializer<Microsoft.AspNetCore.Authentication.Twitter.RequestToken>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Serialize",
+ "Parameters": [
+ {
+ "Name": "model",
+ "Type": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer<Microsoft.AspNetCore.Authentication.Twitter.RequestToken>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Deserialize",
+ "Parameters": [
+ {
+ "Name": "data",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer<Microsoft.AspNetCore.Authentication.Twitter.RequestToken>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Write",
+ "Parameters": [
+ {
+ "Name": "writer",
+ "Type": "System.IO.BinaryWriter"
+ },
+ {
+ "Name": "token",
+ "Type": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Read",
+ "Parameters": [
+ {
+ "Name": "reader",
+ "Type": "System.IO.BinaryReader"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterDefaults",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "DisplayName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AuthenticationScheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": [],
+ "Constant": true,
+ "Literal": "\"Twitter\""
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterHandler",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler<Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateEventsAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Object>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleRemoteAuthenticateAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.HandleRequestResult>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateTicketAsync",
+ "Parameters": [
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "token",
+ "Type": "Microsoft.AspNetCore.Authentication.Twitter.AccessToken"
+ },
+ {
+ "Name": "user",
+ "Type": "Newtonsoft.Json.Linq.JObject"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticationTicket>",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleChallengeAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ConsumerKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ConsumerKey",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ConsumerSecret",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ConsumerSecret",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RetrieveUserDetails",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RetrieveUserDetails",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ClaimActions",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_StateDataFormat",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.Twitter.RequestToken>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_StateDataFormat",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.Twitter.RequestToken>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_StateCookie",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_StateCookie",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.CookieBuilder"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Validate",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterPostConfigureOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.Extensions.Options.IPostConfigureOptions<Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "PostConfigure",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions<Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "dataProtection",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs
new file mode 100644
index 0000000000..f643fad97f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs
@@ -0,0 +1,35 @@
+// 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;
+using Microsoft.IdentityModel.Protocols.WsFederation;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ /// <summary>
+ /// The context object used in for <see cref="WsFederationEvents.AuthenticationFailed"/>.
+ /// </summary>
+ public class AuthenticationFailedContext : RemoteAuthenticationContext<WsFederationOptions>
+ {
+ /// <summary>
+ /// Creates a new context object
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="scheme"></param>
+ /// <param name="options"></param>
+ public AuthenticationFailedContext(HttpContext context, AuthenticationScheme scheme, WsFederationOptions options)
+ : base(context, scheme, options, new AuthenticationProperties())
+ { }
+
+ /// <summary>
+ /// The <see cref="WsFederationMessage"/> from the request, if any.
+ /// </summary>
+ public WsFederationMessage ProtocolMessage { get; set; }
+
+ /// <summary>
+ /// The <see cref="Exception"/> that triggered this event.
+ /// </summary>
+ public Exception Exception { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs
new file mode 100644
index 0000000000..4028fa5e3c
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs
@@ -0,0 +1,33 @@
+// 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;
+using Microsoft.IdentityModel.Protocols.WsFederation;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ /// <summary>
+ /// The context object used for <see cref="WsFederationEvents.MessageReceived"/>.
+ /// </summary>
+ public class MessageReceivedContext : RemoteAuthenticationContext<WsFederationOptions>
+ {
+ /// <summary>
+ /// Creates a new context object.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="scheme"></param>
+ /// <param name="options"></param>
+ /// <param name="properties"></param>
+ public MessageReceivedContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ WsFederationOptions options,
+ AuthenticationProperties properties)
+ : base(context, scheme, options, properties) { }
+
+ /// <summary>
+ /// The <see cref="WsFederationMessage"/> received on this request.
+ /// </summary>
+ public WsFederationMessage ProtocolMessage { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs
new file mode 100644
index 0000000000..654037d0a8
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs
@@ -0,0 +1,44 @@
+// 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;
+using Microsoft.IdentityModel.Protocols.WsFederation;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ /// <summary>
+ /// When a user configures the <see cref="WsFederationHandler"/> to be notified prior to redirecting to an IdentityProvider
+ /// an instance of <see cref="RedirectContext"/> is passed to the 'RedirectToAuthenticationEndpoint' or 'RedirectToEndSessionEndpoint' events.
+ /// </summary>
+ public class RedirectContext : PropertiesContext<WsFederationOptions>
+ {
+ /// <summary>
+ /// Creates a new context object.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="scheme"></param>
+ /// <param name="options"></param>
+ /// <param name="properties"></param>
+ public RedirectContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ WsFederationOptions options,
+ AuthenticationProperties properties)
+ : base(context, scheme, options, properties) { }
+
+ /// <summary>
+ /// The <see cref="WsFederationMessage"/> used to compose the redirect.
+ /// </summary>
+ public WsFederationMessage ProtocolMessage { get; set; }
+
+ /// <summary>
+ /// If true, will skip any default logic for this redirect.
+ /// </summary>
+ public bool Handled { get; private set; }
+
+ /// <summary>
+ /// Skips any default logic for this redirect.
+ /// </summary>
+ public void HandleResponse() => Handled = true;
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RemoteSignoutContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RemoteSignoutContext.cs
new file mode 100644
index 0000000000..8aec24a64e
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RemoteSignoutContext.cs
@@ -0,0 +1,30 @@
+// 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;
+using Microsoft.IdentityModel.Protocols.WsFederation;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ /// <summary>
+ /// An event context for RemoteSignOut.
+ /// </summary>
+ public class RemoteSignOutContext : RemoteAuthenticationContext<WsFederationOptions>
+ {
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="scheme"></param>
+ /// <param name="options"></param>
+ /// <param name="message"></param>
+ public RemoteSignOutContext(HttpContext context, AuthenticationScheme scheme, WsFederationOptions options, WsFederationMessage message)
+ : base(context, scheme, options, new AuthenticationProperties())
+ => ProtocolMessage = message;
+
+ /// <summary>
+ /// The signout message.
+ /// </summary>
+ public WsFederationMessage ProtocolMessage { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenReceivedContext.cs
new file mode 100644
index 0000000000..311f41515f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenReceivedContext.cs
@@ -0,0 +1,28 @@
+// 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.Claims;
+using Microsoft.AspNetCore.Http;
+using Microsoft.IdentityModel.Protocols.WsFederation;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ /// <summary>
+ /// This Context can be used to be informed when an 'AuthorizationCode' is redeemed for tokens at the token endpoint.
+ /// </summary>
+ public class SecurityTokenReceivedContext : RemoteAuthenticationContext<WsFederationOptions>
+ {
+ /// <summary>
+ /// Creates a <see cref="SecurityTokenReceivedContext"/>
+ /// </summary>
+ public SecurityTokenReceivedContext(HttpContext context, AuthenticationScheme scheme, WsFederationOptions options, AuthenticationProperties properties)
+ : base(context, scheme, options, properties)
+ {
+ }
+
+ /// <summary>
+ /// The <see cref="WsFederationMessage"/> received on this request.
+ /// </summary>
+ public WsFederationMessage ProtocolMessage { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.cs
new file mode 100644
index 0000000000..1f32014b6c
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.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.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Http;
+using Microsoft.IdentityModel.Protocols.WsFederation;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ /// <summary>
+ /// The context object used for <see cref="WsFederationEvents.SecurityTokenValidated"/>.
+ /// </summary>
+ public class SecurityTokenValidatedContext : RemoteAuthenticationContext<WsFederationOptions>
+ {
+ /// <summary>
+ /// Creates a <see cref="SecurityTokenValidatedContext"/>
+ /// </summary>
+ public SecurityTokenValidatedContext(HttpContext context, AuthenticationScheme scheme, WsFederationOptions options, ClaimsPrincipal principal, AuthenticationProperties properties)
+ : base(context, scheme, options, properties)
+ => Principal = principal;
+
+ /// <summary>
+ /// The <see cref="WsFederationMessage"/> received on this request.
+ /// </summary>
+ public WsFederationMessage ProtocolMessage { get; set; }
+
+ /// <summary>
+ /// The <see cref="SecurityToken"/> that was validated.
+ /// </summary>
+ public SecurityToken SecurityToken { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs
new file mode 100644
index 0000000000..55c3936f9e
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs
@@ -0,0 +1,74 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ /// <summary>
+ /// Specifies events which the <see cref="WsFederationHandler"></see> invokes to enable developer control over the authentication process. />
+ /// </summary>
+ public class WsFederationEvents : RemoteAuthenticationEvents
+ {
+ /// <summary>
+ /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
+ /// </summary>
+ public Func<AuthenticationFailedContext, Task> OnAuthenticationFailed { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked when a protocol message is first received.
+ /// </summary>
+ public Func<MessageReceivedContext, Task> OnMessageReceived { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked to manipulate redirects to the identity provider for SignIn, SignOut, or Challenge.
+ /// </summary>
+ public Func<RedirectContext, Task> OnRedirectToIdentityProvider { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked when a wsignoutcleanup request is received at the RemoteSignOutPath endpoint.
+ /// </summary>
+ public Func<RemoteSignOutContext, Task> OnRemoteSignOut { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked with the security token that has been extracted from the protocol message.
+ /// </summary>
+ public Func<SecurityTokenReceivedContext, Task> OnSecurityTokenReceived { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated.
+ /// </summary>
+ public Func<SecurityTokenValidatedContext, Task> OnSecurityTokenValidated { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
+ /// </summary>
+ public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context);
+
+ /// <summary>
+ /// Invoked when a protocol message is first received.
+ /// </summary>
+ public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context);
+
+ /// <summary>
+ /// Invoked to manipulate redirects to the identity provider for SignIn, SignOut, or Challenge.
+ /// </summary>
+ public virtual Task RedirectToIdentityProvider(RedirectContext context) => OnRedirectToIdentityProvider(context);
+
+ /// <summary>
+ /// Invoked when a wsignoutcleanup request is received at the RemoteSignOutPath endpoint.
+ /// </summary>
+ public virtual Task RemoteSignOut(RemoteSignOutContext context) => OnRemoteSignOut(context);
+
+ /// <summary>
+ /// Invoked with the security token that has been extracted from the protocol message.
+ /// </summary>
+ public virtual Task SecurityTokenReceived(SecurityTokenReceivedContext context) => OnSecurityTokenReceived(context);
+
+ /// <summary>
+ /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated.
+ /// </summary>
+ public virtual Task SecurityTokenValidated(SecurityTokenValidatedContext context) => OnSecurityTokenValidated(context);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/LoggingExtensions.cs
new file mode 100644
index 0000000000..e28b7e15b0
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/LoggingExtensions.cs
@@ -0,0 +1,85 @@
+// 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.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static Action<ILogger, Exception> _signInWithoutWresult;
+ private static Action<ILogger, Exception> _signInWithoutToken;
+ private static Action<ILogger, Exception> _exceptionProcessingMessage;
+ private static Action<ILogger, string, Exception> _malformedRedirectUri;
+ private static Action<ILogger, Exception> _remoteSignOutHandledResponse;
+ private static Action<ILogger, Exception> _remoteSignOutSkipped;
+ private static Action<ILogger, Exception> _remoteSignOut;
+
+ static LoggingExtensions()
+ {
+ _signInWithoutWresult = LoggerMessage.Define(
+ eventId: 1,
+ logLevel: LogLevel.Debug,
+ formatString: "Received a sign-in message without a WResult.");
+ _signInWithoutToken = LoggerMessage.Define(
+ eventId: 2,
+ logLevel: LogLevel.Debug,
+ formatString: "Received a sign-in message without a token.");
+ _exceptionProcessingMessage = LoggerMessage.Define(
+ eventId: 3,
+ logLevel: LogLevel.Error,
+ formatString: "Exception occurred while processing message.");
+ _malformedRedirectUri = LoggerMessage.Define<string>(
+ eventId: 4,
+ logLevel: LogLevel.Warning,
+ formatString: "The sign-out redirect URI '{0}' is malformed.");
+ _remoteSignOutHandledResponse = LoggerMessage.Define(
+ eventId: 5,
+ logLevel: LogLevel.Debug,
+ formatString: "RemoteSignOutContext.HandledResponse");
+ _remoteSignOutSkipped = LoggerMessage.Define(
+ eventId: 6,
+ logLevel: LogLevel.Debug,
+ formatString: "RemoteSignOutContext.Skipped");
+ _remoteSignOut = LoggerMessage.Define(
+ eventId: 7,
+ logLevel: LogLevel.Information,
+ formatString: "Remote signout request processed.");
+ }
+
+ public static void SignInWithoutWresult(this ILogger logger)
+ {
+ _signInWithoutWresult(logger, null);
+ }
+
+ public static void SignInWithoutToken(this ILogger logger)
+ {
+ _signInWithoutToken(logger, null);
+ }
+
+ public static void ExceptionProcessingMessage(this ILogger logger, Exception ex)
+ {
+ _exceptionProcessingMessage(logger, ex);
+ }
+
+ public static void MalformedRedirectUri(this ILogger logger, string uri)
+ {
+ _malformedRedirectUri(logger, uri, null);
+ }
+
+ public static void RemoteSignOutHandledResponse(this ILogger logger)
+ {
+ _remoteSignOutHandledResponse(logger, null);
+ }
+
+ public static void RemoteSignOutSkipped(this ILogger logger)
+ {
+ _remoteSignOutSkipped(logger, null);
+ }
+
+ public static void RemoteSignOut(this ILogger logger)
+ {
+ _remoteSignOut(logger, null);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Microsoft.AspNetCore.Authentication.WsFederation.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Microsoft.AspNetCore.Authentication.WsFederation.csproj
new file mode 100644
index 0000000000..4edb55cb35
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Microsoft.AspNetCore.Authentication.WsFederation.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core middleware that enables an application to support the WsFederation authentication workflow.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;authentication;security</PackageTags>
+ </PropertyGroup>
+
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.Authentication\Microsoft.AspNetCore.Authentication.csproj" />
+ <PackageReference Include="Microsoft.IdentityModel.Protocols.WsFederation" Version="$(MicrosoftIdentityModelProtocolsWsFederationPackageVersion)" />
+ <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="$(SystemIdentityModelTokensJwtPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..564e826a78
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Properties/Resources.Designer.cs
@@ -0,0 +1,114 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Authentication.WsFederation.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// The service descriptor is missing.
+ /// </summary>
+ internal static string Exception_MissingDescriptor
+ {
+ get => GetString("Exception_MissingDescriptor");
+ }
+
+ /// <summary>
+ /// The service descriptor is missing.
+ /// </summary>
+ internal static string FormatException_MissingDescriptor()
+ => GetString("Exception_MissingDescriptor");
+
+ /// <summary>
+ /// No token validator was found for the given token.
+ /// </summary>
+ internal static string Exception_NoTokenValidatorFound
+ {
+ get => GetString("Exception_NoTokenValidatorFound");
+ }
+
+ /// <summary>
+ /// No token validator was found for the given token.
+ /// </summary>
+ internal static string FormatException_NoTokenValidatorFound()
+ => GetString("Exception_NoTokenValidatorFound");
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string Exception_OptionMustBeProvided
+ {
+ get => GetString("Exception_OptionMustBeProvided");
+ }
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string FormatException_OptionMustBeProvided(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0);
+
+ /// <summary>
+ /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.
+ /// </summary>
+ internal static string Exception_ValidatorHandlerMismatch
+ {
+ get => GetString("Exception_ValidatorHandlerMismatch");
+ }
+
+ /// <summary>
+ /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.
+ /// </summary>
+ internal static string FormatException_ValidatorHandlerMismatch()
+ => GetString("Exception_ValidatorHandlerMismatch");
+
+ /// <summary>
+ /// The sign in message does not contain a required token.
+ /// </summary>
+ internal static string SignInMessageTokenIsMissing
+ {
+ get => GetString("SignInMessageTokenIsMissing");
+ }
+
+ /// <summary>
+ /// The sign in message does not contain a required token.
+ /// </summary>
+ internal static string FormatSignInMessageTokenIsMissing()
+ => GetString("SignInMessageTokenIsMissing");
+
+ /// <summary>
+ /// The sign in message does not contain a required wresult.
+ /// </summary>
+ internal static string SignInMessageWresultIsMissing
+ {
+ get => GetString("SignInMessageWresultIsMissing");
+ }
+
+ /// <summary>
+ /// The sign in message does not contain a required wresult.
+ /// </summary>
+ internal static string FormatSignInMessageWresultIsMissing()
+ => GetString("SignInMessageWresultIsMissing");
+
+ 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/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Resources.resx
new file mode 100644
index 0000000000..e2edafb671
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/Resources.resx
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Exception_MissingDescriptor" xml:space="preserve">
+ <value>The service descriptor is missing.</value>
+ </data>
+ <data name="Exception_NoTokenValidatorFound" xml:space="preserve">
+ <value>No token validator was found for the given token.</value>
+ </data>
+ <data name="Exception_OptionMustBeProvided" xml:space="preserve">
+ <value>The '{0}' option must be provided.</value>
+ </data>
+ <data name="Exception_ValidatorHandlerMismatch" xml:space="preserve">
+ <value>An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.</value>
+ </data>
+ <data name="SignInMessageTokenIsMissing" xml:space="preserve">
+ <value>The sign in message does not contain a required token.</value>
+ </data>
+ <data name="SignInMessageWresultIsMissing" xml:space="preserve">
+ <value>The sign in message does not contain a required wresult.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationDefaults.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationDefaults.cs
new file mode 100644
index 0000000000..3b97d995b5
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationDefaults.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.
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ /// <summary>
+ /// Default values related to WsFederation authentication handler
+ /// </summary>
+ public static class WsFederationDefaults
+ {
+ /// <summary>
+ /// The default authentication type used when registering the WsFederationHandler.
+ /// </summary>
+ public const string AuthenticationScheme = "WsFederation";
+
+ /// <summary>
+ /// The default display name used when registering the WsFederationHandler.
+ /// </summary>
+ public const string DisplayName = "WsFederation";
+
+ /// <summary>
+ /// Constant used to identify userstate inside AuthenticationProperties that have been serialized in the 'wctx' parameter.
+ /// </summary>
+ public static readonly string UserstatePropertiesKey = "WsFederation.Userstate";
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationExtensions.cs
new file mode 100644
index 0000000000..47091d58d5
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationExtensions.cs
@@ -0,0 +1,58 @@
+// 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.Authentication;
+using Microsoft.AspNetCore.Authentication.WsFederation;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ /// <summary>
+ /// Extensions for registering the <see cref="WsFederationHandler"/>.
+ /// </summary>
+ public static class WsFederationExtensions
+ {
+ /// <summary>
+ /// Registers the <see cref="WsFederationHandler"/> using the default authentication scheme, display name, and options.
+ /// </summary>
+ /// <param name="builder"></param>
+ /// <returns></returns>
+ public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder)
+ => builder.AddWsFederation(WsFederationDefaults.AuthenticationScheme, _ => { });
+
+ /// <summary>
+ /// Registers the <see cref="WsFederationHandler"/> using the default authentication scheme, display name, and the given options configuration.
+ /// </summary>
+ /// <param name="builder"></param>
+ /// <param name="configureOptions">A delegate that configures the <see cref="WsFederationOptions"/>.</param>
+ /// <returns></returns>
+ public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, Action<WsFederationOptions> configureOptions)
+ => builder.AddWsFederation(WsFederationDefaults.AuthenticationScheme, configureOptions);
+
+ /// <summary>
+ /// Registers the <see cref="WsFederationHandler"/> using the given authentication scheme, default display name, and the given options configuration.
+ /// </summary>
+ /// <param name="builder"></param>
+ /// <param name="authenticationScheme"></param>
+ /// <param name="configureOptions">A delegate that configures the <see cref="WsFederationOptions"/>.</param>
+ /// <returns></returns>
+ public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, Action<WsFederationOptions> configureOptions)
+ => builder.AddWsFederation(authenticationScheme, WsFederationDefaults.DisplayName, configureOptions);
+
+ /// <summary>
+ /// Registers the <see cref="WsFederationHandler"/> using the given authentication scheme, display name, and options configuration.
+ /// </summary>
+ /// <param name="builder"></param>
+ /// <param name="authenticationScheme"></param>
+ /// <param name="displayName"></param>
+ /// <param name="configureOptions">A delegate that configures the <see cref="WsFederationOptions"/>.</param>
+ /// <returns></returns>
+ public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<WsFederationOptions> configureOptions)
+ {
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<WsFederationOptions>, WsFederationPostConfigureOptions>());
+ return builder.AddRemoteScheme<WsFederationOptions, WsFederationHandler>(authenticationScheme, displayName, configureOptions);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationHandler.cs
new file mode 100644
index 0000000000..e47f8431f9
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationHandler.cs
@@ -0,0 +1,425 @@
+// 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 System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Protocols.WsFederation;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ /// <summary>
+ /// A per-request authentication handler for the WsFederation.
+ /// </summary>
+ public class WsFederationHandler : RemoteAuthenticationHandler<WsFederationOptions>, IAuthenticationSignOutHandler
+ {
+ private const string CorrelationProperty = ".xsrf";
+ private WsFederationConfiguration _configuration;
+
+ /// <summary>
+ /// Creates a new WsFederationAuthenticationHandler
+ /// </summary>
+ /// <param name="options"></param>
+ /// <param name="encoder"></param>
+ /// <param name="clock"></param>
+ /// <param name="logger"></param>
+ public WsFederationHandler(IOptionsMonitor<WsFederationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
+ : base(options, logger, encoder, clock)
+ {
+ }
+
+ /// <summary>
+ /// The handler calls methods on the events which give the application control at certain points where processing is occurring.
+ /// If it is not provided a default instance is supplied which does nothing when the methods are called.
+ /// </summary>
+ protected new WsFederationEvents Events
+ {
+ get { return (WsFederationEvents)base.Events; }
+ set { base.Events = value; }
+ }
+
+ /// <summary>
+ /// Creates a new instance of the events instance.
+ /// </summary>
+ /// <returns>A new instance of the events instance.</returns>
+ protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new WsFederationEvents());
+
+ /// <summary>
+ /// Overridden to handle remote signout requests
+ /// </summary>
+ /// <returns></returns>
+ public override Task<bool> HandleRequestAsync()
+ {
+ // RemoteSignOutPath and CallbackPath may be the same, fall through if the message doesn't match.
+ if (Options.RemoteSignOutPath.HasValue && Options.RemoteSignOutPath == Request.Path && HttpMethods.IsGet(Request.Method)
+ && string.Equals(Request.Query[WsFederationConstants.WsFederationParameterNames.Wa],
+ WsFederationConstants.WsFederationActions.SignOutCleanup, StringComparison.OrdinalIgnoreCase))
+ {
+ // We've received a remote sign-out request
+ return HandleRemoteSignOutAsync();
+ }
+
+ return base.HandleRequestAsync();
+ }
+
+ /// <summary>
+ /// Handles Challenge
+ /// </summary>
+ /// <returns></returns>
+ protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
+ {
+ if (_configuration == null)
+ {
+ _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
+ }
+
+ // Save the original challenge URI so we can redirect back to it when we're done.
+ if (string.IsNullOrEmpty(properties.RedirectUri))
+ {
+ properties.RedirectUri = CurrentUri;
+ }
+
+ var wsFederationMessage = new WsFederationMessage()
+ {
+ IssuerAddress = _configuration.TokenEndpoint ?? string.Empty,
+ Wtrealm = Options.Wtrealm,
+ Wa = WsFederationConstants.WsFederationActions.SignIn,
+ };
+
+ if (!string.IsNullOrEmpty(Options.Wreply))
+ {
+ wsFederationMessage.Wreply = Options.Wreply;
+ }
+ else
+ {
+ wsFederationMessage.Wreply = BuildRedirectUri(Options.CallbackPath);
+ }
+
+ GenerateCorrelationId(properties);
+
+ var redirectContext = new RedirectContext(Context, Scheme, Options, properties)
+ {
+ ProtocolMessage = wsFederationMessage
+ };
+ await Events.RedirectToIdentityProvider(redirectContext);
+
+ if (redirectContext.Handled)
+ {
+ return;
+ }
+
+ wsFederationMessage = redirectContext.ProtocolMessage;
+
+ if (!string.IsNullOrEmpty(wsFederationMessage.Wctx))
+ {
+ properties.Items[WsFederationDefaults.UserstatePropertiesKey] = wsFederationMessage.Wctx;
+ }
+
+ wsFederationMessage.Wctx = Uri.EscapeDataString(Options.StateDataFormat.Protect(properties));
+
+ var redirectUri = wsFederationMessage.CreateSignInUrl();
+ if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
+ {
+ Logger.MalformedRedirectUri(redirectUri);
+ }
+ Response.Redirect(redirectUri);
+ }
+
+ /// <summary>
+ /// Invoked to process incoming authentication messages.
+ /// </summary>
+ /// <returns></returns>
+ protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
+ {
+ WsFederationMessage wsFederationMessage = null;
+ AuthenticationProperties properties = null;
+
+ // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small.
+ if (HttpMethods.IsPost(Request.Method)
+ && !string.IsNullOrEmpty(Request.ContentType)
+ // May have media/type; charset=utf-8, allow partial match.
+ && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)
+ && Request.Body.CanRead)
+ {
+ var form = await Request.ReadFormAsync();
+
+ wsFederationMessage = new WsFederationMessage(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
+ }
+
+ if (wsFederationMessage == null || !wsFederationMessage.IsSignInMessage)
+ {
+ if (Options.SkipUnrecognizedRequests)
+ {
+ // Not for us?
+ return HandleRequestResult.SkipHandler();
+ }
+
+ return HandleRequestResult.Fail("No message.");
+ }
+
+ try
+ {
+ // Retrieve our cached redirect uri
+ var state = wsFederationMessage.Wctx;
+ // WsFed allows for uninitiated logins, state may be missing. See AllowUnsolicitedLogins.
+ properties = Options.StateDataFormat.Unprotect(state);
+
+ if (properties == null)
+ {
+ if (!Options.AllowUnsolicitedLogins)
+ {
+ return HandleRequestResult.Fail("Unsolicited logins are not allowed.");
+ }
+ }
+ else
+ {
+ // Extract the user state from properties and reset.
+ properties.Items.TryGetValue(WsFederationDefaults.UserstatePropertiesKey, out var userState);
+ wsFederationMessage.Wctx = userState;
+ }
+
+ var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options, properties)
+ {
+ ProtocolMessage = wsFederationMessage
+ };
+ await Events.MessageReceived(messageReceivedContext);
+ if (messageReceivedContext.Result != null)
+ {
+ return messageReceivedContext.Result;
+ }
+ wsFederationMessage = messageReceivedContext.ProtocolMessage;
+ properties = messageReceivedContext.Properties; // Provides a new instance if not set.
+
+ // If state did flow from the challenge then validate it. See AllowUnsolicitedLogins above.
+ if (properties.Items.TryGetValue(CorrelationProperty, out string correlationId)
+ && !ValidateCorrelationId(properties))
+ {
+ return HandleRequestResult.Fail("Correlation failed.", properties);
+ }
+
+ if (wsFederationMessage.Wresult == null)
+ {
+ Logger.SignInWithoutWresult();
+ return HandleRequestResult.Fail(Resources.SignInMessageWresultIsMissing, properties);
+ }
+
+ var token = wsFederationMessage.GetToken();
+ if (string.IsNullOrEmpty(token))
+ {
+ Logger.SignInWithoutToken();
+ return HandleRequestResult.Fail(Resources.SignInMessageTokenIsMissing, properties);
+ }
+
+ var securityTokenReceivedContext = new SecurityTokenReceivedContext(Context, Scheme, Options, properties)
+ {
+ ProtocolMessage = wsFederationMessage
+ };
+ await Events.SecurityTokenReceived(securityTokenReceivedContext);
+ if (securityTokenReceivedContext.Result != null)
+ {
+ return securityTokenReceivedContext.Result;
+ }
+ wsFederationMessage = securityTokenReceivedContext.ProtocolMessage;
+ properties = messageReceivedContext.Properties;
+
+ if (_configuration == null)
+ {
+ _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
+ }
+
+ // Copy and augment to avoid cross request race conditions for updated configurations.
+ var tvp = Options.TokenValidationParameters.Clone();
+ var issuers = new[] { _configuration.Issuer };
+ tvp.ValidIssuers = (tvp.ValidIssuers == null ? issuers : tvp.ValidIssuers.Concat(issuers));
+ tvp.IssuerSigningKeys = (tvp.IssuerSigningKeys == null ? _configuration.SigningKeys : tvp.IssuerSigningKeys.Concat(_configuration.SigningKeys));
+
+ ClaimsPrincipal principal = null;
+ SecurityToken parsedToken = null;
+ foreach (var validator in Options.SecurityTokenHandlers)
+ {
+ if (validator.CanReadToken(token))
+ {
+ principal = validator.ValidateToken(token, tvp, out parsedToken);
+ break;
+ }
+ }
+
+ if (principal == null)
+ {
+ throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound);
+ }
+
+ if (Options.UseTokenLifetime && parsedToken != null)
+ {
+ // Override any session persistence to match the token lifetime.
+ var issued = parsedToken.ValidFrom;
+ if (issued != DateTime.MinValue)
+ {
+ properties.IssuedUtc = issued.ToUniversalTime();
+ }
+ var expires = parsedToken.ValidTo;
+ if (expires != DateTime.MinValue)
+ {
+ properties.ExpiresUtc = expires.ToUniversalTime();
+ }
+ properties.AllowRefresh = false;
+ }
+
+ var securityTokenValidatedContext = new SecurityTokenValidatedContext(Context, Scheme, Options, principal, properties)
+ {
+ ProtocolMessage = wsFederationMessage,
+ SecurityToken = parsedToken,
+ };
+
+ await Events.SecurityTokenValidated(securityTokenValidatedContext);
+ if (securityTokenValidatedContext.Result != null)
+ {
+ return securityTokenValidatedContext.Result;
+ }
+
+ // Flow possible changes
+ principal = securityTokenValidatedContext.Principal;
+ properties = securityTokenValidatedContext.Properties;
+
+ return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name));
+ }
+ catch (Exception exception)
+ {
+ Logger.ExceptionProcessingMessage(exception);
+
+ // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification.
+ if (Options.RefreshOnIssuerKeyNotFound && exception.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException)))
+ {
+ Options.ConfigurationManager.RequestRefresh();
+ }
+
+ var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
+ {
+ ProtocolMessage = wsFederationMessage,
+ Exception = exception
+ };
+ await Events.AuthenticationFailed(authenticationFailedContext);
+ if (authenticationFailedContext.Result != null)
+ {
+ return authenticationFailedContext.Result;
+ }
+
+ return HandleRequestResult.Fail(exception, properties);
+ }
+ }
+
+ /// <summary>
+ /// Handles Signout
+ /// </summary>
+ /// <returns></returns>
+ public async virtual Task SignOutAsync(AuthenticationProperties properties)
+ {
+ var target = ResolveTarget(Options.ForwardSignOut);
+ if (target != null)
+ {
+ await Context.SignOutAsync(target, properties);
+ return;
+ }
+
+ if (_configuration == null)
+ {
+ _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
+ }
+
+ var wsFederationMessage = new WsFederationMessage()
+ {
+ IssuerAddress = _configuration.TokenEndpoint ?? string.Empty,
+ Wtrealm = Options.Wtrealm,
+ Wa = WsFederationConstants.WsFederationActions.SignOut,
+ };
+
+ // Set Wreply in order:
+ // 1. properties.Redirect
+ // 2. Options.SignOutWreply
+ // 3. Options.Wreply
+ if (properties != null && !string.IsNullOrEmpty(properties.RedirectUri))
+ {
+ wsFederationMessage.Wreply = BuildRedirectUriIfRelative(properties.RedirectUri);
+ }
+ else if (!string.IsNullOrEmpty(Options.SignOutWreply))
+ {
+ wsFederationMessage.Wreply = BuildRedirectUriIfRelative(Options.SignOutWreply);
+ }
+ else if (!string.IsNullOrEmpty(Options.Wreply))
+ {
+ wsFederationMessage.Wreply = BuildRedirectUriIfRelative(Options.Wreply);
+ }
+
+ var redirectContext = new RedirectContext(Context, Scheme, Options, properties)
+ {
+ ProtocolMessage = wsFederationMessage
+ };
+ await Events.RedirectToIdentityProvider(redirectContext);
+
+ if (!redirectContext.Handled)
+ {
+ var redirectUri = redirectContext.ProtocolMessage.CreateSignOutUrl();
+ if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
+ {
+ Logger.MalformedRedirectUri(redirectUri);
+ }
+ Response.Redirect(redirectUri);
+ }
+ }
+
+ /// <summary>
+ /// Handles wsignoutcleanup1.0 messages sent to the RemoteSignOutPath
+ /// </summary>
+ /// <returns></returns>
+ protected virtual async Task<bool> HandleRemoteSignOutAsync()
+ {
+ var message = new WsFederationMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
+ var remoteSignOutContext = new RemoteSignOutContext(Context, Scheme, Options, message);
+ await Events.RemoteSignOut(remoteSignOutContext);
+
+ if (remoteSignOutContext.Result != null)
+ {
+ if (remoteSignOutContext.Result.Handled)
+ {
+ Logger.RemoteSignOutHandledResponse();
+ return true;
+ }
+ if (remoteSignOutContext.Result.Skipped)
+ {
+ Logger.RemoteSignOutSkipped();
+ return false;
+ }
+ }
+
+ Logger.RemoteSignOut();
+
+ await Context.SignOutAsync(Options.SignOutScheme);
+ return true;
+ }
+
+ /// <summary>
+ /// Build a redirect path if the given path is a relative path.
+ /// </summary>
+ private string BuildRedirectUriIfRelative(string uri)
+ {
+ if (string.IsNullOrEmpty(uri))
+ {
+ return uri;
+ }
+
+ if (!uri.StartsWith("/", StringComparison.Ordinal))
+ {
+ return uri;
+ }
+
+ return BuildRedirectUri(uri);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationOptions.cs
new file mode 100644
index 0000000000..4e06126773
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationOptions.cs
@@ -0,0 +1,180 @@
+// 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.Collections.ObjectModel;
+using System.IdentityModel.Tokens.Jwt;
+using Microsoft.AspNetCore.Http;
+using Microsoft.IdentityModel.Protocols;
+using Microsoft.IdentityModel.Protocols.WsFederation;
+using Microsoft.IdentityModel.Tokens;
+using Microsoft.IdentityModel.Tokens.Saml;
+using Microsoft.IdentityModel.Tokens.Saml2;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ /// <summary>
+ /// Configuration options for <see cref="WsFederationHandler"/>
+ /// </summary>
+ public class WsFederationOptions : RemoteAuthenticationOptions
+ {
+ private ICollection<ISecurityTokenValidator> _securityTokenHandlers = new Collection<ISecurityTokenValidator>()
+ {
+ new Saml2SecurityTokenHandler(),
+ new SamlSecurityTokenHandler(),
+ new JwtSecurityTokenHandler()
+ };
+ private TokenValidationParameters _tokenValidationParameters = new TokenValidationParameters();
+
+ /// <summary>
+ /// Initializes a new <see cref="WsFederationOptions"/>
+ /// </summary>
+ public WsFederationOptions()
+ {
+ CallbackPath = "/signin-wsfed";
+ // In ADFS the cleanup messages are sent to the same callback path as the initial login.
+ // In AAD it sends the cleanup message to a random Reply Url and there's no deterministic way to configure it.
+ // If you manage to get it configured, then you can set RemoteSignOutPath accordingly.
+ RemoteSignOutPath = "/signin-wsfed";
+ Events = new WsFederationEvents();
+ }
+
+ /// <summary>
+ /// Check that the options are valid. Should throw an exception if things are not ok.
+ /// </summary>
+ public override void Validate()
+ {
+ base.Validate();
+
+ if (ConfigurationManager == null)
+ {
+ throw new InvalidOperationException($"Provide {nameof(MetadataAddress)}, "
+ + $"{nameof(Configuration)}, or {nameof(ConfigurationManager)} to {nameof(WsFederationOptions)}");
+ }
+ }
+
+ /// <summary>
+ /// Configuration provided directly by the developer. If provided, then MetadataAddress and the Backchannel properties
+ /// will not be used. This information should not be updated during request processing.
+ /// </summary>
+ public WsFederationConfiguration Configuration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the address to retrieve the wsFederation metadata
+ /// </summary>
+ public string MetadataAddress { get; set; }
+
+ /// <summary>
+ /// Responsible for retrieving, caching, and refreshing the configuration from metadata.
+ /// If not provided, then one will be created using the MetadataAddress and Backchannel properties.
+ /// </summary>
+ public IConfigurationManager<WsFederationConfiguration> ConfigurationManager { get; set; }
+
+ /// <summary>
+ /// Gets or sets if a metadata refresh should be attempted after a SecurityTokenSignatureKeyNotFoundException. This allows for automatic
+ /// recovery in the event of a signature key rollover. This is enabled by default.
+ /// </summary>
+ public bool RefreshOnIssuerKeyNotFound { get; set; } = true;
+
+ /// <summary>
+ /// Indicates if requests to the CallbackPath may also be for other components. If enabled the handler will pass
+ /// requests through that do not contain WsFederation authentication responses. Disabling this and setting the
+ /// CallbackPath to a dedicated endpoint may provide better error handling.
+ /// This is disabled by default.
+ /// </summary>
+ public bool SkipUnrecognizedRequests { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="WsFederationEvents"/> to call when processing WsFederation messages.
+ /// </summary>
+ public new WsFederationEvents Events
+ {
+ get => (WsFederationEvents)base.Events;
+ set => base.Events = value;
+ }
+
+ /// <summary>
+ /// Gets or sets the collection of <see cref="ISecurityTokenValidator"/> used to read and validate the <see cref="SecurityToken"/>s.
+ /// </summary>
+ public ICollection<ISecurityTokenValidator> SecurityTokenHandlers
+ {
+ get
+ {
+ return _securityTokenHandlers;
+ }
+ set
+ {
+ _securityTokenHandlers = value ?? throw new ArgumentNullException(nameof(SecurityTokenHandlers));
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the type used to secure data handled by the middleware.
+ /// </summary>
+ public ISecureDataFormat<AuthenticationProperties> StateDataFormat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="TokenValidationParameters"/>
+ /// </summary>
+ /// <exception cref="ArgumentNullException"> if 'TokenValidationParameters' is null.</exception>
+ public TokenValidationParameters TokenValidationParameters
+ {
+ get
+ {
+ return _tokenValidationParameters;
+ }
+ set
+ {
+ _tokenValidationParameters = value ?? throw new ArgumentNullException(nameof(TokenValidationParameters));
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the 'wreply'. CallbackPath must be set to match or cleared so it can be generated dynamically.
+ /// This field is optional. If not set then it will be generated from the current request and the CallbackPath.
+ /// </summary>
+ public string Wreply { get; set; }
+
+ /// <summary>
+ /// Gets or sets the 'wreply' value used during sign-out.
+ /// If none is specified then the value from the Wreply field is used.
+ /// </summary>
+ public string SignOutWreply { get; set; }
+
+ /// <summary>
+ /// Gets or sets the 'wtrealm'.
+ /// </summary>
+ public string Wtrealm { get; set; }
+
+ /// <summary>
+ /// Indicates that the authentication session lifetime (e.g. cookies) should match that of the authentication token.
+ /// If the token does not provide lifetime information then normal session lifetimes will be used.
+ /// This is enabled by default.
+ /// </summary>
+ public bool UseTokenLifetime { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets if HTTPS is required for the metadata address or authority.
+ /// The default is true. This should be disabled only in development environments.
+ /// </summary>
+ public bool RequireHttpsMetadata { get; set; } = true;
+
+ /// <summary>
+ /// The Ws-Federation protocol allows the user to initiate logins without contacting the application for a Challenge first.
+ /// However, that flow is susceptible to XSRF and other attacks so it is disabled here by default.
+ /// </summary>
+ public bool AllowUnsolicitedLogins { get; set; }
+
+ /// <summary>
+ /// Requests received on this path will cause the handler to invoke SignOut using the SignOutScheme.
+ /// </summary>
+ public PathString RemoteSignOutPath { get; set; }
+
+ /// <summary>
+ /// The Authentication Scheme to use with SignOutAsync from RemoteSignOutPath. SignInScheme will be used if this
+ /// is not set.
+ /// </summary>
+ public string SignOutScheme { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs
new file mode 100644
index 0000000000..62647d4fcd
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs
@@ -0,0 +1,89 @@
+// 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.Net.Http;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Protocols;
+using Microsoft.IdentityModel.Protocols.WsFederation;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ /// <summary>
+ /// Used to setup defaults for all <see cref="WsFederationOptions"/>.
+ /// </summary>
+ public class WsFederationPostConfigureOptions : IPostConfigureOptions<WsFederationOptions>
+ {
+ private readonly IDataProtectionProvider _dp;
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="dataProtection"></param>
+ public WsFederationPostConfigureOptions(IDataProtectionProvider dataProtection)
+ {
+ _dp = dataProtection;
+ }
+
+ /// <summary>
+ /// Invoked to post configure a TOptions instance.
+ /// </summary>
+ /// <param name="name">The name of the options instance being configured.</param>
+ /// <param name="options">The options instance to configure.</param>
+ public void PostConfigure(string name, WsFederationOptions options)
+ {
+ options.DataProtectionProvider = options.DataProtectionProvider ?? _dp;
+
+ if (string.IsNullOrEmpty(options.SignOutScheme))
+ {
+ options.SignOutScheme = options.SignInScheme;
+ }
+
+ if (options.StateDataFormat == null)
+ {
+ var dataProtector = options.DataProtectionProvider.CreateProtector(
+ typeof(WsFederationHandler).FullName, name, "v1");
+ options.StateDataFormat = new PropertiesDataFormat(dataProtector);
+ }
+
+ if (!options.CallbackPath.HasValue && !string.IsNullOrEmpty(options.Wreply) && Uri.TryCreate(options.Wreply, UriKind.Absolute, out var wreply))
+ {
+ // Wreply must be a very specific, case sensitive value, so we can't generate it. Instead we generate CallbackPath from it.
+ options.CallbackPath = PathString.FromUriComponent(wreply);
+ }
+
+ if (string.IsNullOrEmpty(options.TokenValidationParameters.ValidAudience))
+ {
+ options.TokenValidationParameters.ValidAudience = options.Wtrealm;
+ }
+
+ if (options.Backchannel == null)
+ {
+ options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler());
+ options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core WsFederation handler");
+ options.Backchannel.Timeout = options.BackchannelTimeout;
+ options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
+ }
+
+ if (options.ConfigurationManager == null)
+ {
+ if (options.Configuration != null)
+ {
+ options.ConfigurationManager = new StaticConfigurationManager<WsFederationConfiguration>(options.Configuration);
+ }
+ else if (!string.IsNullOrEmpty(options.MetadataAddress))
+ {
+ if (options.RequireHttpsMetadata && !options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException("The MetadataAddress must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false.");
+ }
+
+ options.ConfigurationManager = new ConfigurationManager<WsFederationConfiguration>(options.MetadataAddress, new WsFederationConfigurationRetriever(),
+ new HttpDocumentRetriever(options.Backchannel) { RequireHttps = options.RequireHttpsMetadata });
+ }
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/baseline.netcore.json
new file mode 100644
index 0000000000..41150cbc09
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication.WsFederation/baseline.netcore.json
@@ -0,0 +1,1314 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.WsFederation, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.WsFederationExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddWsFederation",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddWsFederation",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddWsFederation",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddWsFederation",
+ "Parameters": [
+ {
+ "Name": "builder",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.WsFederation.AuthenticationFailedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Exception",
+ "Parameters": [],
+ "ReturnType": "System.Exception",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Exception",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Exception"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.WsFederation.MessageReceivedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.WsFederation.RedirectContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Handled",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleResponse",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.WsFederation.RemoteSignOutContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions"
+ },
+ {
+ "Name": "message",
+ "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenReceivedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenValidatedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ProtocolMessage",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ProtocolMessage",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SecurityToken",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Tokens.SecurityToken",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SecurityToken",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Tokens.SecurityToken"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions"
+ },
+ {
+ "Name": "principal",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_OnAuthenticationFailed",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.WsFederation.AuthenticationFailedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnAuthenticationFailed",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.WsFederation.AuthenticationFailedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnMessageReceived",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.WsFederation.MessageReceivedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnMessageReceived",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.WsFederation.MessageReceivedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnRedirectToIdentityProvider",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.WsFederation.RedirectContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnRedirectToIdentityProvider",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.WsFederation.RedirectContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnRemoteSignOut",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.WsFederation.RemoteSignOutContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnRemoteSignOut",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.WsFederation.RemoteSignOutContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnSecurityTokenReceived",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenReceivedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnSecurityTokenReceived",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenReceivedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnSecurityTokenValidated",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenValidatedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnSecurityTokenValidated",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenValidatedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AuthenticationFailed",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.AuthenticationFailedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "MessageReceived",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.MessageReceivedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RedirectToIdentityProvider",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.RedirectContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RemoteSignOut",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.RemoteSignOutContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SecurityTokenReceived",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenReceivedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SecurityTokenValidated",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenValidatedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationDefaults",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "UserstatePropertiesKey",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "ReadOnly": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Field",
+ "Name": "AuthenticationScheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": [],
+ "Constant": true,
+ "Literal": "\"WsFederation\""
+ },
+ {
+ "Kind": "Field",
+ "Name": "DisplayName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": [],
+ "Constant": true,
+ "Literal": "\"WsFederation\""
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationHandler",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions>",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "HandleRequestAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Boolean>",
+ "Virtual": true,
+ "Override": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateEventsAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Object>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleChallengeAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleRemoteAuthenticateAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.HandleRequestResult>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SignOutAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleRemoteSignOutAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Boolean>",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Validate",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Configuration",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationConfiguration",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Configuration",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationConfiguration"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_MetadataAddress",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_MetadataAddress",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ConfigurationManager",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Protocols.IConfigurationManager<Microsoft.IdentityModel.Protocols.WsFederation.WsFederationConfiguration>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ConfigurationManager",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Protocols.IConfigurationManager<Microsoft.IdentityModel.Protocols.WsFederation.WsFederationConfiguration>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RefreshOnIssuerKeyNotFound",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RefreshOnIssuerKeyNotFound",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SkipUnrecognizedRequests",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SkipUnrecognizedRequests",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SecurityTokenHandlers",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.ICollection<Microsoft.IdentityModel.Tokens.ISecurityTokenValidator>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SecurityTokenHandlers",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Collections.Generic.ICollection<Microsoft.IdentityModel.Tokens.ISecurityTokenValidator>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_StateDataFormat",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationProperties>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_StateDataFormat",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationProperties>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_TokenValidationParameters",
+ "Parameters": [],
+ "ReturnType": "Microsoft.IdentityModel.Tokens.TokenValidationParameters",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_TokenValidationParameters",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.IdentityModel.Tokens.TokenValidationParameters"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Wreply",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Wreply",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SignOutWreply",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SignOutWreply",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Wtrealm",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Wtrealm",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_UseTokenLifetime",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_UseTokenLifetime",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RequireHttpsMetadata",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RequireHttpsMetadata",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AllowUnsolicitedLogins",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AllowUnsolicitedLogins",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RemoteSignOutPath",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RemoteSignOutPath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.PathString"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SignOutScheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SignOutScheme",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationPostConfigureOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.Extensions.Options.IPostConfigureOptions<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "PostConfigure",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "dataProtection",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/AuthAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthAppBuilderExtensions.cs
new file mode 100644
index 0000000000..771601ed1a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthAppBuilderExtensions.cs
@@ -0,0 +1,29 @@
+// 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.Authentication;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Extension methods to add authentication capabilities to an HTTP application pipeline.
+ /// </summary>
+ public static class AuthAppBuilderExtensions
+ {
+ /// <summary>
+ /// Adds the <see cref="AuthenticationMiddleware"/> to the specified <see cref="IApplicationBuilder"/>, which enables authentication capabilities.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the middleware to.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseMiddleware<AuthenticationMiddleware>();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationBuilder.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationBuilder.cs
new file mode 100644
index 0000000000..401b1f488c
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationBuilder.cs
@@ -0,0 +1,120 @@
+// 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.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ /// <summary>
+ /// Used to configure authentication
+ /// </summary>
+ public class AuthenticationBuilder
+ {
+ /// <summary>
+ /// Constructor.
+ /// </summary>
+ /// <param name="services">The services being configured.</param>
+ public AuthenticationBuilder(IServiceCollection services)
+ => Services = services;
+
+ /// <summary>
+ /// The services being configured.
+ /// </summary>
+ public virtual IServiceCollection Services { get; }
+
+
+ private AuthenticationBuilder AddSchemeHelper<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
+ where TOptions : class, new()
+ where THandler : class, IAuthenticationHandler
+ {
+ Services.Configure<AuthenticationOptions>(o =>
+ {
+ o.AddScheme(authenticationScheme, scheme => {
+ scheme.HandlerType = typeof(THandler);
+ scheme.DisplayName = displayName;
+ });
+ });
+ if (configureOptions != null)
+ {
+ Services.Configure(authenticationScheme, configureOptions);
+ }
+ Services.AddTransient<THandler>();
+ return this;
+ }
+
+ /// <summary>
+ /// Adds a <see cref="AuthenticationScheme"/> which can be used by <see cref="IAuthenticationService"/>.
+ /// </summary>
+ /// <typeparam name="TOptions">The <see cref="AuthenticationSchemeOptions"/> type to configure the handler."/>.</typeparam>
+ /// <typeparam name="THandler">The <see cref="AuthenticationHandler{TOptions}"/> used to handle this scheme.</typeparam>
+ /// <param name="authenticationScheme">The name of this scheme.</param>
+ /// <param name="displayName">The display name of this scheme.</param>
+ /// <param name="configureOptions">Used to configure the scheme options.</param>
+ /// <returns>The builder.</returns>
+ public virtual AuthenticationBuilder AddScheme<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
+ where TOptions : AuthenticationSchemeOptions, new()
+ where THandler : AuthenticationHandler<TOptions>
+ => AddSchemeHelper<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
+
+ /// <summary>
+ /// Adds a <see cref="AuthenticationScheme"/> which can be used by <see cref="IAuthenticationService"/>.
+ /// </summary>
+ /// <typeparam name="TOptions">The <see cref="AuthenticationSchemeOptions"/> type to configure the handler."/>.</typeparam>
+ /// <typeparam name="THandler">The <see cref="AuthenticationHandler{TOptions}"/> used to handle this scheme.</typeparam>
+ /// <param name="authenticationScheme">The name of this scheme.</param>
+ /// <param name="configureOptions">Used to configure the scheme options.</param>
+ /// <returns>The builder.</returns>
+ public virtual AuthenticationBuilder AddScheme<TOptions, THandler>(string authenticationScheme, Action<TOptions> configureOptions)
+ where TOptions : AuthenticationSchemeOptions, new()
+ where THandler : AuthenticationHandler<TOptions>
+ => AddScheme<TOptions, THandler>(authenticationScheme, displayName: null, configureOptions: configureOptions);
+
+ /// <summary>
+ /// Adds a <see cref="RemoteAuthenticationHandler{TOptions}"/> based <see cref="AuthenticationScheme"/> that supports remote authentication
+ /// which can be used by <see cref="IAuthenticationService"/>.
+ /// </summary>
+ /// <typeparam name="TOptions">The <see cref="RemoteAuthenticationOptions"/> type to configure the handler."/>.</typeparam>
+ /// <typeparam name="THandler">The <see cref="RemoteAuthenticationHandler{TOptions}"/> used to handle this scheme.</typeparam>
+ /// <param name="authenticationScheme">The name of this scheme.</param>
+ /// <param name="displayName">The display name of this scheme.</param>
+ /// <param name="configureOptions">Used to configure the scheme options.</param>
+ /// <returns>The builder.</returns>
+ public virtual AuthenticationBuilder AddRemoteScheme<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
+ where TOptions : RemoteAuthenticationOptions, new()
+ where THandler : RemoteAuthenticationHandler<TOptions>
+ {
+ Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, EnsureSignInScheme<TOptions>>());
+ return AddScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions: configureOptions);
+ }
+
+ /// <summary>
+ /// Adds a <see cref="PolicySchemeHandler"/> based authentication handler which can be used to
+ /// redirect to other authentication schemes.
+ /// </summary>
+ /// <param name="authenticationScheme">The name of this scheme.</param>
+ /// <param name="displayName">The display name of this scheme.</param>
+ /// <param name="configureOptions">Used to configure the scheme options.</param>
+ /// <returns>The builder.</returns>
+ public virtual AuthenticationBuilder AddPolicyScheme(string authenticationScheme, string displayName, Action<PolicySchemeOptions> configureOptions)
+ => AddSchemeHelper<PolicySchemeOptions, PolicySchemeHandler>(authenticationScheme, displayName, configureOptions);
+
+ // Used to ensure that there's always a default sign in scheme that's not itself
+ private class EnsureSignInScheme<TOptions> : IPostConfigureOptions<TOptions> where TOptions : RemoteAuthenticationOptions
+ {
+ private readonly AuthenticationOptions _authOptions;
+
+ public EnsureSignInScheme(IOptions<AuthenticationOptions> authOptions)
+ {
+ _authOptions = authOptions.Value;
+ }
+
+ public void PostConfigure(string name, TOptions options)
+ {
+ options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme ?? _authOptions.DefaultScheme;
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs
new file mode 100644
index 0000000000..5c9a6473f1
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs
@@ -0,0 +1,244 @@
+// 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.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public abstract class AuthenticationHandler<TOptions> : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new()
+ {
+ private Task<AuthenticateResult> _authenticateTask;
+
+ public AuthenticationScheme Scheme { get; private set; }
+ public TOptions Options { get; private set; }
+ protected HttpContext Context { get; private set; }
+
+ protected HttpRequest Request
+ {
+ get => Context.Request;
+ }
+
+ protected HttpResponse Response
+ {
+ get => Context.Response;
+ }
+
+ protected PathString OriginalPath => Context.Features.Get<IAuthenticationFeature>()?.OriginalPath ?? Request.Path;
+
+ protected PathString OriginalPathBase => Context.Features.Get<IAuthenticationFeature>()?.OriginalPathBase ?? Request.PathBase;
+
+ protected ILogger Logger { get; }
+
+ protected UrlEncoder UrlEncoder { get; }
+
+ protected ISystemClock Clock { get; }
+
+ protected IOptionsMonitor<TOptions> OptionsMonitor { get; }
+
+ /// <summary>
+ /// The handler calls methods on the events which give the application control at certain points where processing is occurring.
+ /// If it is not provided a default instance is supplied which does nothing when the methods are called.
+ /// </summary>
+ protected virtual object Events { get; set; }
+
+ protected virtual string ClaimsIssuer => Options.ClaimsIssuer ?? Scheme.Name;
+
+ protected string CurrentUri
+ {
+ get => Request.Scheme + "://" + Request.Host + Request.PathBase + Request.Path + Request.QueryString;
+ }
+
+ protected AuthenticationHandler(IOptionsMonitor<TOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
+ {
+ Logger = logger.CreateLogger(this.GetType().FullName);
+ UrlEncoder = encoder;
+ Clock = clock;
+ OptionsMonitor = options;
+ }
+
+ /// <summary>
+ /// Initialize the handler, resolve the options and validate them.
+ /// </summary>
+ /// <param name="scheme"></param>
+ /// <param name="context"></param>
+ /// <returns></returns>
+ public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
+ {
+ if (scheme == null)
+ {
+ throw new ArgumentNullException(nameof(scheme));
+ }
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Scheme = scheme;
+ Context = context;
+
+ Options = OptionsMonitor.Get(Scheme.Name) ?? new TOptions();
+ Options.Validate(Scheme.Name);
+
+ await InitializeEventsAsync();
+ await InitializeHandlerAsync();
+ }
+
+ /// <summary>
+ /// Initializes the events object, called once per request by <see cref="InitializeAsync(AuthenticationScheme, HttpContext)"/>.
+ /// </summary>
+ protected virtual async Task InitializeEventsAsync()
+ {
+ Events = Options.Events;
+ if (Options.EventsType != null)
+ {
+ Events = Context.RequestServices.GetRequiredService(Options.EventsType);
+ }
+ Events = Events ?? await CreateEventsAsync();
+ }
+
+ /// <summary>
+ /// Creates a new instance of the events instance.
+ /// </summary>
+ /// <returns>A new instance of the events instance.</returns>
+ protected virtual Task<object> CreateEventsAsync() => Task.FromResult(new object());
+
+ /// <summary>
+ /// Called after options/events have been initialized for the handler to finish initializing itself.
+ /// </summary>
+ /// <returns>A task</returns>
+ protected virtual Task InitializeHandlerAsync() => Task.CompletedTask;
+
+ protected string BuildRedirectUri(string targetPath)
+ => Request.Scheme + "://" + Request.Host + OriginalPathBase + targetPath;
+
+ protected virtual string ResolveTarget(string scheme)
+ {
+ var target = scheme ?? Options.ForwardDefaultSelector?.Invoke(Context) ?? Options.ForwardDefault;
+
+ // Prevent self targetting
+ return string.Equals(target, Scheme.Name, StringComparison.Ordinal)
+ ? null
+ : target;
+ }
+
+ public async Task<AuthenticateResult> AuthenticateAsync()
+ {
+ var target = ResolveTarget(Options.ForwardAuthenticate);
+ if (target != null)
+ {
+ return await Context.AuthenticateAsync(target);
+ }
+
+ // Calling Authenticate more than once should always return the original value.
+ var result = await HandleAuthenticateOnceAsync();
+ if (result?.Failure == null)
+ {
+ var ticket = result?.Ticket;
+ if (ticket?.Principal != null)
+ {
+ Logger.AuthenticationSchemeAuthenticated(Scheme.Name);
+ }
+ else
+ {
+ Logger.AuthenticationSchemeNotAuthenticated(Scheme.Name);
+ }
+ }
+ else
+ {
+ Logger.AuthenticationSchemeNotAuthenticatedWithFailure(Scheme.Name, result.Failure.Message);
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Used to ensure HandleAuthenticateAsync is only invoked once. The subsequent calls
+ /// will return the same authenticate result.
+ /// </summary>
+ protected Task<AuthenticateResult> HandleAuthenticateOnceAsync()
+ {
+ if (_authenticateTask == null)
+ {
+ _authenticateTask = HandleAuthenticateAsync();
+ }
+
+ return _authenticateTask;
+ }
+
+ /// <summary>
+ /// Used to ensure HandleAuthenticateAsync is only invoked once safely. The subsequent
+ /// calls will return the same authentication result. Any exceptions will be converted
+ /// into a failed authentication result containing the exception.
+ /// </summary>
+ protected async Task<AuthenticateResult> HandleAuthenticateOnceSafeAsync()
+ {
+ try
+ {
+ return await HandleAuthenticateOnceAsync();
+ }
+ catch (Exception ex)
+ {
+ return AuthenticateResult.Fail(ex);
+ }
+ }
+
+ protected abstract Task<AuthenticateResult> HandleAuthenticateAsync();
+
+ /// <summary>
+ /// Override this method to handle Forbid.
+ /// </summary>
+ /// <param name="properties"></param>
+ /// <returns>A Task.</returns>
+ protected virtual Task HandleForbiddenAsync(AuthenticationProperties properties)
+ {
+ Response.StatusCode = 403;
+ return Task.CompletedTask;
+ }
+
+ /// <summary>
+ /// Override this method to deal with 401 challenge concerns, if an authentication scheme in question
+ /// deals an authentication interaction as part of it's request flow. (like adding a response header, or
+ /// changing the 401 result to 302 of a login page or external sign-in location.)
+ /// </summary>
+ /// <param name="properties"></param>
+ /// <returns>A Task.</returns>
+ protected virtual Task HandleChallengeAsync(AuthenticationProperties properties)
+ {
+ Response.StatusCode = 401;
+ return Task.CompletedTask;
+ }
+
+ public async Task ChallengeAsync(AuthenticationProperties properties)
+ {
+ var target = ResolveTarget(Options.ForwardChallenge);
+ if (target != null)
+ {
+ await Context.ChallengeAsync(target, properties);
+ return;
+ }
+
+ properties = properties ?? new AuthenticationProperties();
+ await HandleChallengeAsync(properties);
+ Logger.AuthenticationSchemeChallenged(Scheme.Name);
+ }
+
+ public async Task ForbidAsync(AuthenticationProperties properties)
+ {
+ var target = ResolveTarget(Options.ForwardForbid);
+ if (target != null)
+ {
+ await Context.ForbidAsync(target, properties);
+ return;
+ }
+
+ properties = properties ?? new AuthenticationProperties();
+ await HandleForbiddenAsync(properties);
+ Logger.AuthenticationSchemeForbidden(Scheme.Name);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs
new file mode 100644
index 0000000000..0c62cc3c39
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs
@@ -0,0 +1,64 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class AuthenticationMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
+ {
+ if (next == null)
+ {
+ throw new ArgumentNullException(nameof(next));
+ }
+ if (schemes == null)
+ {
+ throw new ArgumentNullException(nameof(schemes));
+ }
+
+ _next = next;
+ Schemes = schemes;
+ }
+
+ public IAuthenticationSchemeProvider Schemes { get; set; }
+
+ public async Task Invoke(HttpContext context)
+ {
+ context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
+ {
+ OriginalPath = context.Request.Path,
+ OriginalPathBase = context.Request.PathBase
+ });
+
+ // Give any IAuthenticationRequestHandler schemes a chance to handle the request
+ var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
+ foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
+ {
+ var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
+ if (handler != null && await handler.HandleRequestAsync())
+ {
+ return;
+ }
+ }
+
+ var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
+ if (defaultAuthenticate != null)
+ {
+ var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
+ if (result?.Principal != null)
+ {
+ context.User = result.Principal;
+ }
+ }
+
+ await _next(context);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs
new file mode 100644
index 0000000000..a547d203b4
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs
@@ -0,0 +1,93 @@
+// 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.Authentication
+{
+ /// <summary>
+ /// Contains the options used by the <see cref="AuthenticationHandler{T}"/>.
+ /// </summary>
+ public class AuthenticationSchemeOptions
+ {
+ /// <summary>
+ /// Check that the options are valid. Should throw an exception if things are not ok.
+ /// </summary>
+ public virtual void Validate() { }
+
+ /// <summary>
+ /// Checks that the options are valid for a specific scheme
+ /// </summary>
+ /// <param name="scheme">The scheme being validated.</param>
+ public virtual void Validate(string scheme)
+ => Validate();
+
+ /// <summary>
+ /// Gets or sets the issuer that should be used for any claims that are created
+ /// </summary>
+ public string ClaimsIssuer { get; set; }
+
+ /// <summary>
+ /// Instance used for events
+ /// </summary>
+ public object Events { get; set; }
+
+ /// <summary>
+ /// If set, will be used as the service type to get the Events instance instead of the property.
+ /// </summary>
+ public Type EventsType { get; set; }
+
+ /// <summary>
+ /// If set, this specifies a default scheme that authentication handlers should forward all authentication operations to
+ /// by default. The default forwarding logic will check the most specific ForwardAuthenticate/Challenge/Forbid/SignIn/SignOut
+ /// setting first, followed by checking the ForwardDefaultSelector, followed by ForwardDefault. The first non null result
+ /// will be used as the target scheme to forward to.
+ /// </summary>
+ public string ForwardDefault { get; set; }
+
+ /// <summary>
+ /// If set, this specifies the target scheme that this scheme should forward AuthenticateAsync calls to.
+ /// For example Context.AuthenticateAsync("ThisScheme") => Context.AuthenticateAsync("ForwardAuthenticateValue");
+ /// Set the target to the current scheme to disable forwarding and allow normal processing.
+ /// </summary>
+ public string ForwardAuthenticate { get; set; }
+
+ /// <summary>
+ /// If set, this specifies the target scheme that this scheme should forward ChallengeAsync calls to.
+ /// For example Context.ChallengeAsync("ThisScheme") => Context.ChallengeAsync("ForwardChallengeValue");
+ /// Set the target to the current scheme to disable forwarding and allow normal processing.
+ /// </summary>
+ public string ForwardChallenge { get; set; }
+
+ /// <summary>
+ /// If set, this specifies the target scheme that this scheme should forward ForbidAsync calls to.
+ /// For example Context.ForbidAsync("ThisScheme") => Context.ForbidAsync("ForwardForbidValue");
+ /// Set the target to the current scheme to disable forwarding and allow normal processing.
+ /// </summary>
+ public string ForwardForbid { get; set; }
+
+ /// <summary>
+ /// If set, this specifies the target scheme that this scheme should forward SignInAsync calls to.
+ /// For example Context.SignInAsync("ThisScheme") => Context.SignInAsync("ForwardSignInValue");
+ /// Set the target to the current scheme to disable forwarding and allow normal processing.
+ /// </summary>
+ public string ForwardSignIn { get; set; }
+
+ /// <summary>
+ /// If set, this specifies the target scheme that this scheme should forward SignOutAsync calls to.
+ /// For example Context.SignOutAsync("ThisScheme") => Context.SignInAsync("ForwardSignOutValue");
+ /// Set the target to the current scheme to disable forwarding and allow normal processing.
+ /// </summary>
+ public string ForwardSignOut { get; set; }
+
+ /// <summary>
+ /// Used to select a default scheme for the current request that authentication handlers should forward all authentication operations to
+ /// by default. The default forwarding logic will check the most specific ForwardAuthenticate/Challenge/Forbid/SignIn/SignOut
+ /// setting first, followed by checking the ForwardDefaultSelector, followed by ForwardDefault. The first non null result
+ /// will be used as the target scheme to forward to.
+ /// </summary>
+ public Func<HttpContext, string> ForwardDefaultSelector { get; set; }
+
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationServiceCollectionExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..b274eaace4
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/AuthenticationServiceCollectionExtensions.cs
@@ -0,0 +1,108 @@
+// 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.Authentication;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ /// <summary>
+ /// Extension methods for setting up authentication services in an <see cref="IServiceCollection" />.
+ /// </summary>
+ public static class AuthenticationServiceCollectionExtensions
+ {
+ public static AuthenticationBuilder AddAuthentication(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.AddAuthenticationCore();
+ services.AddDataProtection();
+ services.AddWebEncoders();
+ services.TryAddSingleton<ISystemClock, SystemClock>();
+ return new AuthenticationBuilder(services);
+ }
+
+ public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, string defaultScheme)
+ => services.AddAuthentication(o => o.DefaultScheme = defaultScheme);
+
+ public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, Action<AuthenticationOptions> configureOptions) {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (configureOptions == null)
+ {
+ throw new ArgumentNullException(nameof(configureOptions));
+ }
+
+ var builder = services.AddAuthentication();
+ services.Configure(configureOptions);
+ return builder;
+ }
+
+ [Obsolete("AddScheme is obsolete. Use AddAuthentication().AddScheme instead.")]
+ public static IServiceCollection AddScheme<TOptions, THandler>(this IServiceCollection services, string authenticationScheme, string displayName, Action<AuthenticationSchemeBuilder> configureScheme, Action<TOptions> configureOptions)
+ where TOptions : AuthenticationSchemeOptions, new()
+ where THandler : AuthenticationHandler<TOptions>
+ {
+ services.AddAuthentication(o =>
+ {
+ o.AddScheme(authenticationScheme, scheme => {
+ scheme.HandlerType = typeof(THandler);
+ scheme.DisplayName = displayName;
+ configureScheme?.Invoke(scheme);
+ });
+ });
+ if (configureOptions != null)
+ {
+ services.Configure(authenticationScheme, configureOptions);
+ }
+ services.AddTransient<THandler>();
+ return services;
+ }
+
+ [Obsolete("AddScheme is obsolete. Use AddAuthentication().AddScheme instead.")]
+ public static IServiceCollection AddScheme<TOptions, THandler>(this IServiceCollection services, string authenticationScheme, Action<TOptions> configureOptions)
+ where TOptions : AuthenticationSchemeOptions, new()
+ where THandler : AuthenticationHandler<TOptions>
+ => services.AddScheme<TOptions, THandler>(authenticationScheme, displayName: null, configureScheme: null, configureOptions: configureOptions);
+
+ [Obsolete("AddScheme is obsolete. Use AddAuthentication().AddScheme instead.")]
+ public static IServiceCollection AddScheme<TOptions, THandler>(this IServiceCollection services, string authenticationScheme, string displayName, Action<TOptions> configureOptions)
+ where TOptions : AuthenticationSchemeOptions, new()
+ where THandler : AuthenticationHandler<TOptions>
+ => services.AddScheme<TOptions, THandler>(authenticationScheme, displayName, configureScheme: null, configureOptions: configureOptions);
+
+ [Obsolete("AddScheme is obsolete. Use AddAuthentication().AddScheme instead.")]
+ public static IServiceCollection AddRemoteScheme<TOptions, THandler>(this IServiceCollection services, string authenticationScheme, string displayName, Action<TOptions> configureOptions)
+ where TOptions : RemoteAuthenticationOptions, new()
+ where THandler : RemoteAuthenticationHandler<TOptions>
+ {
+ services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, EnsureSignInScheme<TOptions>>());
+ return services.AddScheme<TOptions, THandler>(authenticationScheme, displayName, configureScheme: null, configureOptions: configureOptions);
+ }
+
+ // Used to ensure that there's always a sign in scheme
+ private class EnsureSignInScheme<TOptions> : IPostConfigureOptions<TOptions> where TOptions : RemoteAuthenticationOptions
+ {
+ private readonly AuthenticationOptions _authOptions;
+
+ public EnsureSignInScheme(IOptions<AuthenticationOptions> authOptions)
+ {
+ _authOptions = authOptions.Value;
+ }
+
+ public void PostConfigure(string name, TOptions options)
+ {
+ options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme;
+ }
+ }
+
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/IDataSerializer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/IDataSerializer.cs
new file mode 100644
index 0000000000..ad9c523005
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/IDataSerializer.cs
@@ -0,0 +1,11 @@
+// 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.Authentication
+{
+ public interface IDataSerializer<TModel>
+ {
+ byte[] Serialize(TModel model);
+ TModel Deserialize(byte[] data);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/ISecureDataFormat.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/ISecureDataFormat.cs
new file mode 100644
index 0000000000..73b1b882b5
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/ISecureDataFormat.cs
@@ -0,0 +1,13 @@
+// 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.Authentication
+{
+ public interface ISecureDataFormat<TData>
+ {
+ string Protect(TData data);
+ string Protect(TData data, string purpose);
+ TData Unprotect(string protectedText);
+ TData Unprotect(string protectedText, string purpose);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesDataFormat.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesDataFormat.cs
new file mode 100644
index 0000000000..3d31e4bd2d
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesDataFormat.cs
@@ -0,0 +1,16 @@
+// 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.Authentication;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class PropertiesDataFormat : SecureDataFormat<AuthenticationProperties>
+ {
+ public PropertiesDataFormat(IDataProtector protector)
+ : base(new PropertiesSerializer(), protector)
+ {
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesSerializer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesSerializer.cs
new file mode 100644
index 0000000000..dd30b45ae0
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/PropertiesSerializer.cs
@@ -0,0 +1,87 @@
+// 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.IO;
+using Microsoft.AspNetCore.Http.Authentication;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class PropertiesSerializer : IDataSerializer<AuthenticationProperties>
+ {
+ private const int FormatVersion = 1;
+
+ public static PropertiesSerializer Default { get; } = new PropertiesSerializer();
+
+ public virtual byte[] Serialize(AuthenticationProperties model)
+ {
+ using (var memory = new MemoryStream())
+ {
+ using (var writer = new BinaryWriter(memory))
+ {
+ Write(writer, model);
+ writer.Flush();
+ return memory.ToArray();
+ }
+ }
+ }
+
+ public virtual AuthenticationProperties Deserialize(byte[] data)
+ {
+ using (var memory = new MemoryStream(data))
+ {
+ using (var reader = new BinaryReader(memory))
+ {
+ return Read(reader);
+ }
+ }
+ }
+
+ public virtual void Write(BinaryWriter writer, AuthenticationProperties properties)
+ {
+ if (writer == null)
+ {
+ throw new ArgumentNullException(nameof(writer));
+ }
+
+ if (properties == null)
+ {
+ throw new ArgumentNullException(nameof(properties));
+ }
+
+ writer.Write(FormatVersion);
+ writer.Write(properties.Items.Count);
+
+ foreach (var item in properties.Items)
+ {
+ writer.Write(item.Key ?? string.Empty);
+ writer.Write(item.Value ?? string.Empty);
+ }
+ }
+
+ public virtual AuthenticationProperties Read(BinaryReader reader)
+ {
+ if (reader == null)
+ {
+ throw new ArgumentNullException(nameof(reader));
+ }
+
+ if (reader.ReadInt32() != FormatVersion)
+ {
+ return null;
+ }
+
+ var count = reader.ReadInt32();
+ var extra = new Dictionary<string, string>(count);
+
+ for (var index = 0; index != count; ++index)
+ {
+ string key = reader.ReadString();
+ string value = reader.ReadString();
+ extra.Add(key, value);
+ }
+ return new AuthenticationProperties(extra);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/SecureDataFormat.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/SecureDataFormat.cs
new file mode 100644
index 0000000000..f35025d8bb
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/SecureDataFormat.cs
@@ -0,0 +1,79 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class SecureDataFormat<TData> : ISecureDataFormat<TData>
+ {
+ private readonly IDataSerializer<TData> _serializer;
+ private readonly IDataProtector _protector;
+
+ public SecureDataFormat(IDataSerializer<TData> serializer, IDataProtector protector)
+ {
+ _serializer = serializer;
+ _protector = protector;
+ }
+
+ public string Protect(TData data)
+ {
+ return Protect(data, purpose: null);
+ }
+
+ public string Protect(TData data, string purpose)
+ {
+ var userData = _serializer.Serialize(data);
+
+ var protector = _protector;
+ if (!string.IsNullOrEmpty(purpose))
+ {
+ protector = protector.CreateProtector(purpose);
+ }
+
+ var protectedData = protector.Protect(userData);
+ return Base64UrlTextEncoder.Encode(protectedData);
+ }
+
+ public TData Unprotect(string protectedText)
+ {
+ return Unprotect(protectedText, purpose: null);
+ }
+
+ public TData Unprotect(string protectedText, string purpose)
+ {
+ try
+ {
+ if (protectedText == null)
+ {
+ return default(TData);
+ }
+
+ var protectedData = Base64UrlTextEncoder.Decode(protectedText);
+ if (protectedData == null)
+ {
+ return default(TData);
+ }
+
+ var protector = _protector;
+ if (!string.IsNullOrEmpty(purpose))
+ {
+ protector = protector.CreateProtector(purpose);
+ }
+
+ var userData = protector.Unprotect(protectedData);
+ if (userData == null)
+ {
+ return default(TData);
+ }
+
+ return _serializer.Deserialize(userData);
+ }
+ catch
+ {
+ // TODO trace exception, but do not leak other information
+ return default(TData);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TextEncoder.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TextEncoder.cs
new file mode 100644
index 0000000000..1f7ecc7184
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TextEncoder.cs
@@ -0,0 +1,30 @@
+// 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.Authentication
+{
+ public static class Base64UrlTextEncoder
+ {
+ /// <summary>
+ /// Encodes supplied data into Base64 and replaces any URL encodable characters into non-URL encodable
+ /// characters.
+ /// </summary>
+ /// <param name="data">Data to be encoded.</param>
+ /// <returns>Base64 encoded string modified with non-URL encodable characters</returns>
+ public static string Encode(byte[] data)
+ {
+ return WebUtilities.WebEncoders.Base64UrlEncode(data);
+ }
+
+ /// <summary>
+ /// Decodes supplied string by replacing the non-URL encodable characters with URL encodable characters and
+ /// then decodes the Base64 string.
+ /// </summary>
+ /// <param name="text">The string to be decoded.</param>
+ /// <returns>The decoded data.</returns>
+ public static byte[] Decode(string text)
+ {
+ return WebUtilities.WebEncoders.Base64UrlDecode(text);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketDataFormat.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketDataFormat.cs
new file mode 100644
index 0000000000..e43943cfc8
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketDataFormat.cs
@@ -0,0 +1,15 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class TicketDataFormat : SecureDataFormat<AuthenticationTicket>
+ {
+ public TicketDataFormat(IDataProtector protector)
+ : base(TicketSerializer.Default, protector)
+ {
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketSerializer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketSerializer.cs
new file mode 100644
index 0000000000..e33ec71725
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Data/TicketSerializer.cs
@@ -0,0 +1,275 @@
+// 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 System.Linq;
+using System.Security.Claims;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ // This MUST be kept in sync with Microsoft.Owin.Security.Interop.AspNetTicketSerializer
+ public class TicketSerializer : IDataSerializer<AuthenticationTicket>
+ {
+ private const string DefaultStringPlaceholder = "\0";
+ private const int FormatVersion = 5;
+
+ public static TicketSerializer Default { get; } = new TicketSerializer();
+
+ public virtual byte[] Serialize(AuthenticationTicket ticket)
+ {
+ using (var memory = new MemoryStream())
+ {
+ using (var writer = new BinaryWriter(memory))
+ {
+ Write(writer, ticket);
+ }
+ return memory.ToArray();
+ }
+ }
+
+ public virtual AuthenticationTicket Deserialize(byte[] data)
+ {
+ using (var memory = new MemoryStream(data))
+ {
+ using (var reader = new BinaryReader(memory))
+ {
+ return Read(reader);
+ }
+ }
+ }
+
+ public virtual void Write(BinaryWriter writer, AuthenticationTicket ticket)
+ {
+ if (writer == null)
+ {
+ throw new ArgumentNullException(nameof(writer));
+ }
+
+ if (ticket == null)
+ {
+ throw new ArgumentNullException(nameof(ticket));
+ }
+
+ writer.Write(FormatVersion);
+ writer.Write(ticket.AuthenticationScheme);
+
+ // Write the number of identities contained in the principal.
+ var principal = ticket.Principal;
+ writer.Write(principal.Identities.Count());
+
+ foreach (var identity in principal.Identities)
+ {
+ WriteIdentity(writer, identity);
+ }
+
+ PropertiesSerializer.Default.Write(writer, ticket.Properties);
+ }
+
+ protected virtual void WriteIdentity(BinaryWriter writer, ClaimsIdentity identity)
+ {
+ if (writer == null)
+ {
+ throw new ArgumentNullException(nameof(writer));
+ }
+
+ if (identity == null)
+ {
+ throw new ArgumentNullException(nameof(identity));
+ }
+
+ var authenticationType = identity.AuthenticationType ?? string.Empty;
+
+ writer.Write(authenticationType);
+ WriteWithDefault(writer, identity.NameClaimType, ClaimsIdentity.DefaultNameClaimType);
+ WriteWithDefault(writer, identity.RoleClaimType, ClaimsIdentity.DefaultRoleClaimType);
+
+ // Write the number of claims contained in the identity.
+ writer.Write(identity.Claims.Count());
+
+ foreach (var claim in identity.Claims)
+ {
+ WriteClaim(writer, claim);
+ }
+
+ var bootstrap = identity.BootstrapContext as string;
+ if (!string.IsNullOrEmpty(bootstrap))
+ {
+ writer.Write(true);
+ writer.Write(bootstrap);
+ }
+ else
+ {
+ writer.Write(false);
+ }
+
+ if (identity.Actor != null)
+ {
+ writer.Write(true);
+ WriteIdentity(writer, identity.Actor);
+ }
+ else
+ {
+ writer.Write(false);
+ }
+ }
+
+ protected virtual void WriteClaim(BinaryWriter writer, Claim claim)
+ {
+ if (writer == null)
+ {
+ throw new ArgumentNullException(nameof(writer));
+ }
+
+ if (claim == null)
+ {
+ throw new ArgumentNullException(nameof(claim));
+ }
+
+ WriteWithDefault(writer, claim.Type, claim.Subject?.NameClaimType ?? ClaimsIdentity.DefaultNameClaimType);
+ writer.Write(claim.Value);
+ WriteWithDefault(writer, claim.ValueType, ClaimValueTypes.String);
+ WriteWithDefault(writer, claim.Issuer, ClaimsIdentity.DefaultIssuer);
+ WriteWithDefault(writer, claim.OriginalIssuer, claim.Issuer);
+
+ // Write the number of properties contained in the claim.
+ writer.Write(claim.Properties.Count);
+
+ foreach (var property in claim.Properties)
+ {
+ writer.Write(property.Key ?? string.Empty);
+ writer.Write(property.Value ?? string.Empty);
+ }
+ }
+
+ public virtual AuthenticationTicket Read(BinaryReader reader)
+ {
+ if (reader == null)
+ {
+ throw new ArgumentNullException(nameof(reader));
+ }
+
+ if (reader.ReadInt32() != FormatVersion)
+ {
+ return null;
+ }
+
+ var scheme = reader.ReadString();
+
+ // Read the number of identities stored
+ // in the serialized payload.
+ var count = reader.ReadInt32();
+ if (count < 0)
+ {
+ return null;
+ }
+
+ var identities = new ClaimsIdentity[count];
+ for (var index = 0; index != count; ++index)
+ {
+ identities[index] = ReadIdentity(reader);
+ }
+
+ var properties = PropertiesSerializer.Default.Read(reader);
+
+ return new AuthenticationTicket(new ClaimsPrincipal(identities), properties, scheme);
+ }
+
+ protected virtual ClaimsIdentity ReadIdentity(BinaryReader reader)
+ {
+ if (reader == null)
+ {
+ throw new ArgumentNullException(nameof(reader));
+ }
+
+ var authenticationType = reader.ReadString();
+ var nameClaimType = ReadWithDefault(reader, ClaimsIdentity.DefaultNameClaimType);
+ var roleClaimType = ReadWithDefault(reader, ClaimsIdentity.DefaultRoleClaimType);
+
+ // Read the number of claims contained
+ // in the serialized identity.
+ var count = reader.ReadInt32();
+
+ var identity = new ClaimsIdentity(authenticationType, nameClaimType, roleClaimType);
+
+ for (int index = 0; index != count; ++index)
+ {
+ var claim = ReadClaim(reader, identity);
+
+ identity.AddClaim(claim);
+ }
+
+ // Determine whether the identity
+ // has a bootstrap context attached.
+ if (reader.ReadBoolean())
+ {
+ identity.BootstrapContext = reader.ReadString();
+ }
+
+ // Determine whether the identity
+ // has an actor identity attached.
+ if (reader.ReadBoolean())
+ {
+ identity.Actor = ReadIdentity(reader);
+ }
+
+ return identity;
+ }
+
+ protected virtual Claim ReadClaim(BinaryReader reader, ClaimsIdentity identity)
+ {
+ if (reader == null)
+ {
+ throw new ArgumentNullException(nameof(reader));
+ }
+
+ if (identity == null)
+ {
+ throw new ArgumentNullException(nameof(identity));
+ }
+
+ var type = ReadWithDefault(reader, identity.NameClaimType);
+ var value = reader.ReadString();
+ var valueType = ReadWithDefault(reader, ClaimValueTypes.String);
+ var issuer = ReadWithDefault(reader, ClaimsIdentity.DefaultIssuer);
+ var originalIssuer = ReadWithDefault(reader, issuer);
+
+ var claim = new Claim(type, value, valueType, issuer, originalIssuer, identity);
+
+ // Read the number of properties stored in the claim.
+ var count = reader.ReadInt32();
+
+ for (var index = 0; index != count; ++index)
+ {
+ var key = reader.ReadString();
+ var propertyValue = reader.ReadString();
+
+ claim.Properties.Add(key, propertyValue);
+ }
+
+ return claim;
+ }
+
+ private static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue)
+ {
+ if (string.Equals(value, defaultValue, StringComparison.Ordinal))
+ {
+ writer.Write(DefaultStringPlaceholder);
+ }
+ else
+ {
+ writer.Write(value);
+ }
+ }
+
+ private static string ReadWithDefault(BinaryReader reader, string defaultValue)
+ {
+ var value = reader.ReadString();
+ if (string.Equals(value, DefaultStringPlaceholder, StringComparison.Ordinal))
+ {
+ return defaultValue;
+ }
+ return value;
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/BaseContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/BaseContext.cs
new file mode 100644
index 0000000000..915fc2377f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/BaseContext.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;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ /// <summary>
+ /// Base class used by other context classes.
+ /// </summary>
+ public abstract class BaseContext<TOptions> where TOptions : AuthenticationSchemeOptions
+ {
+ /// <summary>
+ /// Constructor.
+ /// </summary>
+ /// <param name="context">The context.</param>
+ /// <param name="scheme">The authentication scheme.</param>
+ /// <param name="options">The authentication options associated with the scheme.</param>
+ protected BaseContext(HttpContext context, AuthenticationScheme scheme, TOptions options)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+ if (scheme == null)
+ {
+ throw new ArgumentNullException(nameof(scheme));
+ }
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ HttpContext = context;
+ Scheme = scheme;
+ Options = options;
+ }
+
+ /// <summary>
+ /// The authentication scheme.
+ /// </summary>
+ public AuthenticationScheme Scheme { get; }
+
+ /// <summary>
+ /// Gets the authentication options associated with the scheme.
+ /// </summary>
+ public TOptions Options { get; }
+
+ /// <summary>
+ /// The context.
+ /// </summary>
+ public HttpContext HttpContext { get; }
+
+ /// <summary>
+ /// The request.
+ /// </summary>
+ public HttpRequest Request => HttpContext.Request;
+
+ /// <summary>
+ /// The response.
+ /// </summary>
+ public HttpResponse Response => HttpContext.Response;
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/HandleRequestContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/HandleRequestContext.cs
new file mode 100644
index 0000000000..52dd9ce12f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/HandleRequestContext.cs
@@ -0,0 +1,32 @@
+// 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.Authentication
+{
+ public class HandleRequestContext<TOptions> : BaseContext<TOptions> where TOptions : AuthenticationSchemeOptions
+ {
+ protected HandleRequestContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ TOptions options)
+ : base(context, scheme, options) { }
+
+ /// <summary>
+ /// The <see cref="HandleRequestResult"/> which is used by the handler.
+ /// </summary>
+ public HandleRequestResult Result { get; protected set; }
+
+ /// <summary>
+ /// Discontinue all processing for this request and return to the client.
+ /// The caller is responsible for generating the full response.
+ /// </summary>
+ public void HandleResponse() => Result = HandleRequestResult.Handle();
+
+ /// <summary>
+ /// Discontinue processing the request in the current handler.
+ /// </summary>
+ public void SkipHandler() => Result = HandleRequestResult.SkipHandler();
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/PrincipalContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/PrincipalContext.cs
new file mode 100644
index 0000000000..8bf40760a1
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/PrincipalContext.cs
@@ -0,0 +1,30 @@
+// 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 Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ /// <summary>
+ /// Base context for authentication events which deal with a ClaimsPrincipal.
+ /// </summary>
+ public abstract class PrincipalContext<TOptions> : PropertiesContext<TOptions> where TOptions : AuthenticationSchemeOptions
+ {
+ /// <summary>
+ /// Constructor.
+ /// </summary>
+ /// <param name="context">The context.</param>
+ /// <param name="scheme">The authentication scheme.</param>
+ /// <param name="options">The authentication options associated with the scheme.</param>
+ /// <param name="properties">The authentication properties.</param>
+ protected PrincipalContext(HttpContext context, AuthenticationScheme scheme, TOptions options, AuthenticationProperties properties)
+ : base(context, scheme, options, properties) { }
+
+ /// <summary>
+ /// Gets the <see cref="ClaimsPrincipal"/> containing the user claims.
+ /// </summary>
+ public virtual ClaimsPrincipal Principal { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/PropertiesContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/PropertiesContext.cs
new file mode 100644
index 0000000000..f1730d0d7f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/PropertiesContext.cs
@@ -0,0 +1,31 @@
+// 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.Authentication
+{
+ /// <summary>
+ /// Base context for authentication events which contain <see cref="AuthenticationProperties"/>.
+ /// </summary>
+ public abstract class PropertiesContext<TOptions> : BaseContext<TOptions> where TOptions : AuthenticationSchemeOptions
+ {
+ /// <summary>
+ /// Constructor.
+ /// </summary>
+ /// <param name="context">The context.</param>
+ /// <param name="scheme">The authentication scheme.</param>
+ /// <param name="options">The authentication options associated with the scheme.</param>
+ /// <param name="properties">The authentication properties.</param>
+ protected PropertiesContext(HttpContext context, AuthenticationScheme scheme, TOptions options, AuthenticationProperties properties)
+ : base(context, scheme, options)
+ {
+ Properties = properties ?? new AuthenticationProperties();
+ }
+
+ /// <summary>
+ /// Gets or sets the <see cref="AuthenticationProperties"/>.
+ /// </summary>
+ public virtual AuthenticationProperties Properties { get; protected set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RedirectContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RedirectContext.cs
new file mode 100644
index 0000000000..dac24cafa6
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RedirectContext.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 Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ /// <summary>
+ /// Context passed for redirect events.
+ /// </summary>
+ public class RedirectContext<TOptions> : PropertiesContext<TOptions> where TOptions : AuthenticationSchemeOptions
+ {
+ /// <summary>
+ /// Creates a new context object.
+ /// </summary>
+ /// <param name="context">The HTTP request context</param>
+ /// <param name="scheme">The scheme data</param>
+ /// <param name="options">The handler options</param>
+ /// <param name="redirectUri">The initial redirect URI</param>
+ /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
+ public RedirectContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ TOptions options,
+ AuthenticationProperties properties,
+ string redirectUri)
+ : base(context, scheme, options, properties)
+ {
+ Properties = properties;
+ RedirectUri = redirectUri;
+ }
+
+ /// <summary>
+ /// Gets or Sets the URI used for the redirect operation.
+ /// </summary>
+ public string RedirectUri { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationContext.cs
new file mode 100644
index 0000000000..b7a0168798
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationContext.cs
@@ -0,0 +1,49 @@
+// 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 Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ /// <summary>
+ /// Base context for remote authentication.
+ /// </summary>
+ public abstract class RemoteAuthenticationContext<TOptions> : HandleRequestContext<TOptions> where TOptions : AuthenticationSchemeOptions
+ {
+ /// <summary>
+ /// Constructor.
+ /// </summary>
+ /// <param name="context">The context.</param>
+ /// <param name="scheme">The authentication scheme.</param>
+ /// <param name="options">The authentication options associated with the scheme.</param>
+ /// <param name="properties">The authentication properties.</param>
+ protected RemoteAuthenticationContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ TOptions options,
+ AuthenticationProperties properties)
+ : base(context, scheme, options)
+ => Properties = properties ?? new AuthenticationProperties();
+
+ /// <summary>
+ /// Gets the <see cref="ClaimsPrincipal"/> containing the user claims.
+ /// </summary>
+ public ClaimsPrincipal Principal { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="AuthenticationProperties"/>.
+ /// </summary>
+ public virtual AuthenticationProperties Properties { get; set; }
+
+ /// <summary>
+ /// Calls success creating a ticket with the <see cref="Principal"/> and <see cref="Properties"/>.
+ /// </summary>
+ public void Success() => Result = HandleRequestResult.Success(new AuthenticationTicket(Principal, Properties, Scheme.Name));
+
+ public void Fail(Exception failure) => Result = HandleRequestResult.Fail(failure);
+
+ public void Fail(string failureMessage) => Result = HandleRequestResult.Fail(failureMessage);
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.cs
new file mode 100644
index 0000000000..ca0f4a5c01
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.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;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class RemoteAuthenticationEvents
+ {
+ public Func<RemoteFailureContext, Task> OnRemoteFailure { get; set; } = context => Task.CompletedTask;
+
+ public Func<TicketReceivedContext, Task> OnTicketReceived { get; set; } = context => Task.CompletedTask;
+
+ /// <summary>
+ /// Invoked when there is a remote failure
+ /// </summary>
+ public virtual Task RemoteFailure(RemoteFailureContext context) => OnRemoteFailure(context);
+
+ /// <summary>
+ /// Invoked after the remote ticket has been received.
+ /// </summary>
+ public virtual Task TicketReceived(TicketReceivedContext context) => OnTicketReceived(context);
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteFailureContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteFailureContext.cs
new file mode 100644
index 0000000000..6b3598f40a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/RemoteFailureContext.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;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ /// <summary>
+ /// Provides failure context information to handler providers.
+ /// </summary>
+ public class RemoteFailureContext : HandleRequestContext<RemoteAuthenticationOptions>
+ {
+ public RemoteFailureContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ RemoteAuthenticationOptions options,
+ Exception failure)
+ : base(context, scheme, options)
+ {
+ Failure = failure;
+ }
+
+ /// <summary>
+ /// User friendly error message for the error.
+ /// </summary>
+ public Exception Failure { get; set; }
+
+ /// <summary>
+ /// Additional state values for the authentication session.
+ /// </summary>
+ public AuthenticationProperties Properties { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/ResultContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/ResultContext.cs
new file mode 100644
index 0000000000..12b21f4bf6
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/ResultContext.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;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ /// <summary>
+ /// Base context for events that produce AuthenticateResults.
+ /// </summary>
+ public abstract class ResultContext<TOptions> : BaseContext<TOptions> where TOptions : AuthenticationSchemeOptions
+ {
+ /// <summary>
+ /// Constructor.
+ /// </summary>
+ /// <param name="context">The context.</param>
+ /// <param name="scheme">The authentication scheme.</param>
+ /// <param name="options">The authentication options associated with the scheme.</param>
+ protected ResultContext(HttpContext context, AuthenticationScheme scheme, TOptions options)
+ : base(context, scheme, options) { }
+
+ /// <summary>
+ /// Gets or sets the <see cref="ClaimsPrincipal"/> containing the user claims.
+ /// </summary>
+ public ClaimsPrincipal Principal { get; set; }
+
+ private AuthenticationProperties _properties;
+ /// <summary>
+ /// Gets or sets the <see cref="AuthenticationProperties"/>.
+ /// </summary>
+ public AuthenticationProperties Properties {
+ get => _properties ?? (_properties = new AuthenticationProperties());
+ set => _properties = value;
+ }
+
+ /// <summary>
+ /// Gets the <see cref="AuthenticateResult"/> result.
+ /// </summary>
+ public AuthenticateResult Result { get; private set; }
+
+ /// <summary>
+ /// Calls success creating a ticket with the <see cref="Principal"/> and <see cref="Properties"/>.
+ /// </summary>
+ public void Success() => Result = HandleRequestResult.Success(new AuthenticationTicket(Principal, Properties, Scheme.Name));
+
+ /// <summary>
+ /// Indicates that there was no information returned for this authentication scheme.
+ /// </summary>
+ public void NoResult() => Result = AuthenticateResult.NoResult();
+
+ /// <summary>
+ /// Indicates that there was a failure during authentication.
+ /// </summary>
+ /// <param name="failure"></param>
+ public void Fail(Exception failure) => Result = AuthenticateResult.Fail(failure);
+
+ /// <summary>
+ /// Indicates that there was a failure during authentication.
+ /// </summary>
+ /// <param name="failureMessage"></param>
+ public void Fail(string failureMessage) => Result = AuthenticateResult.Fail(failureMessage);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Events/TicketReceivedContext.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/TicketReceivedContext.cs
new file mode 100644
index 0000000000..51b77a37fa
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Events/TicketReceivedContext.cs
@@ -0,0 +1,24 @@
+// 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.Claims;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ /// <summary>
+ /// Provides context information to handler providers.
+ /// </summary>
+ public class TicketReceivedContext : RemoteAuthenticationContext<RemoteAuthenticationOptions>
+ {
+ public TicketReceivedContext(
+ HttpContext context,
+ AuthenticationScheme scheme,
+ RemoteAuthenticationOptions options,
+ AuthenticationTicket ticket)
+ : base(context, scheme, options, ticket?.Properties)
+ => Principal = ticket?.Principal;
+
+ public string ReturnUri { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs
new file mode 100644
index 0000000000..da9b6ea01c
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs
@@ -0,0 +1,96 @@
+// 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.Authentication
+{
+ /// <summary>
+ /// Contains the result of an Authenticate call
+ /// </summary>
+ public class HandleRequestResult : AuthenticateResult
+ {
+ /// <summary>
+ /// Indicates that stage of authentication was directly handled by
+ /// user intervention and no further processing should be attempted.
+ /// </summary>
+ public bool Handled { get; private set; }
+
+ /// <summary>
+ /// Indicates that the default authentication logic should be
+ /// skipped and that the rest of the pipeline should be invoked.
+ /// </summary>
+ public bool Skipped { get; private set; }
+
+ /// <summary>
+ /// Indicates that authentication was successful.
+ /// </summary>
+ /// <param name="ticket">The ticket representing the authentication result.</param>
+ /// <returns>The result.</returns>
+ public static new HandleRequestResult Success(AuthenticationTicket ticket)
+ {
+ if (ticket == null)
+ {
+ throw new ArgumentNullException(nameof(ticket));
+ }
+ return new HandleRequestResult() { Ticket = ticket, Properties = ticket.Properties };
+ }
+
+ /// <summary>
+ /// Indicates that there was a failure during authentication.
+ /// </summary>
+ /// <param name="failure">The failure exception.</param>
+ /// <returns>The result.</returns>
+ public static new HandleRequestResult Fail(Exception failure)
+ {
+ return new HandleRequestResult() { Failure = failure };
+ }
+
+ /// <summary>
+ /// Indicates that there was a failure during authentication.
+ /// </summary>
+ /// <param name="failure">The failure exception.</param>
+ /// <param name="properties">Additional state values for the authentication session.</param>
+ /// <returns>The result.</returns>
+ public static new HandleRequestResult Fail(Exception failure, AuthenticationProperties properties)
+ {
+ return new HandleRequestResult() { Failure = failure, Properties = properties };
+ }
+
+ /// <summary>
+ /// Indicates that there was a failure during authentication.
+ /// </summary>
+ /// <param name="failureMessage">The failure message.</param>
+ /// <returns>The result.</returns>
+ public static new HandleRequestResult Fail(string failureMessage)
+ => Fail(new Exception(failureMessage));
+
+ /// <summary>
+ /// Indicates that there was a failure during authentication.
+ /// </summary>
+ /// <param name="failureMessage">The failure message.</param>
+ /// <param name="properties">Additional state values for the authentication session.</param>
+ /// <returns>The result.</returns>
+ public static new HandleRequestResult Fail(string failureMessage, AuthenticationProperties properties)
+ => Fail(new Exception(failureMessage), properties);
+
+ /// <summary>
+ /// Discontinue all processing for this request and return to the client.
+ /// The caller is responsible for generating the full response.
+ /// </summary>
+ /// <returns>The result.</returns>
+ public static HandleRequestResult Handle()
+ {
+ return new HandleRequestResult() { Handled = true };
+ }
+
+ /// <summary>
+ /// Discontinue processing the request in the current handler.
+ /// </summary>
+ /// <returns>The result.</returns>
+ public static HandleRequestResult SkipHandler()
+ {
+ return new HandleRequestResult() { Skipped = true };
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/ISystemClock.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/ISystemClock.cs
new file mode 100644
index 0000000000..5582669861
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/ISystemClock.cs
@@ -0,0 +1,19 @@
+// 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.Authentication
+{
+ /// <summary>
+ /// Abstracts the system clock to facilitate testing.
+ /// </summary>
+ public interface ISystemClock
+ {
+ /// <summary>
+ /// Retrieves the current system time in UTC.
+ /// </summary>
+ DateTimeOffset UtcNow { get; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Internal/RequestPathBaseCookieBuilder.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Internal/RequestPathBaseCookieBuilder.cs
new file mode 100644
index 0000000000..f42617cb23
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Internal/RequestPathBaseCookieBuilder.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;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.Internal
+{
+ /// <summary>
+ /// A cookie builder that sets <see cref="CookieOptions.Path"/> to the request path base.
+ /// </summary>
+ public class RequestPathBaseCookieBuilder : CookieBuilder
+ {
+ /// <summary>
+ /// Gets an optional value that is appended to the request path base.
+ /// </summary>
+ protected virtual string AdditionalPath { get; }
+
+ public override CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom)
+ {
+ // check if the user has overridden the default value of path. If so, use that instead of our default value.
+ var path = Path;
+ if (path == null)
+ {
+ var originalPathBase = context.Features.Get<IAuthenticationFeature>()?.OriginalPathBase ?? context.Request.PathBase;
+ path = originalPathBase + AdditionalPath;
+ }
+
+ var options = base.Build(context, expiresFrom);
+
+ options.Path = !string.IsNullOrEmpty(path)
+ ? path
+ : "/";
+
+ return options;
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs
new file mode 100644
index 0000000000..8cba6c0d5e
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs
@@ -0,0 +1,125 @@
+// 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.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static Action<ILogger, string, Exception> _authSchemeAuthenticated;
+ private static Action<ILogger, string, Exception> _authSchemeNotAuthenticated;
+ private static Action<ILogger, string, string, Exception> _authSchemeNotAuthenticatedWithFailure;
+ private static Action<ILogger, string, Exception> _authSchemeChallenged;
+ private static Action<ILogger, string, Exception> _authSchemeForbidden;
+ private static Action<ILogger, string, Exception> _remoteAuthenticationError;
+ private static Action<ILogger, Exception> _signInHandled;
+ private static Action<ILogger, Exception> _signInSkipped;
+ private static Action<ILogger, string, Exception> _correlationPropertyNotFound;
+ private static Action<ILogger, string, Exception> _correlationCookieNotFound;
+ private static Action<ILogger, string, string, Exception> _unexpectedCorrelationCookieValue;
+
+ static LoggingExtensions()
+ {
+ _remoteAuthenticationError = LoggerMessage.Define<string>(
+ eventId: 4,
+ logLevel: LogLevel.Information,
+ formatString: "Error from RemoteAuthentication: {ErrorMessage}.");
+ _signInHandled = LoggerMessage.Define(
+ eventId: 5,
+ logLevel: LogLevel.Debug,
+ formatString: "The SigningIn event returned Handled.");
+ _signInSkipped = LoggerMessage.Define(
+ eventId: 6,
+ logLevel: LogLevel.Debug,
+ formatString: "The SigningIn event returned Skipped.");
+ _authSchemeNotAuthenticatedWithFailure = LoggerMessage.Define<string, string>(
+ eventId: 7,
+ logLevel: LogLevel.Information,
+ formatString: "{AuthenticationScheme} was not authenticated. Failure message: {FailureMessage}");
+ _authSchemeAuthenticated = LoggerMessage.Define<string>(
+ eventId: 8,
+ logLevel: LogLevel.Debug,
+ formatString: "AuthenticationScheme: {AuthenticationScheme} was successfully authenticated.");
+ _authSchemeNotAuthenticated = LoggerMessage.Define<string>(
+ eventId: 9,
+ logLevel: LogLevel.Debug,
+ formatString: "AuthenticationScheme: {AuthenticationScheme} was not authenticated.");
+ _authSchemeChallenged = LoggerMessage.Define<string>(
+ eventId: 12,
+ logLevel: LogLevel.Information,
+ formatString: "AuthenticationScheme: {AuthenticationScheme} was challenged.");
+ _authSchemeForbidden = LoggerMessage.Define<string>(
+ eventId: 13,
+ logLevel: LogLevel.Information,
+ formatString: "AuthenticationScheme: {AuthenticationScheme} was forbidden.");
+ _correlationPropertyNotFound = LoggerMessage.Define<string>(
+ eventId: 14,
+ logLevel: LogLevel.Warning,
+ formatString: "{CorrelationProperty} state property not found.");
+ _correlationCookieNotFound = LoggerMessage.Define<string>(
+ eventId: 15,
+ logLevel: LogLevel.Warning,
+ formatString: "'{CorrelationCookieName}' cookie not found.");
+ _unexpectedCorrelationCookieValue = LoggerMessage.Define<string, string>(
+ eventId: 16,
+ logLevel: LogLevel.Warning,
+ formatString: "The correlation cookie value '{CorrelationCookieName}' did not match the expected value '{CorrelationCookieValue}'.");
+ }
+
+ public static void AuthenticationSchemeAuthenticated(this ILogger logger, string authenticationScheme)
+ {
+ _authSchemeAuthenticated(logger, authenticationScheme, null);
+ }
+
+ public static void AuthenticationSchemeNotAuthenticated(this ILogger logger, string authenticationScheme)
+ {
+ _authSchemeNotAuthenticated(logger, authenticationScheme, null);
+ }
+
+ public static void AuthenticationSchemeNotAuthenticatedWithFailure(this ILogger logger, string authenticationScheme, string failureMessage)
+ {
+ _authSchemeNotAuthenticatedWithFailure(logger, authenticationScheme, failureMessage, null);
+ }
+
+ public static void AuthenticationSchemeChallenged(this ILogger logger, string authenticationScheme)
+ {
+ _authSchemeChallenged(logger, authenticationScheme, null);
+ }
+
+ public static void AuthenticationSchemeForbidden(this ILogger logger, string authenticationScheme)
+ {
+ _authSchemeForbidden(logger, authenticationScheme, null);
+ }
+
+ public static void RemoteAuthenticationError(this ILogger logger, string errorMessage)
+ {
+ _remoteAuthenticationError(logger, errorMessage, null);
+ }
+
+ public static void SigninHandled(this ILogger logger)
+ {
+ _signInHandled(logger, null);
+ }
+
+ public static void SigninSkipped(this ILogger logger)
+ {
+ _signInSkipped(logger, null);
+ }
+
+ public static void CorrelationPropertyNotFound(this ILogger logger, string correlationPrefix)
+ {
+ _correlationPropertyNotFound(logger, correlationPrefix, null);
+ }
+
+ public static void CorrelationCookieNotFound(this ILogger logger, string cookieName)
+ {
+ _correlationCookieNotFound(logger, cookieName, null);
+ }
+
+ public static void UnexpectedCorrelationCookieValue(this ILogger logger, string cookieName, string cookieValue)
+ {
+ _unexpectedCorrelationCookieValue(logger, cookieName, cookieValue, null);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj b/src/Security/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj
new file mode 100644
index 0000000000..7e3ce4eb39
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core common types used by the various authentication middleware components.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;authentication;security</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Authentication.Core" Version="$(MicrosoftAspNetCoreAuthenticationCorePackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="$(MicrosoftAspNetCoreDataProtectionPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(MicrosoftAspNetCoreHttpExtensionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.SecurityHelper.Sources" PrivateAssets="All" Version="$(MicrosoftExtensionsSecurityHelperSourcesPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.WebEncoders" Version="$(MicrosoftExtensionsWebEncodersPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeHandler.cs
new file mode 100644
index 0000000000..4dbbb7de2d
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeHandler.cs
@@ -0,0 +1,36 @@
+// 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.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ /// <summary>
+ /// PolicySchemes are used to redirect authentication methods to another scheme.
+ /// </summary>
+ public class PolicySchemeHandler : SignInAuthenticationHandler<PolicySchemeOptions>
+ {
+ public PolicySchemeHandler(IOptionsMonitor<PolicySchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
+ { }
+
+ protected override Task HandleChallengeAsync(AuthenticationProperties properties)
+ => throw new NotImplementedException();
+
+ protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
+ => throw new NotImplementedException();
+
+ protected override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
+ => throw new NotImplementedException();
+
+ protected override Task HandleSignOutAsync(AuthenticationProperties properties)
+ => throw new NotImplementedException();
+
+ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
+ => throw new NotImplementedException();
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeOptions.cs
new file mode 100644
index 0000000000..1921c77ec8
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/PolicySchemeOptions.cs
@@ -0,0 +1,11 @@
+// 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.Authentication
+{
+ /// <summary>
+ /// Contains the options used by the <see cref="PolicySchemeHandler"/>.
+ /// </summary>
+ public class PolicySchemeOptions : AuthenticationSchemeOptions
+ { }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..b1941a7dca
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Properties/Resources.Designer.cs
@@ -0,0 +1,100 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Authentication
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Authentication.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// The default data protection provider may only be used when the IApplicationBuilder.Properties contains an appropriate 'host.AppName' key.
+ /// </summary>
+ internal static string Exception_DefaultDpapiRequiresAppNameKey
+ {
+ get => GetString("Exception_DefaultDpapiRequiresAppNameKey");
+ }
+
+ /// <summary>
+ /// The default data protection provider may only be used when the IApplicationBuilder.Properties contains an appropriate 'host.AppName' key.
+ /// </summary>
+ internal static string FormatException_DefaultDpapiRequiresAppNameKey()
+ => GetString("Exception_DefaultDpapiRequiresAppNameKey");
+
+ /// <summary>
+ /// The state passed to UnhookAuthentication may only be the return value from HookAuthentication.
+ /// </summary>
+ internal static string Exception_UnhookAuthenticationStateType
+ {
+ get => GetString("Exception_UnhookAuthenticationStateType");
+ }
+
+ /// <summary>
+ /// The state passed to UnhookAuthentication may only be the return value from HookAuthentication.
+ /// </summary>
+ internal static string FormatException_UnhookAuthenticationStateType()
+ => GetString("Exception_UnhookAuthenticationStateType");
+
+ /// <summary>
+ /// The AuthenticationTokenProvider's required synchronous events have not been registered.
+ /// </summary>
+ internal static string Exception_AuthenticationTokenDoesNotProvideSyncMethods
+ {
+ get => GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods");
+ }
+
+ /// <summary>
+ /// The AuthenticationTokenProvider's required synchronous events have not been registered.
+ /// </summary>
+ internal static string FormatException_AuthenticationTokenDoesNotProvideSyncMethods()
+ => GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods");
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string Exception_OptionMustBeProvided
+ {
+ get => GetString("Exception_OptionMustBeProvided");
+ }
+
+ /// <summary>
+ /// The '{0}' option must be provided.
+ /// </summary>
+ internal static string FormatException_OptionMustBeProvided(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0);
+
+ /// <summary>
+ /// The SignInScheme for a remote authentication handler cannot be set to itself. If it was not explicitly set, the AuthenticationOptions.DefaultSignInScheme or DefaultScheme is used.
+ /// </summary>
+ internal static string Exception_RemoteSignInSchemeCannotBeSelf
+ {
+ get => GetString("Exception_RemoteSignInSchemeCannotBeSelf");
+ }
+
+ /// <summary>
+ /// The SignInScheme for a remote authentication handler cannot be set to itself. If it was not explicitly set, the AuthenticationOptions.DefaultSignInScheme or DefaultScheme is used.
+ /// </summary>
+ internal static string FormatException_RemoteSignInSchemeCannotBeSelf()
+ => GetString("Exception_RemoteSignInSchemeCannotBeSelf");
+
+ 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/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs
new file mode 100644
index 0000000000..bea4895d62
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs
@@ -0,0 +1,245 @@
+// 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.Cryptography;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public abstract class RemoteAuthenticationHandler<TOptions> : AuthenticationHandler<TOptions>, IAuthenticationRequestHandler
+ where TOptions : RemoteAuthenticationOptions, new()
+ {
+ private const string CorrelationProperty = ".xsrf";
+ private const string CorrelationMarker = "N";
+ private const string AuthSchemeKey = ".AuthScheme";
+
+ private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
+
+ protected string SignInScheme => Options.SignInScheme;
+
+ /// <summary>
+ /// The handler calls methods on the events which give the application control at certain points where processing is occurring.
+ /// If it is not provided a default instance is supplied which does nothing when the methods are called.
+ /// </summary>
+ protected new RemoteAuthenticationEvents Events
+ {
+ get { return (RemoteAuthenticationEvents)base.Events; }
+ set { base.Events = value; }
+ }
+
+ protected RemoteAuthenticationHandler(IOptionsMonitor<TOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
+ : base(options, logger, encoder, clock) { }
+
+ protected override Task<object> CreateEventsAsync()
+ => Task.FromResult<object>(new RemoteAuthenticationEvents());
+
+ public virtual Task<bool> ShouldHandleRequestAsync()
+ => Task.FromResult(Options.CallbackPath == Request.Path);
+
+ public virtual async Task<bool> HandleRequestAsync()
+ {
+ if (!await ShouldHandleRequestAsync())
+ {
+ return false;
+ }
+
+ AuthenticationTicket ticket = null;
+ Exception exception = null;
+ AuthenticationProperties properties = null;
+ try
+ {
+ var authResult = await HandleRemoteAuthenticateAsync();
+ if (authResult == null)
+ {
+ exception = new InvalidOperationException("Invalid return state, unable to redirect.");
+ }
+ else if (authResult.Handled)
+ {
+ return true;
+ }
+ else if (authResult.Skipped || authResult.None)
+ {
+ return false;
+ }
+ else if (!authResult.Succeeded)
+ {
+ exception = authResult.Failure ?? new InvalidOperationException("Invalid return state, unable to redirect.");
+ properties = authResult.Properties;
+ }
+
+ ticket = authResult?.Ticket;
+ }
+ catch (Exception ex)
+ {
+ exception = ex;
+ }
+
+ if (exception != null)
+ {
+ Logger.RemoteAuthenticationError(exception.Message);
+ var errorContext = new RemoteFailureContext(Context, Scheme, Options, exception)
+ {
+ Properties = properties
+ };
+ await Events.RemoteFailure(errorContext);
+
+ if (errorContext.Result != null)
+ {
+ if (errorContext.Result.Handled)
+ {
+ return true;
+ }
+ else if (errorContext.Result.Skipped)
+ {
+ return false;
+ }
+ else if (errorContext.Result.Failure != null)
+ {
+ throw new Exception("An error was returned from the RemoteFailure event.", errorContext.Result.Failure);
+ }
+ }
+
+ if (errorContext.Failure != null)
+ {
+ throw new Exception("An error was encountered while handling the remote login.", errorContext.Failure);
+ }
+ }
+
+ // We have a ticket if we get here
+ var ticketContext = new TicketReceivedContext(Context, Scheme, Options, ticket)
+ {
+ ReturnUri = ticket.Properties.RedirectUri
+ };
+
+ ticket.Properties.RedirectUri = null;
+
+ // Mark which provider produced this identity so we can cross-check later in HandleAuthenticateAsync
+ ticketContext.Properties.Items[AuthSchemeKey] = Scheme.Name;
+
+ await Events.TicketReceived(ticketContext);
+
+ if (ticketContext.Result != null)
+ {
+ if (ticketContext.Result.Handled)
+ {
+ Logger.SigninHandled();
+ return true;
+ }
+ else if (ticketContext.Result.Skipped)
+ {
+ Logger.SigninSkipped();
+ return false;
+ }
+ }
+
+ await Context.SignInAsync(SignInScheme, ticketContext.Principal, ticketContext.Properties);
+
+ // Default redirect path is the base path
+ if (string.IsNullOrEmpty(ticketContext.ReturnUri))
+ {
+ ticketContext.ReturnUri = "/";
+ }
+
+ Response.Redirect(ticketContext.ReturnUri);
+ return true;
+ }
+
+ /// <summary>
+ /// Authenticate the user identity with the identity provider.
+ ///
+ /// The method process the request on the endpoint defined by CallbackPath.
+ /// </summary>
+ protected abstract Task<HandleRequestResult> HandleRemoteAuthenticateAsync();
+
+ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
+ {
+ var result = await Context.AuthenticateAsync(SignInScheme);
+ if (result != null)
+ {
+ if (result.Failure != null)
+ {
+ return result;
+ }
+
+ // The SignInScheme may be shared with multiple providers, make sure this provider issued the identity.
+ string authenticatedScheme;
+ var ticket = result.Ticket;
+ if (ticket != null && ticket.Principal != null && ticket.Properties != null
+ && ticket.Properties.Items.TryGetValue(AuthSchemeKey, out authenticatedScheme)
+ && string.Equals(Scheme.Name, authenticatedScheme, StringComparison.Ordinal))
+ {
+ return AuthenticateResult.Success(new AuthenticationTicket(ticket.Principal,
+ ticket.Properties, Scheme.Name));
+ }
+
+ return AuthenticateResult.Fail("Not authenticated");
+ }
+
+ return AuthenticateResult.Fail("Remote authentication does not directly support AuthenticateAsync");
+ }
+
+ protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
+ => Context.ForbidAsync(SignInScheme);
+
+ protected virtual void GenerateCorrelationId(AuthenticationProperties properties)
+ {
+ if (properties == null)
+ {
+ throw new ArgumentNullException(nameof(properties));
+ }
+
+ var bytes = new byte[32];
+ CryptoRandom.GetBytes(bytes);
+ var correlationId = Base64UrlTextEncoder.Encode(bytes);
+
+ var cookieOptions = Options.CorrelationCookie.Build(Context, Clock.UtcNow);
+
+ properties.Items[CorrelationProperty] = correlationId;
+
+ var cookieName = Options.CorrelationCookie.Name + Scheme.Name + "." + correlationId;
+
+ Response.Cookies.Append(cookieName, CorrelationMarker, cookieOptions);
+ }
+
+ protected virtual bool ValidateCorrelationId(AuthenticationProperties properties)
+ {
+ if (properties == null)
+ {
+ throw new ArgumentNullException(nameof(properties));
+ }
+
+ if (!properties.Items.TryGetValue(CorrelationProperty, out string correlationId))
+ {
+ Logger.CorrelationPropertyNotFound(Options.CorrelationCookie.Name);
+ return false;
+ }
+
+ properties.Items.Remove(CorrelationProperty);
+
+ var cookieName = Options.CorrelationCookie.Name + Scheme.Name + "." + correlationId;
+
+ var correlationCookie = Request.Cookies[cookieName];
+ if (string.IsNullOrEmpty(correlationCookie))
+ {
+ Logger.CorrelationCookieNotFound(cookieName);
+ return false;
+ }
+
+ var cookieOptions = Options.CorrelationCookie.Build(Context, Clock.UtcNow);
+
+ Response.Cookies.Delete(cookieName, cookieOptions);
+
+ if (!string.Equals(correlationCookie, CorrelationMarker, StringComparison.Ordinal))
+ {
+ Logger.UnexpectedCorrelationCookieValue(cookieName, correlationCookie);
+ return false;
+ }
+
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs
new file mode 100644
index 0000000000..1bd3b210e5
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs
@@ -0,0 +1,153 @@
+// 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.Net.Http;
+using Microsoft.AspNetCore.Authentication.Internal;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ /// <summary>
+ /// Contains the options used by the <see cref="RemoteAuthenticationHandler{T}"/>.
+ /// </summary>
+ public class RemoteAuthenticationOptions : AuthenticationSchemeOptions
+ {
+ private const string CorrelationPrefix = ".AspNetCore.Correlation.";
+
+ private CookieBuilder _correlationCookieBuilder;
+
+ /// <summary>
+ /// Initializes a new <see cref="RemoteAuthenticationOptions"/>.
+ /// </summary>
+ public RemoteAuthenticationOptions()
+ {
+ _correlationCookieBuilder = new CorrelationCookieBuilder(this)
+ {
+ Name = CorrelationPrefix,
+ HttpOnly = true,
+ SameSite = SameSiteMode.None,
+ SecurePolicy = CookieSecurePolicy.SameAsRequest,
+ IsEssential = true,
+ };
+ }
+
+ /// <summary>
+ /// Checks that the options are valid for a specific scheme
+ /// </summary>
+ /// <param name="scheme">The scheme being validated.</param>
+ public override void Validate(string scheme)
+ {
+ base.Validate(scheme);
+ if (string.Equals(scheme, SignInScheme, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException(Resources.Exception_RemoteSignInSchemeCannotBeSelf);
+ }
+ }
+
+ /// <summary>
+ /// Check that the options are valid. Should throw an exception if things are not ok.
+ /// </summary>
+ public override void Validate()
+ {
+ base.Validate();
+ if (CallbackPath == null || !CallbackPath.HasValue)
+ {
+ throw new ArgumentException(Resources.FormatException_OptionMustBeProvided(nameof(CallbackPath)), nameof(CallbackPath));
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets timeout value in milliseconds for back channel communications with the remote identity provider.
+ /// </summary>
+ /// <value>
+ /// The back channel timeout.
+ /// </value>
+ public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromSeconds(60);
+
+ /// <summary>
+ /// The HttpMessageHandler used to communicate with remote identity provider.
+ /// This cannot be set at the same time as BackchannelCertificateValidator unless the value
+ /// can be downcast to a WebRequestHandler.
+ /// </summary>
+ public HttpMessageHandler BackchannelHttpHandler { get; set; }
+
+ /// <summary>
+ /// Used to communicate with the remote identity provider.
+ /// </summary>
+ public HttpClient Backchannel { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type used to secure data.
+ /// </summary>
+ public IDataProtectionProvider DataProtectionProvider { get; set; }
+
+ /// <summary>
+ /// The request path within the application's base path where the user-agent will be returned.
+ /// The middleware will process this request when it arrives.
+ /// </summary>
+ public PathString CallbackPath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the authentication scheme corresponding to the middleware
+ /// responsible of persisting user's identity after a successful authentication.
+ /// This value typically corresponds to a cookie middleware registered in the Startup class.
+ /// When omitted, <see cref="AuthenticationOptions.DefaultSignInScheme"/> is used as a fallback value.
+ /// </summary>
+ public string SignInScheme { get; set; }
+
+ /// <summary>
+ /// Gets or sets the time limit for completing the authentication flow (15 minutes by default).
+ /// </summary>
+ public TimeSpan RemoteAuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(15);
+
+ public new RemoteAuthenticationEvents Events
+ {
+ get => (RemoteAuthenticationEvents)base.Events;
+ set => base.Events = value;
+ }
+
+ /// <summary>
+ /// Defines whether access and refresh tokens should be stored in the
+ /// <see cref="Http.Authentication.AuthenticationProperties"/> after a successful authorization.
+ /// This property is set to <c>false</c> by default to reduce
+ /// the size of the final authentication cookie.
+ /// </summary>
+ public bool SaveTokens { get; set; }
+
+ /// <summary>
+ /// Determines the settings used to create the correlation cookie before the
+ /// cookie gets added to the response.
+ /// </summary>
+ public CookieBuilder CorrelationCookie
+ {
+ get => _correlationCookieBuilder;
+ set => _correlationCookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ private class CorrelationCookieBuilder : RequestPathBaseCookieBuilder
+ {
+ private readonly RemoteAuthenticationOptions _options;
+
+ public CorrelationCookieBuilder(RemoteAuthenticationOptions remoteAuthenticationOptions)
+ {
+ _options = remoteAuthenticationOptions;
+ }
+
+ protected override string AdditionalPath => _options.CallbackPath;
+
+ public override CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom)
+ {
+ var cookieOptions = base.Build(context, expiresFrom);
+
+ if (!Expiration.HasValue || !cookieOptions.Expires.HasValue)
+ {
+ cookieOptions.Expires = expiresFrom.Add(_options.RemoteAuthenticationTimeout);
+ }
+
+ return cookieOptions;
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authentication/Resources.resx
new file mode 100644
index 0000000000..9e831dc74f
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/Resources.resx
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Exception_DefaultDpapiRequiresAppNameKey" xml:space="preserve">
+ <value>The default data protection provider may only be used when the IApplicationBuilder.Properties contains an appropriate 'host.AppName' key.</value>
+ </data>
+ <data name="Exception_UnhookAuthenticationStateType" xml:space="preserve">
+ <value>The state passed to UnhookAuthentication may only be the return value from HookAuthentication.</value>
+ </data>
+ <data name="Exception_AuthenticationTokenDoesNotProvideSyncMethods" xml:space="preserve">
+ <value>The AuthenticationTokenProvider's required synchronous events have not been registered.</value>
+ </data>
+ <data name="Exception_OptionMustBeProvided" xml:space="preserve">
+ <value>The '{0}' option must be provided.</value>
+ </data>
+ <data name="Exception_RemoteSignInSchemeCannotBeSelf" xml:space="preserve">
+ <value>The SignInScheme for a remote authentication handler cannot be set to itself. If it was not explicitly set, the AuthenticationOptions.DefaultSignInScheme or DefaultScheme is used.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/SignInAuthenticationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/SignInAuthenticationHandler.cs
new file mode 100644
index 0000000000..dbd612dc10
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/SignInAuthenticationHandler.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 System;
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ /// <summary>
+ /// Adds support for SignInAsync
+ /// </summary>
+ public abstract class SignInAuthenticationHandler<TOptions> : SignOutAuthenticationHandler<TOptions>, IAuthenticationSignInHandler
+ where TOptions : AuthenticationSchemeOptions, new()
+ {
+ public SignInAuthenticationHandler(IOptionsMonitor<TOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
+ { }
+
+ public virtual Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
+ {
+ var target = ResolveTarget(Options.ForwardSignIn);
+ return (target != null)
+ ? Context.SignInAsync(target, user, properties)
+ : HandleSignInAsync(user, properties ?? new AuthenticationProperties());
+ }
+
+ /// <summary>
+ /// Override this method to handle SignIn.
+ /// </summary>
+ /// <param name="user"></param>
+ /// <param name="properties"></param>
+ /// <returns>A Task.</returns>
+ protected abstract Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties);
+
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/SignOutAuthenticationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/SignOutAuthenticationHandler.cs
new file mode 100644
index 0000000000..015cb39e05
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/SignOutAuthenticationHandler.cs
@@ -0,0 +1,36 @@
+// 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.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ /// <summary>
+ /// Adds support for SignOutAsync
+ /// </summary>
+ public abstract class SignOutAuthenticationHandler<TOptions> : AuthenticationHandler<TOptions>, IAuthenticationSignOutHandler
+ where TOptions : AuthenticationSchemeOptions, new()
+ {
+ public SignOutAuthenticationHandler(IOptionsMonitor<TOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
+ { }
+
+ public virtual Task SignOutAsync(AuthenticationProperties properties)
+ {
+ var target = ResolveTarget(Options.ForwardSignOut);
+ return (target != null)
+ ? Context.SignOutAsync(target, properties)
+ : HandleSignOutAsync(properties ?? new AuthenticationProperties());
+ }
+
+ /// <summary>
+ /// Override this method to handle SignOut.
+ /// </summary>
+ /// <param name="properties"></param>
+ /// <returns>A Task.</returns>
+ protected abstract Task HandleSignOutAsync(AuthenticationProperties properties);
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/SystemClock.cs b/src/Security/src/Microsoft.AspNetCore.Authentication/SystemClock.cs
new file mode 100644
index 0000000000..2320982ce3
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/SystemClock.cs
@@ -0,0 +1,27 @@
+// 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.Authentication
+{
+ /// <summary>
+ /// Provides access to the normal system clock with precision in seconds.
+ /// </summary>
+ public class SystemClock : ISystemClock
+ {
+ /// <summary>
+ /// Retrieves the current system time in UTC.
+ /// </summary>
+ public DateTimeOffset UtcNow
+ {
+ get
+ {
+ // the clock measures whole seconds only, to have integral expires_in results, and
+ // because milliseconds do not round-trip serialization formats
+ var utcNowPrecisionSeconds = new DateTime((DateTime.UtcNow.Ticks / TimeSpan.TicksPerSecond) * TimeSpan.TicksPerSecond, DateTimeKind.Utc);
+ return new DateTimeOffset(utcNowPrecisionSeconds);
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json
new file mode 100644
index 0000000000..08eeb5e7b2
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json
@@ -0,0 +1,3330 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Authentication, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Services",
+ "Parameters": [],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddScheme<T0, T1>",
+ "Parameters": [
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<T0>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ },
+ {
+ "ParameterName": "THandler",
+ "ParameterPosition": 1,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationHandler<T0>"
+ ]
+ }
+ ]
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddScheme<T0, T1>",
+ "Parameters": [
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<T0>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ },
+ {
+ "ParameterName": "THandler",
+ "ParameterPosition": 1,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationHandler<T0>"
+ ]
+ }
+ ]
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddRemoteScheme<T0, T1>",
+ "Parameters": [
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<T0>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions"
+ ]
+ },
+ {
+ "ParameterName": "THandler",
+ "ParameterPosition": 1,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler<T0>"
+ ]
+ }
+ ]
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddPolicyScheme",
+ "Parameters": [
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.PolicySchemeOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.AuthenticationHandler<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authentication.IAuthenticationHandler"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Scheme",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationScheme",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Options",
+ "Parameters": [],
+ "ReturnType": "T0",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Context",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.HttpContext",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Request",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.HttpRequest",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Response",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.HttpResponse",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OriginalPath",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OriginalPathBase",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Logger",
+ "Parameters": [],
+ "ReturnType": "Microsoft.Extensions.Logging.ILogger",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_UrlEncoder",
+ "Parameters": [],
+ "ReturnType": "System.Text.Encodings.Web.UrlEncoder",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Clock",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.ISystemClock",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OptionsMonitor",
+ "Parameters": [],
+ "ReturnType": "Microsoft.Extensions.Options.IOptionsMonitor<T0>",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "System.Object",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ClaimsIssuer",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CurrentUri",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "InitializeAsync",
+ "Parameters": [
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "InitializeEventsAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateEventsAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Object>",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "InitializeHandlerAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "BuildRedirectUri",
+ "Parameters": [
+ {
+ "Name": "targetPath",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ResolveTarget",
+ "Parameters": [
+ {
+ "Name": "scheme",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AuthenticateAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleAuthenticateOnceAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleAuthenticateOnceSafeAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleAuthenticateAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>",
+ "Virtual": true,
+ "Abstract": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleForbiddenAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleChallengeAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ChallengeAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ForbidAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<T0>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Protected",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.AuthenticationMiddleware",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Schemes",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Schemes",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Invoke",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "next",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ },
+ {
+ "Name": "schemes",
+ "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Validate",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Validate",
+ "Parameters": [
+ {
+ "Name": "scheme",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ClaimsIssuer",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ClaimsIssuer",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "System.Object",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_EventsType",
+ "Parameters": [],
+ "ReturnType": "System.Type",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_EventsType",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Type"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ForwardDefault",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ForwardDefault",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ForwardAuthenticate",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ForwardAuthenticate",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ForwardChallenge",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ForwardChallenge",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ForwardForbid",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ForwardForbid",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ForwardSignIn",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ForwardSignIn",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ForwardSignOut",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ForwardSignOut",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ForwardDefaultSelector",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Http.HttpContext, System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ForwardDefaultSelector",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Http.HttpContext, System.String>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.IDataSerializer<T0>",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Serialize",
+ "Parameters": [
+ {
+ "Name": "model",
+ "Type": "T0"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Deserialize",
+ "Parameters": [
+ {
+ "Name": "data",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "T0",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TModel",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": []
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<T0>",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Protect",
+ "Parameters": [
+ {
+ "Name": "data",
+ "Type": "T0"
+ }
+ ],
+ "ReturnType": "System.String",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Protect",
+ "Parameters": [
+ {
+ "Name": "data",
+ "Type": "T0"
+ },
+ {
+ "Name": "purpose",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Unprotect",
+ "Parameters": [
+ {
+ "Name": "protectedText",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "T0",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Unprotect",
+ "Parameters": [
+ {
+ "Name": "protectedText",
+ "Type": "System.String"
+ },
+ {
+ "Name": "purpose",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "T0",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TData",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": []
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.PropertiesDataFormat",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.SecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationProperties>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "protector",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.PropertiesSerializer",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authentication.IDataSerializer<Microsoft.AspNetCore.Authentication.AuthenticationProperties>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Default",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.PropertiesSerializer",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Serialize",
+ "Parameters": [
+ {
+ "Name": "model",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer<Microsoft.AspNetCore.Authentication.AuthenticationProperties>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Deserialize",
+ "Parameters": [
+ {
+ "Name": "data",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer<Microsoft.AspNetCore.Authentication.AuthenticationProperties>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Write",
+ "Parameters": [
+ {
+ "Name": "writer",
+ "Type": "System.IO.BinaryWriter"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Read",
+ "Parameters": [
+ {
+ "Name": "reader",
+ "Type": "System.IO.BinaryReader"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.SecureDataFormat<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authentication.ISecureDataFormat<T0>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Protect",
+ "Parameters": [
+ {
+ "Name": "data",
+ "Type": "T0"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<T0>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Protect",
+ "Parameters": [
+ {
+ "Name": "data",
+ "Type": "T0"
+ },
+ {
+ "Name": "purpose",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<T0>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Unprotect",
+ "Parameters": [
+ {
+ "Name": "protectedText",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "T0",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<T0>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Unprotect",
+ "Parameters": [
+ {
+ "Name": "protectedText",
+ "Type": "System.String"
+ },
+ {
+ "Name": "purpose",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "T0",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISecureDataFormat<T0>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "serializer",
+ "Type": "Microsoft.AspNetCore.Authentication.IDataSerializer<T0>"
+ },
+ {
+ "Name": "protector",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TData",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": []
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.Base64UrlTextEncoder",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Encode",
+ "Parameters": [
+ {
+ "Name": "data",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Decode",
+ "Parameters": [
+ {
+ "Name": "text",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.TicketDataFormat",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.SecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationTicket>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "protector",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.TicketSerializer",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authentication.IDataSerializer<Microsoft.AspNetCore.Authentication.AuthenticationTicket>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Default",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.TicketSerializer",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Serialize",
+ "Parameters": [
+ {
+ "Name": "ticket",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer<Microsoft.AspNetCore.Authentication.AuthenticationTicket>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Deserialize",
+ "Parameters": [
+ {
+ "Name": "data",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationTicket",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer<Microsoft.AspNetCore.Authentication.AuthenticationTicket>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Write",
+ "Parameters": [
+ {
+ "Name": "writer",
+ "Type": "System.IO.BinaryWriter"
+ },
+ {
+ "Name": "ticket",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "WriteIdentity",
+ "Parameters": [
+ {
+ "Name": "writer",
+ "Type": "System.IO.BinaryWriter"
+ },
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "WriteClaim",
+ "Parameters": [
+ {
+ "Name": "writer",
+ "Type": "System.IO.BinaryWriter"
+ },
+ {
+ "Name": "claim",
+ "Type": "System.Security.Claims.Claim"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Read",
+ "Parameters": [
+ {
+ "Name": "reader",
+ "Type": "System.IO.BinaryReader"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationTicket",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ReadIdentity",
+ "Parameters": [
+ {
+ "Name": "reader",
+ "Type": "System.IO.BinaryReader"
+ }
+ ],
+ "ReturnType": "System.Security.Claims.ClaimsIdentity",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ReadClaim",
+ "Parameters": [
+ {
+ "Name": "reader",
+ "Type": "System.IO.BinaryReader"
+ },
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ }
+ ],
+ "ReturnType": "System.Security.Claims.Claim",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.BaseContext<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Scheme",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationScheme",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Options",
+ "Parameters": [],
+ "ReturnType": "T0",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_HttpContext",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.HttpContext",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Request",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.HttpRequest",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Response",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.HttpResponse",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "T0"
+ }
+ ],
+ "Visibility": "Protected",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.HandleRequestContext<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.BaseContext<T0>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Result",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Result",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.HandleRequestResult"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleResponse",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SkipHandler",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "T0"
+ }
+ ],
+ "Visibility": "Protected",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.PrincipalContext<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext<T0>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Principal",
+ "Parameters": [],
+ "ReturnType": "System.Security.Claims.ClaimsPrincipal",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Principal",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "T0"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Protected",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.PropertiesContext<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "BaseType": "Microsoft.AspNetCore.Authentication.BaseContext<T0>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Properties",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Properties",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "T0"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Protected",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.RedirectContext<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext<T0>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_RedirectUri",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RedirectUri",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "T0"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ },
+ {
+ "Name": "redirectUri",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "BaseType": "Microsoft.AspNetCore.Authentication.HandleRequestContext<T0>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Principal",
+ "Parameters": [],
+ "ReturnType": "System.Security.Claims.ClaimsPrincipal",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Principal",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Properties",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Properties",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Success",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Fail",
+ "Parameters": [
+ {
+ "Name": "failure",
+ "Type": "System.Exception"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Fail",
+ "Parameters": [
+ {
+ "Name": "failureMessage",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "T0"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "Visibility": "Protected",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_OnRemoteFailure",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.RemoteFailureContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnRemoteFailure",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.RemoteFailureContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnTicketReceived",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authentication.TicketReceivedContext, System.Threading.Tasks.Task>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnTicketReceived",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Authentication.TicketReceivedContext, System.Threading.Tasks.Task>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RemoteFailure",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.RemoteFailureContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "TicketReceived",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authentication.TicketReceivedContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.RemoteFailureContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.HandleRequestContext<Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Failure",
+ "Parameters": [],
+ "ReturnType": "System.Exception",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Failure",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Exception"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Properties",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Properties",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions"
+ },
+ {
+ "Name": "failure",
+ "Type": "System.Exception"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.ResultContext<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "BaseType": "Microsoft.AspNetCore.Authentication.BaseContext<T0>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Principal",
+ "Parameters": [],
+ "ReturnType": "System.Security.Claims.ClaimsPrincipal",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Principal",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Properties",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Properties",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Result",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticateResult",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Success",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "NoResult",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Fail",
+ "Parameters": [
+ {
+ "Name": "failure",
+ "Type": "System.Exception"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Fail",
+ "Parameters": [
+ {
+ "Name": "failureMessage",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "T0"
+ }
+ ],
+ "Visibility": "Protected",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.TicketReceivedContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext<Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ReturnUri",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ReturnUri",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "scheme",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions"
+ },
+ {
+ "Name": "ticket",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.HandleRequestResult",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticateResult",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Handled",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Skipped",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Success",
+ "Parameters": [
+ {
+ "Name": "ticket",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Fail",
+ "Parameters": [
+ {
+ "Name": "failure",
+ "Type": "System.Exception"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Fail",
+ "Parameters": [
+ {
+ "Name": "failure",
+ "Type": "System.Exception"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Fail",
+ "Parameters": [
+ {
+ "Name": "failureMessage",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Fail",
+ "Parameters": [
+ {
+ "Name": "failureMessage",
+ "Type": "System.String"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Handle",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "SkipHandler",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.ISystemClock",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_UtcNow",
+ "Parameters": [],
+ "ReturnType": "System.DateTimeOffset",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.PolicySchemeHandler",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.SignInAuthenticationHandler<Microsoft.AspNetCore.Authentication.PolicySchemeOptions>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "HandleChallengeAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleForbiddenAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleSignInAsync",
+ "Parameters": [
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleSignOutAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleAuthenticateAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.Authentication.PolicySchemeOptions>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.PolicySchemeOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationHandler<T0>",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_SignInScheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateEventsAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Object>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ShouldHandleRequestAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Boolean>",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleRequestAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<System.Boolean>",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleRemoteAuthenticateAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.HandleRequestResult>",
+ "Virtual": true,
+ "Abstract": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleAuthenticateAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleForbiddenAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GenerateCorrelationId",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ValidateCorrelationId",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Boolean",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<T0>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Protected",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Validate",
+ "Parameters": [
+ {
+ "Name": "scheme",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Validate",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_BackchannelTimeout",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_BackchannelTimeout",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_BackchannelHttpHandler",
+ "Parameters": [],
+ "ReturnType": "System.Net.Http.HttpMessageHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_BackchannelHttpHandler",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Net.Http.HttpMessageHandler"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Backchannel",
+ "Parameters": [],
+ "ReturnType": "System.Net.Http.HttpClient",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Backchannel",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Net.Http.HttpClient"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_DataProtectionProvider",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_DataProtectionProvider",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CallbackPath",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.PathString",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CallbackPath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.PathString"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SignInScheme",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SignInScheme",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_RemoteAuthenticationTimeout",
+ "Parameters": [],
+ "ReturnType": "System.TimeSpan",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_RemoteAuthenticationTimeout",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.TimeSpan"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Events",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Events",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SaveTokens",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_SaveTokens",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CorrelationCookie",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CorrelationCookie",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.CookieBuilder"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.SignInAuthenticationHandler<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "BaseType": "Microsoft.AspNetCore.Authentication.SignOutAuthenticationHandler<T0>",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authentication.IAuthenticationSignInHandler"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "SignInAsync",
+ "Parameters": [
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignInHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleSignInAsync",
+ "Parameters": [
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Abstract": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<T0>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.SignOutAuthenticationHandler<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationHandler<T0>",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "SignOutAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleSignOutAsync",
+ "Parameters": [
+ {
+ "Name": "properties",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Abstract": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptionsMonitor<T0>"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ },
+ {
+ "Name": "encoder",
+ "Type": "System.Text.Encodings.Web.UrlEncoder"
+ },
+ {
+ "Name": "clock",
+ "Type": "Microsoft.AspNetCore.Authentication.ISystemClock"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authentication.SystemClock",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authentication.ISystemClock"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_UtcNow",
+ "Parameters": [],
+ "ReturnType": "System.DateTimeOffset",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISystemClock",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.AuthAppBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseAuthentication",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.AuthenticationServiceCollectionExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddAuthentication",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddAuthentication",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "defaultScheme",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddAuthentication",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.AuthenticationOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddScheme<T0, T1>",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureScheme",
+ "Type": "System.Action<Microsoft.AspNetCore.Authentication.AuthenticationSchemeBuilder>"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<T0>"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ },
+ {
+ "ParameterName": "THandler",
+ "ParameterPosition": 1,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationHandler<T0>"
+ ]
+ }
+ ]
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddScheme<T0, T1>",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<T0>"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ },
+ {
+ "ParameterName": "THandler",
+ "ParameterPosition": 1,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationHandler<T0>"
+ ]
+ }
+ ]
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddScheme<T0, T1>",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<T0>"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions"
+ ]
+ },
+ {
+ "ParameterName": "THandler",
+ "ParameterPosition": 1,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.AuthenticationHandler<T0>"
+ ]
+ }
+ ]
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddRemoteScheme<T0, T1>",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "authenticationScheme",
+ "Type": "System.String"
+ },
+ {
+ "Name": "displayName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configureOptions",
+ "Type": "System.Action<T0>"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": [
+ {
+ "ParameterName": "TOptions",
+ "ParameterPosition": 0,
+ "New": true,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions"
+ ]
+ },
+ {
+ "ParameterName": "THandler",
+ "ParameterPosition": 1,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler<T0>"
+ ]
+ }
+ ]
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/IPolicyEvaluator.cs b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/IPolicyEvaluator.cs
new file mode 100644
index 0000000000..dd5e6fc038
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/IPolicyEvaluator.cs
@@ -0,0 +1,40 @@
+// 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.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authorization.Policy
+{
+ /// <summary>
+ /// Base class for authorization handlers that need to be called for a specific requirement type.
+ /// </summary>
+ public interface IPolicyEvaluator
+ {
+ /// <summary>
+ /// Does authentication for <see cref="AuthorizationPolicy.AuthenticationSchemes"/> and sets the resulting
+ /// <see cref="ClaimsPrincipal"/> to <see cref="HttpContext.User"/>. If no schemes are set, this is a no-op.
+ /// </summary>
+ /// <param name="policy">The <see cref="AuthorizationPolicy"/>.</param>
+ /// <param name="context">The <see cref="HttpContext"/>.</param>
+ /// <returns><see cref="AuthenticateResult.Success"/> unless all schemes specified by <see cref="AuthorizationPolicy.AuthenticationSchemes"/> fail to authenticate. </returns>
+ Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context);
+
+ /// <summary>
+ /// Attempts authorization for a policy using <see cref="IAuthorizationService"/>.
+ /// </summary>
+ /// <param name="policy">The <see cref="AuthorizationPolicy"/>.</param>
+ /// <param name="authenticationResult">The result of a call to <see cref="AuthenticateAsync(AuthorizationPolicy, HttpContext)"/>.</param>
+ /// <param name="context">The <see cref="HttpContext"/>.</param>
+ /// <param name="resource">
+ /// An optional resource the policy should be checked with.
+ /// If a resource is not required for policy evaluation you may pass null as the value.
+ /// </param>
+ /// <returns>Returns <see cref="PolicyAuthorizationResult.Success"/> if authorization succeeds.
+ /// Otherwise returns <see cref="PolicyAuthorizationResult.Forbid"/> if <see cref="AuthenticateResult.Succeeded"/>, otherwise
+ /// returns <see cref="PolicyAuthorizationResult.Challenge"/></returns>
+ Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource);
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj
new file mode 100644
index 0000000000..16e4aa2622
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core authorization policy helper classes.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;authorization</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.AspNetCore.Authorization\Microsoft.AspNetCore.Authorization.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Authentication.Abstractions" Version="$(MicrosoftAspNetCoreAuthenticationAbstractionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.SecurityHelper.Sources" PrivateAssets="All" Version="$(MicrosoftExtensionsSecurityHelperSourcesPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyAuthorizationResult.cs b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyAuthorizationResult.cs
new file mode 100644
index 0000000000..d7d481dcd6
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyAuthorizationResult.cs
@@ -0,0 +1,35 @@
+// 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.Authorization.Policy
+{
+ public class PolicyAuthorizationResult
+ {
+ private PolicyAuthorizationResult() { }
+
+ /// <summary>
+ /// If true, means the callee should challenge and try again.
+ /// </summary>
+ public bool Challenged { get; private set; }
+
+ /// <summary>
+ /// Authorization was forbidden.
+ /// </summary>
+ public bool Forbidden { get; private set; }
+
+ /// <summary>
+ /// Authorization was successful.
+ /// </summary>
+ public bool Succeeded { get; private set; }
+
+ public static PolicyAuthorizationResult Challenge()
+ => new PolicyAuthorizationResult { Challenged = true };
+
+ public static PolicyAuthorizationResult Forbid()
+ => new PolicyAuthorizationResult { Forbidden = true };
+
+ public static PolicyAuthorizationResult Success()
+ => new PolicyAuthorizationResult { Succeeded = true };
+
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyEvaluator.cs b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyEvaluator.cs
new file mode 100644
index 0000000000..3100ff4d3e
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyEvaluator.cs
@@ -0,0 +1,96 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Internal;
+
+namespace Microsoft.AspNetCore.Authorization.Policy
+{
+ public class PolicyEvaluator : IPolicyEvaluator
+ {
+ private readonly IAuthorizationService _authorization;
+
+ /// <summary>
+ /// Constructor
+ /// </summary>
+ /// <param name="authorization">The authorization service.</param>
+ public PolicyEvaluator(IAuthorizationService authorization)
+ {
+ _authorization = authorization;
+ }
+
+ /// <summary>
+ /// Does authentication for <see cref="AuthorizationPolicy.AuthenticationSchemes"/> and sets the resulting
+ /// <see cref="ClaimsPrincipal"/> to <see cref="HttpContext.User"/>. If no schemes are set, this is a no-op.
+ /// </summary>
+ /// <param name="policy">The <see cref="AuthorizationPolicy"/>.</param>
+ /// <param name="context">The <see cref="HttpContext"/>.</param>
+ /// <returns><see cref="AuthenticateResult.Success"/> unless all schemes specified by <see cref="AuthorizationPolicy.AuthenticationSchemes"/> failed to authenticate. </returns>
+ public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
+ {
+ if (policy.AuthenticationSchemes != null && policy.AuthenticationSchemes.Count > 0)
+ {
+ ClaimsPrincipal newPrincipal = null;
+ foreach (var scheme in policy.AuthenticationSchemes)
+ {
+ var result = await context.AuthenticateAsync(scheme);
+ if (result != null && result.Succeeded)
+ {
+ newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result.Principal);
+ }
+ }
+
+ if (newPrincipal != null)
+ {
+ context.User = newPrincipal;
+ return AuthenticateResult.Success(new AuthenticationTicket(newPrincipal, string.Join(";", policy.AuthenticationSchemes)));
+ }
+ else
+ {
+ context.User = new ClaimsPrincipal(new ClaimsIdentity());
+ return AuthenticateResult.NoResult();
+ }
+ }
+
+ return (context.User?.Identity?.IsAuthenticated ?? false)
+ ? AuthenticateResult.Success(new AuthenticationTicket(context.User, "context.User"))
+ : AuthenticateResult.NoResult();
+ }
+
+ /// <summary>
+ /// Attempts authorization for a policy using <see cref="IAuthorizationService"/>.
+ /// </summary>
+ /// <param name="policy">The <see cref="AuthorizationPolicy"/>.</param>
+ /// <param name="authenticationResult">The result of a call to <see cref="AuthenticateAsync(AuthorizationPolicy, HttpContext)"/>.</param>
+ /// <param name="context">The <see cref="HttpContext"/>.</param>
+ /// <param name="resource">
+ /// An optional resource the policy should be checked with.
+ /// If a resource is not required for policy evaluation you may pass null as the value.
+ /// </param>
+ /// <returns>Returns <see cref="PolicyAuthorizationResult.Success"/> if authorization succeeds.
+ /// Otherwise returns <see cref="PolicyAuthorizationResult.Forbid"/> if <see cref="AuthenticateResult.Succeeded"/>, otherwise
+ /// returns <see cref="PolicyAuthorizationResult.Challenge"/></returns>
+ public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource)
+ {
+ if (policy == null)
+ {
+ throw new ArgumentNullException(nameof(policy));
+ }
+
+ var result = await _authorization.AuthorizeAsync(context.User, resource, policy);
+ if (result.Succeeded)
+ {
+ return PolicyAuthorizationResult.Success();
+ }
+
+ // If authentication was successful, return forbidden, otherwise challenge
+ return (authenticationResult.Succeeded)
+ ? PolicyAuthorizationResult.Forbid()
+ : PolicyAuthorizationResult.Challenge();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyServiceCollectionExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..9b72a5cab4
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/PolicyServiceCollectionExtensions.cs
@@ -0,0 +1,31 @@
+// 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.Authorization.Policy;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ /// <summary>
+ /// Extension methods for setting up authorization services in an <see cref="IServiceCollection" />.
+ /// </summary>
+ public static class PolicyServiceCollectionExtensions
+ {
+ /// <summary>
+ /// Adds authorization policy services to the specified <see cref="IServiceCollection" />.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
+ /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
+ public static IServiceCollection AddAuthorizationPolicyEvaluator(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.TryAdd(ServiceDescriptor.Transient<IPolicyEvaluator, PolicyEvaluator>());
+ return services;
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json
new file mode 100644
index 0000000000..e8708538d3
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json
@@ -0,0 +1,211 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Authorization.Policy, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AuthenticateAsync",
+ "Parameters": [
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy"
+ },
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AuthorizeAsync",
+ "Parameters": [
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy"
+ },
+ {
+ "Name": "authenticationResult",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticateResult"
+ },
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "resource",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult>",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Challenged",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Forbidden",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Succeeded",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Challenge",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Forbid",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Success",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AuthenticateAsync",
+ "Parameters": [
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy"
+ },
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AuthorizeAsync",
+ "Parameters": [
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy"
+ },
+ {
+ "Name": "authenticationResult",
+ "Type": "Microsoft.AspNetCore.Authentication.AuthenticateResult"
+ },
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "resource",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult>",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "authorization",
+ "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.PolicyServiceCollectionExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddAuthorizationPolicyEvaluator",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AllowAnonymousAttribute.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AllowAnonymousAttribute.cs
new file mode 100644
index 0000000000..cb3f1b1728
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AllowAnonymousAttribute.cs
@@ -0,0 +1,15 @@
+// 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.Authorization
+{
+ /// <summary>
+ /// Specifies that the class or method that this attribute is applied to does not require authorization.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public class AllowAnonymousAttribute : Attribute, IAllowAnonymous
+ {
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationFailure.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationFailure.cs
new file mode 100644
index 0000000000..89956c9aa0
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationFailure.cs
@@ -0,0 +1,46 @@
+// 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.Authorization
+{
+ /// <summary>
+ /// Encapsulates a failure result of <see cref="IAuthorizationService.AuthorizeAsync(ClaimsPrincipal, object, IEnumerable{IAuthorizationRequirement})"/>.
+ /// </summary>
+ public class AuthorizationFailure
+ {
+ private AuthorizationFailure() { }
+
+ /// <summary>
+ /// Failure was due to <see cref="AuthorizationHandlerContext.Fail"/> being called.
+ /// </summary>
+ public bool FailCalled { get; private set; }
+
+ /// <summary>
+ /// Failure was due to these requirements not being met via <see cref="AuthorizationHandlerContext.Succeed(IAuthorizationRequirement)"/>.
+ /// </summary>
+ public IEnumerable<IAuthorizationRequirement> FailedRequirements { get; private set; }
+
+ /// <summary>
+ /// Return a failure due to <see cref="AuthorizationHandlerContext.Fail"/> being called.
+ /// </summary>
+ /// <returns>The failure.</returns>
+ public static AuthorizationFailure ExplicitFail()
+ => new AuthorizationFailure
+ {
+ FailCalled = true,
+ FailedRequirements = new IAuthorizationRequirement[0]
+ };
+
+ /// <summary>
+ /// Return a failure due to some requirements not being met via <see cref="AuthorizationHandlerContext.Succeed(IAuthorizationRequirement)"/>.
+ /// </summary>
+ /// <param name="failed">The requirements that were not met.</param>
+ /// <returns>The failure.</returns>
+ public static AuthorizationFailure Failed(IEnumerable<IAuthorizationRequirement> failed)
+ => new AuthorizationFailure { FailedRequirements = failed };
+
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandler.cs
new file mode 100644
index 0000000000..a4a923c3c7
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandler.cs
@@ -0,0 +1,68 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// Base class for authorization handlers that need to be called for a specific requirement type.
+ /// </summary>
+ /// <typeparam name="TRequirement">The type of the requirement to handle.</typeparam>
+ public abstract class AuthorizationHandler<TRequirement> : IAuthorizationHandler
+ where TRequirement : IAuthorizationRequirement
+ {
+ /// <summary>
+ /// Makes a decision if authorization is allowed.
+ /// </summary>
+ /// <param name="context">The authorization context.</param>
+ public virtual async Task HandleAsync(AuthorizationHandlerContext context)
+ {
+ foreach (var req in context.Requirements.OfType<TRequirement>())
+ {
+ await HandleRequirementAsync(context, req);
+ }
+ }
+
+ /// <summary>
+ /// Makes a decision if authorization is allowed based on a specific requirement.
+ /// </summary>
+ /// <param name="context">The authorization context.</param>
+ /// <param name="requirement">The requirement to evaluate.</param>
+ protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement);
+ }
+
+ /// <summary>
+ /// Base class for authorization handlers that need to be called for specific requirement and
+ /// resource types.
+ /// </summary>
+ /// <typeparam name="TRequirement">The type of the requirement to evaluate.</typeparam>
+ /// <typeparam name="TResource">The type of the resource to evaluate.</typeparam>
+ public abstract class AuthorizationHandler<TRequirement, TResource> : IAuthorizationHandler
+ where TRequirement : IAuthorizationRequirement
+ {
+ /// <summary>
+ /// Makes a decision if authorization is allowed.
+ /// </summary>
+ /// <param name="context">The authorization context.</param>
+ public virtual async Task HandleAsync(AuthorizationHandlerContext context)
+ {
+ if (context.Resource is TResource)
+ {
+ foreach (var req in context.Requirements.OfType<TRequirement>())
+ {
+ await HandleRequirementAsync(context, req, (TResource)context.Resource);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Makes a decision if authorization is allowed based on a specific requirement and resource.
+ /// </summary>
+ /// <param name="context">The authorization context.</param>
+ /// <param name="requirement">The requirement to evaluate.</param>
+ /// <param name="resource">The resource to evaluate.</param>
+ protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, TResource resource);
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs
new file mode 100644
index 0000000000..b6378e4073
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs
@@ -0,0 +1,98 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// Contains authorization information used by <see cref="IAuthorizationHandler"/>.
+ /// </summary>
+ public class AuthorizationHandlerContext
+ {
+ private HashSet<IAuthorizationRequirement> _pendingRequirements;
+ private bool _failCalled;
+ private bool _succeedCalled;
+
+ /// <summary>
+ /// Creates a new instance of <see cref="AuthorizationHandlerContext"/>.
+ /// </summary>
+ /// <param name="requirements">A collection of all the <see cref="IAuthorizationRequirement"/> for the current authorization action.</param>
+ /// <param name="user">A <see cref="ClaimsPrincipal"/> representing the current user.</param>
+ /// <param name="resource">An optional resource to evaluate the <paramref name="requirements"/> against.</param>
+ public AuthorizationHandlerContext(
+ IEnumerable<IAuthorizationRequirement> requirements,
+ ClaimsPrincipal user,
+ object resource)
+ {
+ if (requirements == null)
+ {
+ throw new ArgumentNullException(nameof(requirements));
+ }
+
+ Requirements = requirements;
+ User = user;
+ Resource = resource;
+ _pendingRequirements = new HashSet<IAuthorizationRequirement>(requirements);
+ }
+
+ /// <summary>
+ /// The collection of all the <see cref="IAuthorizationRequirement"/> for the current authorization action.
+ /// </summary>
+ public virtual IEnumerable<IAuthorizationRequirement> Requirements { get; }
+
+ /// <summary>
+ /// The <see cref="ClaimsPrincipal"/> representing the current user.
+ /// </summary>
+ public virtual ClaimsPrincipal User { get; }
+
+ /// <summary>
+ /// The optional resource to evaluate the <see cref="AuthorizationHandlerContext.Requirements"/> against.
+ /// </summary>
+ public virtual object Resource { get; }
+
+ /// <summary>
+ /// Gets the requirements that have not yet been marked as succeeded.
+ /// </summary>
+ public virtual IEnumerable<IAuthorizationRequirement> PendingRequirements { get { return _pendingRequirements; } }
+
+ /// <summary>
+ /// Flag indicating whether the current authorization processing has failed.
+ /// </summary>
+ public virtual bool HasFailed { get { return _failCalled; } }
+
+ /// <summary>
+ /// Flag indicating whether the current authorization processing has succeeded.
+ /// </summary>
+ public virtual bool HasSucceeded
+ {
+ get
+ {
+ return !_failCalled && _succeedCalled && !PendingRequirements.Any();
+ }
+ }
+
+ /// <summary>
+ /// Called to indicate <see cref="AuthorizationHandlerContext.HasSucceeded"/> will
+ /// never return true, even if all requirements are met.
+ /// </summary>
+ public virtual void Fail()
+ {
+ _failCalled = true;
+ }
+
+ /// <summary>
+ /// Called to mark the specified <paramref name="requirement"/> as being
+ /// successfully evaluated.
+ /// </summary>
+ /// <param name="requirement">The requirement whose evaluation has succeeded.</param>
+ public virtual void Succeed(IAuthorizationRequirement requirement)
+ {
+ _succeedCalled = true;
+ _pendingRequirements.Remove(requirement);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs
new file mode 100644
index 0000000000..6899913afb
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs
@@ -0,0 +1,87 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// Provides programmatic configuration used by <see cref="IAuthorizationService"/> and <see cref="IAuthorizationPolicyProvider"/>.
+ /// </summary>
+ public class AuthorizationOptions
+ {
+ private IDictionary<string, AuthorizationPolicy> PolicyMap { get; } = new Dictionary<string, AuthorizationPolicy>(StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>
+ /// Determines whether authentication handlers should be invoked after a failure.
+ /// Defaults to true.
+ /// </summary>
+ public bool InvokeHandlersAfterFailure { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the default authorization policy.
+ /// </summary>
+ /// <remarks>
+ /// The default policy is to require any authenticated user.
+ /// </remarks>
+ public AuthorizationPolicy DefaultPolicy { get; set; } = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
+
+ /// <summary>
+ /// Add an authorization policy with the provided name.
+ /// </summary>
+ /// <param name="name">The name of the policy.</param>
+ /// <param name="policy">The authorization policy.</param>
+ public void AddPolicy(string name, AuthorizationPolicy policy)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (policy == null)
+ {
+ throw new ArgumentNullException(nameof(policy));
+ }
+
+ PolicyMap[name] = policy;
+ }
+
+ /// <summary>
+ /// Add a policy that is built from a delegate with the provided name.
+ /// </summary>
+ /// <param name="name">The name of the policy.</param>
+ /// <param name="configurePolicy">The delegate that will be used to build the policy.</param>
+ public void AddPolicy(string name, Action<AuthorizationPolicyBuilder> configurePolicy)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (configurePolicy == null)
+ {
+ throw new ArgumentNullException(nameof(configurePolicy));
+ }
+
+ var policyBuilder = new AuthorizationPolicyBuilder();
+ configurePolicy(policyBuilder);
+ PolicyMap[name] = policyBuilder.Build();
+ }
+
+ /// <summary>
+ /// Returns the policy for the specified name, or null if a policy with the name does not exist.
+ /// </summary>
+ /// <param name="name">The name of the policy to return.</param>
+ /// <returns>The policy for the specified name, or null if a policy with the name does not exist.</returns>
+ public AuthorizationPolicy GetPolicy(string name)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ return PolicyMap.ContainsKey(name) ? PolicyMap[name] : null;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicy.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicy.cs
new file mode 100644
index 0000000000..36e0ca7c38
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicy.cs
@@ -0,0 +1,165 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// Represents a collection of authorization requirements and the scheme or
+ /// schemes they are evaluated against, all of which must succeed
+ /// for authorization to succeed.
+ /// </summary>
+ public class AuthorizationPolicy
+ {
+ /// <summary>
+ /// Creates a new instance of <see cref="AuthorizationPolicy"/>.
+ /// </summary>
+ /// <param name="requirements">
+ /// The list of <see cref="IAuthorizationRequirement"/>s which must succeed for
+ /// this policy to be successful.
+ /// </param>
+ /// <param name="authenticationSchemes">
+ /// The authentication schemes the <paramref name="requirements"/> are evaluated against.
+ /// </param>
+ public AuthorizationPolicy(IEnumerable<IAuthorizationRequirement> requirements, IEnumerable<string> authenticationSchemes)
+ {
+ if (requirements == null)
+ {
+ throw new ArgumentNullException(nameof(requirements));
+ }
+
+ if (authenticationSchemes == null)
+ {
+ throw new ArgumentNullException(nameof(authenticationSchemes));
+ }
+
+ if (requirements.Count() == 0)
+ {
+ throw new InvalidOperationException(Resources.Exception_AuthorizationPolicyEmpty);
+ }
+ Requirements = new List<IAuthorizationRequirement>(requirements).AsReadOnly();
+ AuthenticationSchemes = new List<string>(authenticationSchemes).AsReadOnly();
+ }
+
+ /// <summary>
+ /// Gets a readonly list of <see cref="IAuthorizationRequirement"/>s which must succeed for
+ /// this policy to be successful.
+ /// </summary>
+ public IReadOnlyList<IAuthorizationRequirement> Requirements { get; }
+
+ /// <summary>
+ /// Gets a readonly list of the authentication schemes the <see cref="AuthorizationPolicy.Requirements"/>
+ /// are evaluated against.
+ /// </summary>
+ public IReadOnlyList<string> AuthenticationSchemes { get; }
+
+ /// <summary>
+ /// Combines the specified <see cref="AuthorizationPolicy"/> into a single policy.
+ /// </summary>
+ /// <param name="policies">The authorization policies to combine.</param>
+ /// <returns>
+ /// A new <see cref="AuthorizationPolicy"/> which represents the combination of the
+ /// specified <paramref name="policies"/>.
+ /// </returns>
+ public static AuthorizationPolicy Combine(params AuthorizationPolicy[] policies)
+ {
+ if (policies == null)
+ {
+ throw new ArgumentNullException(nameof(policies));
+ }
+
+ return Combine((IEnumerable<AuthorizationPolicy>)policies);
+ }
+
+ /// <summary>
+ /// Combines the specified <see cref="AuthorizationPolicy"/> into a single policy.
+ /// </summary>
+ /// <param name="policies">The authorization policies to combine.</param>
+ /// <returns>
+ /// A new <see cref="AuthorizationPolicy"/> which represents the combination of the
+ /// specified <paramref name="policies"/>.
+ /// </returns>
+ public static AuthorizationPolicy Combine(IEnumerable<AuthorizationPolicy> policies)
+ {
+ if (policies == null)
+ {
+ throw new ArgumentNullException(nameof(policies));
+ }
+
+ var builder = new AuthorizationPolicyBuilder();
+ foreach (var policy in policies)
+ {
+ builder.Combine(policy);
+ }
+ return builder.Build();
+ }
+
+ /// <summary>
+ /// Combines the <see cref="AuthorizationPolicy"/> provided by the specified
+ /// <paramref name="policyProvider"/>.
+ /// </summary>
+ /// <param name="policyProvider">A <see cref="IAuthorizationPolicyProvider"/> which provides the policies to combine.</param>
+ /// <param name="authorizeData">A collection of authorization data used to apply authorization to a resource.</param>
+ /// <returns>
+ /// A new <see cref="AuthorizationPolicy"/> which represents the combination of the
+ /// authorization policies provided by the specified <paramref name="policyProvider"/>.
+ /// </returns>
+ public static async Task<AuthorizationPolicy> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData)
+ {
+ if (policyProvider == null)
+ {
+ throw new ArgumentNullException(nameof(policyProvider));
+ }
+
+ if (authorizeData == null)
+ {
+ throw new ArgumentNullException(nameof(authorizeData));
+ }
+
+ var policyBuilder = new AuthorizationPolicyBuilder();
+ var any = false;
+ foreach (var authorizeDatum in authorizeData)
+ {
+ any = true;
+ var useDefaultPolicy = true;
+ if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy))
+ {
+ var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy);
+ if (policy == null)
+ {
+ throw new InvalidOperationException(Resources.FormatException_AuthorizationPolicyNotFound(authorizeDatum.Policy));
+ }
+ policyBuilder.Combine(policy);
+ useDefaultPolicy = false;
+ }
+ var rolesSplit = authorizeDatum.Roles?.Split(',');
+ if (rolesSplit != null && rolesSplit.Any())
+ {
+ var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim());
+ policyBuilder.RequireRole(trimmedRolesSplit);
+ useDefaultPolicy = false;
+ }
+ var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(',');
+ if (authTypesSplit != null && authTypesSplit.Any())
+ {
+ foreach (var authType in authTypesSplit)
+ {
+ if (!string.IsNullOrWhiteSpace(authType))
+ {
+ policyBuilder.AuthenticationSchemes.Add(authType.Trim());
+ }
+ }
+ }
+ if (useDefaultPolicy)
+ {
+ policyBuilder.Combine(await policyProvider.GetDefaultPolicyAsync());
+ }
+ }
+ return any ? policyBuilder.Build() : null;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.cs
new file mode 100644
index 0000000000..37335df8f2
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.cs
@@ -0,0 +1,250 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization.Infrastructure;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// Used for building policies during application startup.
+ /// </summary>
+ public class AuthorizationPolicyBuilder
+ {
+ /// <summary>
+ /// Creates a new instance of <see cref="AuthorizationPolicyBuilder"/>
+ /// </summary>
+ /// <param name="authenticationSchemes">An array of authentication schemes the policy should be evaluated against.</param>
+ public AuthorizationPolicyBuilder(params string[] authenticationSchemes)
+ {
+ AddAuthenticationSchemes(authenticationSchemes);
+ }
+
+ /// <summary>
+ /// Creates a new instance of <see cref="AuthorizationPolicyBuilder"/>.
+ /// </summary>
+ /// <param name="policy">The <see cref="AuthorizationPolicy"/> to build.</param>
+ public AuthorizationPolicyBuilder(AuthorizationPolicy policy)
+ {
+ Combine(policy);
+ }
+
+ /// <summary>
+ /// Gets or sets a list of <see cref="IAuthorizationRequirement"/>s which must succeed for
+ /// this policy to be successful.
+ /// </summary>
+ public IList<IAuthorizationRequirement> Requirements { get; set; } = new List<IAuthorizationRequirement>();
+
+ /// <summary>
+ /// Gets or sets a list authentication schemes the <see cref="AuthorizationPolicyBuilder.Requirements"/>
+ /// are evaluated against.
+ /// </summary>
+ public IList<string> AuthenticationSchemes { get; set; } = new List<string>();
+
+ /// <summary>
+ /// Adds the specified authentication <paramref name="schemes"/> to the
+ /// <see cref="AuthorizationPolicyBuilder.AuthenticationSchemes"/> for this instance.
+ /// </summary>
+ /// <param name="schemes">The schemes to add.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public AuthorizationPolicyBuilder AddAuthenticationSchemes(params string[] schemes)
+ {
+ foreach (var authType in schemes)
+ {
+ AuthenticationSchemes.Add(authType);
+ }
+ return this;
+ }
+
+ /// <summary>
+ /// Adds the specified <paramref name="requirements"/> to the
+ /// <see cref="AuthorizationPolicyBuilder.Requirements"/> for this instance.
+ /// </summary>
+ /// <param name="requirements">The authorization requirements to add.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public AuthorizationPolicyBuilder AddRequirements(params IAuthorizationRequirement[] requirements)
+ {
+ foreach (var req in requirements)
+ {
+ Requirements.Add(req);
+ }
+ return this;
+ }
+
+ /// <summary>
+ /// Combines the specified <paramref name="policy"/> into the current instance.
+ /// </summary>
+ /// <param name="policy">The <see cref="AuthorizationPolicy"/> to combine.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public AuthorizationPolicyBuilder Combine(AuthorizationPolicy policy)
+ {
+ if (policy == null)
+ {
+ throw new ArgumentNullException(nameof(policy));
+ }
+
+ AddAuthenticationSchemes(policy.AuthenticationSchemes.ToArray());
+ AddRequirements(policy.Requirements.ToArray());
+ return this;
+ }
+
+ /// <summary>
+ /// Adds a <see cref="ClaimsAuthorizationRequirement"/>
+ /// to the current instance.
+ /// </summary>
+ /// <param name="claimType">The claim type required.</param>
+ /// <param name="requiredValues">Values the claim must process one or more of for evaluation to succeed.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public AuthorizationPolicyBuilder RequireClaim(string claimType, params string[] requiredValues)
+ {
+ if (claimType == null)
+ {
+ throw new ArgumentNullException(nameof(claimType));
+ }
+
+ return RequireClaim(claimType, (IEnumerable<string>)requiredValues);
+ }
+
+ /// <summary>
+ /// Adds a <see cref="ClaimsAuthorizationRequirement"/>
+ /// to the current instance.
+ /// </summary>
+ /// <param name="claimType">The claim type required.</param>
+ /// <param name="requiredValues">Values the claim must process one or more of for evaluation to succeed.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public AuthorizationPolicyBuilder RequireClaim(string claimType, IEnumerable<string> requiredValues)
+ {
+ if (claimType == null)
+ {
+ throw new ArgumentNullException(nameof(claimType));
+ }
+
+ Requirements.Add(new ClaimsAuthorizationRequirement(claimType, requiredValues));
+ return this;
+ }
+
+ /// <summary>
+ /// Adds a <see cref="ClaimsAuthorizationRequirement"/>
+ /// to the current instance.
+ /// </summary>
+ /// <param name="claimType">The claim type required, which no restrictions on claim value.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public AuthorizationPolicyBuilder RequireClaim(string claimType)
+ {
+ if (claimType == null)
+ {
+ throw new ArgumentNullException(nameof(claimType));
+ }
+
+ Requirements.Add(new ClaimsAuthorizationRequirement(claimType, allowedValues: null));
+ return this;
+ }
+
+ /// <summary>
+ /// Adds a <see cref="RolesAuthorizationRequirement"/>
+ /// to the current instance.
+ /// </summary>
+ /// <param name="roles">The roles required.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public AuthorizationPolicyBuilder RequireRole(params string[] roles)
+ {
+ if (roles == null)
+ {
+ throw new ArgumentNullException(nameof(roles));
+ }
+
+ return RequireRole((IEnumerable<string>)roles);
+ }
+
+ /// <summary>
+ /// Adds a <see cref="RolesAuthorizationRequirement"/>
+ /// to the current instance.
+ /// </summary>
+ /// <param name="roles">The roles required.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public AuthorizationPolicyBuilder RequireRole(IEnumerable<string> roles)
+ {
+ if (roles == null)
+ {
+ throw new ArgumentNullException(nameof(roles));
+ }
+
+ Requirements.Add(new RolesAuthorizationRequirement(roles));
+ return this;
+ }
+
+ /// <summary>
+ /// Adds a <see cref="NameAuthorizationRequirement"/>
+ /// to the current instance.
+ /// </summary>
+ /// <param name="userName">The user name the current user must possess.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public AuthorizationPolicyBuilder RequireUserName(string userName)
+ {
+ if (userName == null)
+ {
+ throw new ArgumentNullException(nameof(userName));
+ }
+
+ Requirements.Add(new NameAuthorizationRequirement(userName));
+ return this;
+ }
+
+ /// <summary>
+ /// Adds a <see cref="DenyAnonymousAuthorizationRequirement"/> to the current instance.
+ /// </summary>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public AuthorizationPolicyBuilder RequireAuthenticatedUser()
+ {
+ Requirements.Add(new DenyAnonymousAuthorizationRequirement());
+ return this;
+ }
+
+ /// <summary>
+ /// Adds an <see cref="AssertionRequirement"/> to the current instance.
+ /// </summary>
+ /// <param name="handler">The handler to evaluate during authorization.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public AuthorizationPolicyBuilder RequireAssertion(Func<AuthorizationHandlerContext, bool> handler)
+ {
+ if (handler == null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ Requirements.Add(new AssertionRequirement(handler));
+ return this;
+ }
+
+ /// <summary>
+ /// Adds an <see cref="AssertionRequirement"/> to the current instance.
+ /// </summary>
+ /// <param name="handler">The handler to evaluate during authorization.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public AuthorizationPolicyBuilder RequireAssertion(Func<AuthorizationHandlerContext, Task<bool>> handler)
+ {
+ if (handler == null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ Requirements.Add(new AssertionRequirement(handler));
+ return this;
+ }
+
+ /// <summary>
+ /// Builds a new <see cref="AuthorizationPolicy"/> from the requirements
+ /// in this instance.
+ /// </summary>
+ /// <returns>
+ /// A new <see cref="AuthorizationPolicy"/> built from the requirements in this instance.
+ /// </returns>
+ public AuthorizationPolicy Build()
+ {
+ return new AuthorizationPolicy(Requirements, AuthenticationSchemes.Distinct());
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationResult.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationResult.cs
new file mode 100644
index 0000000000..46dca35fb5
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationResult.cs
@@ -0,0 +1,37 @@
+// 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.Authorization
+{
+ /// <summary>
+ /// Encapsulates the result of <see cref="IAuthorizationService.AuthorizeAsync(ClaimsPrincipal, object, IEnumerable{IAuthorizationRequirement})"/>.
+ /// </summary>
+ public class AuthorizationResult
+ {
+ private AuthorizationResult() { }
+
+ /// <summary>
+ /// True if authorization was successful.
+ /// </summary>
+ public bool Succeeded { get; private set; }
+
+ /// <summary>
+ /// Contains information about why authorization failed.
+ /// </summary>
+ public AuthorizationFailure Failure { get; private set; }
+
+ /// <summary>
+ /// Returns a successful result.
+ /// </summary>
+ /// <returns>A successful result.</returns>
+ public static AuthorizationResult Success() => new AuthorizationResult { Succeeded = true };
+
+ public static AuthorizationResult Failed(AuthorizationFailure failure) => new AuthorizationResult { Failure = failure };
+
+ public static AuthorizationResult Failed() => new AuthorizationResult { Failure = AuthorizationFailure.ExplicitFail() };
+
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..c089fba285
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs
@@ -0,0 +1,59 @@
+// 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.Authorization;
+using Microsoft.AspNetCore.Authorization.Infrastructure;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ /// <summary>
+ /// Extension methods for setting up authorization services in an <see cref="IServiceCollection" />.
+ /// </summary>
+ public static class AuthorizationServiceCollectionExtensions
+ {
+ /// <summary>
+ /// Adds authorization services to the specified <see cref="IServiceCollection" />.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
+ /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
+ public static IServiceCollection AddAuthorization(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.TryAdd(ServiceDescriptor.Transient<IAuthorizationService, DefaultAuthorizationService>());
+ services.TryAdd(ServiceDescriptor.Transient<IAuthorizationPolicyProvider, DefaultAuthorizationPolicyProvider>());
+ services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerProvider, DefaultAuthorizationHandlerProvider>());
+ services.TryAdd(ServiceDescriptor.Transient<IAuthorizationEvaluator, DefaultAuthorizationEvaluator>());
+ services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerContextFactory, DefaultAuthorizationHandlerContextFactory>());
+ services.TryAddEnumerable(ServiceDescriptor.Transient<IAuthorizationHandler, PassThroughAuthorizationHandler>());
+ return services;
+ }
+
+ /// <summary>
+ /// Adds authorization services to the specified <see cref="IServiceCollection" />.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
+ /// <param name="configure">An action delegate to configure the provided <see cref="AuthorizationOptions"/>.</param>
+ /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
+ public static IServiceCollection AddAuthorization(this IServiceCollection services, Action<AuthorizationOptions> configure)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (configure == null)
+ {
+ throw new ArgumentNullException(nameof(configure));
+ }
+
+ services.Configure(configure);
+ return services.AddAuthorization();
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceExtensions.cs
new file mode 100644
index 0000000000..866b5dbc51
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceExtensions.cs
@@ -0,0 +1,118 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// Extension methods for <see cref="IAuthorizationService"/>.
+ /// </summary>
+ public static class AuthorizationServiceExtensions
+ {
+ /// <summary>
+ /// Checks if a user meets a specific requirement for the specified resource
+ /// </summary>
+ /// <param name="service">The <see cref="IAuthorizationService"/> providing authorization.</param>
+ /// <param name="user">The user to evaluate the policy against.</param>
+ /// <param name="resource">The resource to evaluate the policy against.</param>
+ /// <param name="requirement">The requirement to evaluate the policy against.</param>
+ /// <returns>
+ /// A flag indicating whether requirement evaluation has succeeded or failed.
+ /// This value is <value>true</value> when the user fulfills the policy, otherwise <value>false</value>.
+ /// </returns>
+ public static Task<AuthorizationResult> AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, object resource, IAuthorizationRequirement requirement)
+ {
+ if (service == null)
+ {
+ throw new ArgumentNullException(nameof(service));
+ }
+
+ if (requirement == null)
+ {
+ throw new ArgumentNullException(nameof(requirement));
+ }
+
+ return service.AuthorizeAsync(user, resource, new IAuthorizationRequirement[] { requirement });
+ }
+
+ /// <summary>
+ /// Checks if a user meets a specific authorization policy against the specified resource.
+ /// </summary>
+ /// <param name="service">The <see cref="IAuthorizationService"/> providing authorization.</param>
+ /// <param name="user">The user to evaluate the policy against.</param>
+ /// <param name="resource">The resource to evaluate the policy against.</param>
+ /// <param name="policy">The policy to evaluate.</param>
+ /// <returns>
+ /// A flag indicating whether policy evaluation has succeeded or failed.
+ /// This value is <value>true</value> when the user fulfills the policy, otherwise <value>false</value>.
+ /// </returns>
+ public static Task<AuthorizationResult> AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, object resource, AuthorizationPolicy policy)
+ {
+ if (service == null)
+ {
+ throw new ArgumentNullException(nameof(service));
+ }
+
+ if (policy == null)
+ {
+ throw new ArgumentNullException(nameof(policy));
+ }
+
+ return service.AuthorizeAsync(user, resource, policy.Requirements);
+ }
+
+ /// <summary>
+ /// Checks if a user meets a specific authorization policy against the specified resource.
+ /// </summary>
+ /// <param name="service">The <see cref="IAuthorizationService"/> providing authorization.</param>
+ /// <param name="user">The user to evaluate the policy against.</param>
+ /// <param name="policy">The policy to evaluate.</param>
+ /// <returns>
+ /// A flag indicating whether policy evaluation has succeeded or failed.
+ /// This value is <value>true</value> when the user fulfills the policy, otherwise <value>false</value>.
+ /// </returns>
+ public static Task<AuthorizationResult> AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, AuthorizationPolicy policy)
+ {
+ if (service == null)
+ {
+ throw new ArgumentNullException(nameof(service));
+ }
+
+ if (policy == null)
+ {
+ throw new ArgumentNullException(nameof(policy));
+ }
+
+ return service.AuthorizeAsync(user, resource: null, policy: policy);
+ }
+
+ /// <summary>
+ /// Checks if a user meets a specific authorization policy against the specified resource.
+ /// </summary>
+ /// <param name="service">The <see cref="IAuthorizationService"/> providing authorization.</param>
+ /// <param name="user">The user to evaluate the policy against.</param>
+ /// <param name="policyName">The name of the policy to evaluate.</param>
+ /// <returns>
+ /// A flag indicating whether policy evaluation has succeeded or failed.
+ /// This value is <value>true</value> when the user fulfills the policy, otherwise <value>false</value>.
+ /// </returns>
+ public static Task<AuthorizationResult> AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, string policyName)
+ {
+ if (service == null)
+ {
+ throw new ArgumentNullException(nameof(service));
+ }
+
+ if (policyName == null)
+ {
+ throw new ArgumentNullException(nameof(policyName));
+ }
+
+ return service.AuthorizeAsync(user, resource: null, policyName: policyName);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs
new file mode 100644
index 0000000000..63bfa30d45
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.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.
+
+using System;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// Specifies that the class or method that this attribute is applied to requires the specified authorization.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
+ public class AuthorizeAttribute : Attribute, IAuthorizeData
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthorizeAttribute"/> class.
+ /// </summary>
+ public AuthorizeAttribute() { }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthorizeAttribute"/> class with the specified policy.
+ /// </summary>
+ /// <param name="policy">The name of the policy to require for authorization.</param>
+ public AuthorizeAttribute(string policy)
+ {
+ Policy = policy;
+ }
+
+ /// <summary>
+ /// Gets or sets the policy name that determines access to the resource.
+ /// </summary>
+ public string Policy { get; set; }
+
+ /// <summary>
+ /// Gets or sets a comma delimited list of roles that are allowed to access the resource.
+ /// </summary>
+ public string Roles { get; set; }
+
+ /// <summary>
+ /// Gets or sets a comma delimited list of schemes from which user information is constructed.
+ /// </summary>
+ public string AuthenticationSchemes { get; set; }
+
+ /// <summary>
+ /// Gets or sets a comma delimited list of schemes from which user information is constructed.
+ /// </summary>
+ [Obsolete("Use AuthenticationSchemes instead.", error: false)]
+ public string ActiveAuthenticationSchemes
+ {
+ get => AuthenticationSchemes;
+ set => AuthenticationSchemes = value;
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationEvaluator.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationEvaluator.cs
new file mode 100644
index 0000000000..4bbc283be0
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationEvaluator.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.
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// Determines whether an authorization request was successful or not.
+ /// </summary>
+ public class DefaultAuthorizationEvaluator : IAuthorizationEvaluator
+ {
+ /// <summary>
+ /// Determines whether the authorization result was successful or not.
+ /// </summary>
+ /// <param name="context">The authorization information.</param>
+ /// <returns>The <see cref="AuthorizationResult"/>.</returns>
+ public AuthorizationResult Evaluate(AuthorizationHandlerContext context)
+ => context.HasSucceeded
+ ? AuthorizationResult.Success()
+ : AuthorizationResult.Failed(context.HasFailed
+ ? AuthorizationFailure.ExplicitFail()
+ : AuthorizationFailure.Failed(context.PendingRequirements));
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerContextFactory.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerContextFactory.cs
new file mode 100644
index 0000000000..2dae5e5e73
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerContextFactory.cs
@@ -0,0 +1,29 @@
+// 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.Authorization
+{
+ /// <summary>
+ /// A type used to provide a <see cref="AuthorizationHandlerContext"/> used for authorization.
+ /// </summary>
+ public class DefaultAuthorizationHandlerContextFactory : IAuthorizationHandlerContextFactory
+ {
+ /// <summary>
+ /// Creates a <see cref="AuthorizationHandlerContext"/> used for authorization.
+ /// </summary>
+ /// <param name="requirements">The requirements to evaluate.</param>
+ /// <param name="user">The user to evaluate the requirements against.</param>
+ /// <param name="resource">
+ /// An optional resource the policy should be checked with.
+ /// If a resource is not required for policy evaluation you may pass null as the value.
+ /// </param>
+ /// <returns>The <see cref="AuthorizationHandlerContext"/>.</returns>
+ public virtual AuthorizationHandlerContext CreateContext(IEnumerable<IAuthorizationRequirement> requirements, ClaimsPrincipal user, object resource)
+ {
+ return new AuthorizationHandlerContext(requirements, user, resource);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerProvider.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerProvider.cs
new file mode 100644
index 0000000000..d297d4cdc6
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerProvider.cs
@@ -0,0 +1,35 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// The default implementation of a handler provider,
+ /// which provides the <see cref="IAuthorizationHandler"/>s for an authorization request.
+ /// </summary>
+ public class DefaultAuthorizationHandlerProvider : IAuthorizationHandlerProvider
+ {
+ private readonly IEnumerable<IAuthorizationHandler> _handlers;
+
+ /// <summary>
+ /// Creates a new instance of <see cref="DefaultAuthorizationHandlerProvider"/>.
+ /// </summary>
+ /// <param name="handlers">The <see cref="IAuthorizationHandler"/>s.</param>
+ public DefaultAuthorizationHandlerProvider(IEnumerable<IAuthorizationHandler> handlers)
+ {
+ if (handlers == null)
+ {
+ throw new ArgumentNullException(nameof(handlers));
+ }
+
+ _handlers = handlers;
+ }
+
+ public Task<IEnumerable<IAuthorizationHandler>> GetHandlersAsync(AuthorizationHandlerContext context)
+ => Task.FromResult(_handlers);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs
new file mode 100644
index 0000000000..0e4329dcc0
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs
@@ -0,0 +1,54 @@
+// 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.Threading.Tasks;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// The default implementation of a policy provider,
+ /// which provides a <see cref="AuthorizationPolicy"/> for a particular name.
+ /// </summary>
+ public class DefaultAuthorizationPolicyProvider : IAuthorizationPolicyProvider
+ {
+ private readonly AuthorizationOptions _options;
+
+ /// <summary>
+ /// Creates a new instance of <see cref="DefaultAuthorizationPolicyProvider"/>.
+ /// </summary>
+ /// <param name="options">The options used to configure this instance.</param>
+ public DefaultAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ _options = options.Value;
+ }
+
+ /// <summary>
+ /// Gets the default authorization policy.
+ /// </summary>
+ /// <returns>The default authorization policy.</returns>
+ public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
+ {
+ return Task.FromResult(_options.DefaultPolicy);
+ }
+
+ /// <summary>
+ /// Gets a <see cref="AuthorizationPolicy"/> from the given <paramref name="policyName"/>
+ /// </summary>
+ /// <param name="policyName">The policy name to retrieve.</param>
+ /// <returns>The named <see cref="AuthorizationPolicy"/>.</returns>
+ public virtual Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
+ {
+ // MVC caches policies specifically for this class, so this method MUST return the same policy per
+ // policyName for every request or it could allow undesired access. It also must return synchronously.
+ // A change to either of these behaviors would require shipping a patch of MVC as well.
+ return Task.FromResult(_options.GetPolicy(policyName));
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs
new file mode 100644
index 0000000000..bc5d571c47
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs
@@ -0,0 +1,135 @@
+// 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 System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// The default implementation of an <see cref="IAuthorizationService"/>.
+ /// </summary>
+ public class DefaultAuthorizationService : IAuthorizationService
+ {
+ private readonly AuthorizationOptions _options;
+ private readonly IAuthorizationHandlerContextFactory _contextFactory;
+ private readonly IAuthorizationHandlerProvider _handlers;
+ private readonly IAuthorizationEvaluator _evaluator;
+ private readonly IAuthorizationPolicyProvider _policyProvider;
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Creates a new instance of <see cref="DefaultAuthorizationService"/>.
+ /// </summary>
+ /// <param name="policyProvider">The <see cref="IAuthorizationPolicyProvider"/> used to provide policies.</param>
+ /// <param name="handlers">The handlers used to fulfill <see cref="IAuthorizationRequirement"/>s.</param>
+ /// <param name="logger">The logger used to log messages, warnings and errors.</param>
+ /// <param name="contextFactory">The <see cref="IAuthorizationHandlerContextFactory"/> used to create the context to handle the authorization.</param>
+ /// <param name="evaluator">The <see cref="IAuthorizationEvaluator"/> used to determine if authorization was successful.</param>
+ /// <param name="options">The <see cref="AuthorizationOptions"/> used.</param>
+ public DefaultAuthorizationService(IAuthorizationPolicyProvider policyProvider, IAuthorizationHandlerProvider handlers, ILogger<DefaultAuthorizationService> logger, IAuthorizationHandlerContextFactory contextFactory, IAuthorizationEvaluator evaluator, IOptions<AuthorizationOptions> options)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+ if (policyProvider == null)
+ {
+ throw new ArgumentNullException(nameof(policyProvider));
+ }
+ if (handlers == null)
+ {
+ throw new ArgumentNullException(nameof(handlers));
+ }
+ if (logger == null)
+ {
+ throw new ArgumentNullException(nameof(logger));
+ }
+ if (contextFactory == null)
+ {
+ throw new ArgumentNullException(nameof(contextFactory));
+ }
+ if (evaluator == null)
+ {
+ throw new ArgumentNullException(nameof(evaluator));
+ }
+
+ _options = options.Value;
+ _handlers = handlers;
+ _policyProvider = policyProvider;
+ _logger = logger;
+ _evaluator = evaluator;
+ _contextFactory = contextFactory;
+ }
+
+ /// <summary>
+ /// Checks if a user meets a specific set of requirements for the specified resource.
+ /// </summary>
+ /// <param name="user">The user to evaluate the requirements against.</param>
+ /// <param name="resource">The resource to evaluate the requirements against.</param>
+ /// <param name="requirements">The requirements to evaluate.</param>
+ /// <returns>
+ /// A flag indicating whether authorization has succeeded.
+ /// This value is <value>true</value> when the user fulfills the policy otherwise <value>false</value>.
+ /// </returns>
+ public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
+ {
+ if (requirements == null)
+ {
+ throw new ArgumentNullException(nameof(requirements));
+ }
+
+ var authContext = _contextFactory.CreateContext(requirements, user, resource);
+ var handlers = await _handlers.GetHandlersAsync(authContext);
+ foreach (var handler in handlers)
+ {
+ await handler.HandleAsync(authContext);
+ if (!_options.InvokeHandlersAfterFailure && authContext.HasFailed)
+ {
+ break;
+ }
+ }
+
+ var result = _evaluator.Evaluate(authContext);
+ if (result.Succeeded)
+ {
+ _logger.UserAuthorizationSucceeded();
+ }
+ else
+ {
+ _logger.UserAuthorizationFailed();
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Checks if a user meets a specific authorization policy.
+ /// </summary>
+ /// <param name="user">The user to check the policy against.</param>
+ /// <param name="resource">The resource the policy should be checked with.</param>
+ /// <param name="policyName">The name of the policy to check against a specific context.</param>
+ /// <returns>
+ /// A flag indicating whether authorization has succeeded.
+ /// This value is <value>true</value> when the user fulfills the policy otherwise <value>false</value>.
+ /// </returns>
+ public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
+ {
+ if (policyName == null)
+ {
+ throw new ArgumentNullException(nameof(policyName));
+ }
+
+ var policy = await _policyProvider.GetPolicyAsync(policyName);
+ if (policy == null)
+ {
+ throw new InvalidOperationException($"No policy found: {policyName}.");
+ }
+ return await this.AuthorizeAsync(user, resource, policy);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAllowAnonymous.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAllowAnonymous.cs
new file mode 100644
index 0000000000..8531c3daab
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAllowAnonymous.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.Authorization
+{
+ /// <summary>
+ /// Marker interface to enable the <see cref="AllowAnonymousAttribute"/>.
+ /// </summary>
+ public interface IAllowAnonymous
+ {
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationEvaluator.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationEvaluator.cs
new file mode 100644
index 0000000000..baa6f828cd
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationEvaluator.cs
@@ -0,0 +1,18 @@
+// 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.Authorization
+{
+ /// <summary>
+ /// Determines whether an authorization request was successful or not.
+ /// </summary>
+ public interface IAuthorizationEvaluator
+ {
+ /// <summary>
+ /// Determines whether the authorization result was successful or not.
+ /// </summary>
+ /// <param name="context">The authorization information.</param>
+ /// <returns>The <see cref="AuthorizationResult"/>.</returns>
+ AuthorizationResult Evaluate(AuthorizationHandlerContext context);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandler.cs
new file mode 100644
index 0000000000..afe9e43f02
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandler.cs
@@ -0,0 +1,19 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// Classes implementing this interface are able to make a decision if authorization is allowed.
+ /// </summary>
+ public interface IAuthorizationHandler
+ {
+ /// <summary>
+ /// Makes a decision if authorization is allowed.
+ /// </summary>
+ /// <param name="context">The authorization information.</param>
+ Task HandleAsync(AuthorizationHandlerContext context);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerContextFactory.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerContextFactory.cs
new file mode 100644
index 0000000000..272109eea9
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerContextFactory.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 System.Collections.Generic;
+using System.Security.Claims;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// A type used to provide a <see cref="AuthorizationHandlerContext"/> used for authorization.
+ /// </summary>
+ public interface IAuthorizationHandlerContextFactory
+ {
+ /// <summary>
+ /// Creates a <see cref="AuthorizationHandlerContext"/> used for authorization.
+ /// </summary>
+ /// <param name="requirements">The requirements to evaluate.</param>
+ /// <param name="user">The user to evaluate the requirements against.</param>
+ /// <param name="resource">
+ /// An optional resource the policy should be checked with.
+ /// If a resource is not required for policy evaluation you may pass null as the value.
+ /// </param>
+ /// <returns>The <see cref="AuthorizationHandlerContext"/>.</returns>
+ AuthorizationHandlerContext CreateContext(IEnumerable<IAuthorizationRequirement> requirements, ClaimsPrincipal user, object resource);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerProvider.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerProvider.cs
new file mode 100644
index 0000000000..7f0d9f5d31
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerProvider.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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// A type which can provide the <see cref="IAuthorizationHandler"/>s for an authorization request.
+ /// </summary>
+ public interface IAuthorizationHandlerProvider
+ {
+ /// <summary>
+ /// Return the handlers that will be called for the authorization request.
+ /// </summary>
+ /// <param name="context">The <see cref="AuthorizationHandlerContext"/>.</param>
+ /// <returns>The list of handlers.</returns>
+ Task<IEnumerable<IAuthorizationHandler>> GetHandlersAsync(AuthorizationHandlerContext context);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs
new file mode 100644
index 0000000000..9e9d0f468a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.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 System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// A type which can provide a <see cref="AuthorizationPolicy"/> for a particular name.
+ /// </summary>
+ public interface IAuthorizationPolicyProvider
+ {
+ /// <summary>
+ /// Gets a <see cref="AuthorizationPolicy"/> from the given <paramref name="policyName"/>
+ /// </summary>
+ /// <param name="policyName">The policy name to retrieve.</param>
+ /// <returns>The named <see cref="AuthorizationPolicy"/>.</returns>
+ Task<AuthorizationPolicy> GetPolicyAsync(string policyName);
+
+ /// <summary>
+ /// Gets the default authorization policy.
+ /// </summary>
+ /// <returns>The default authorization policy.</returns>
+ Task<AuthorizationPolicy> GetDefaultPolicyAsync();
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationRequirement.cs
new file mode 100644
index 0000000000..0bdcaff86a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationRequirement.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.Authorization
+{
+ /// <summary>
+ /// Represents an authorization requirement.
+ /// </summary>
+ public interface IAuthorizationRequirement
+ {
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationService.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationService.cs
new file mode 100644
index 0000000000..8976425ba6
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizationService.cs
@@ -0,0 +1,54 @@
+// 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;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// Checks policy based permissions for a user
+ /// </summary>
+ public interface IAuthorizationService
+ {
+ /// <summary>
+ /// Checks if a user meets a specific set of requirements for the specified resource
+ /// </summary>
+ /// <param name="user">The user to evaluate the requirements against.</param>
+ /// <param name="resource">
+ /// An optional resource the policy should be checked with.
+ /// If a resource is not required for policy evaluation you may pass null as the value.
+ /// </param>
+ /// <param name="requirements">The requirements to evaluate.</param>
+ /// <returns>
+ /// A flag indicating whether authorization has succeeded.
+ /// This value is <value>true</value> when the user fulfills the policy; otherwise <value>false</value>.
+ /// </returns>
+ /// <remarks>
+ /// Resource is an optional parameter and may be null. Please ensure that you check it is not
+ /// null before acting upon it.
+ /// </remarks>
+ Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements);
+
+ /// <summary>
+ /// Checks if a user meets a specific authorization policy
+ /// </summary>
+ /// <param name="user">The user to check the policy against.</param>
+ /// <param name="resource">
+ /// An optional resource the policy should be checked with.
+ /// If a resource is not required for policy evaluation you may pass null as the value.
+ /// </param>
+ /// <param name="policyName">The name of the policy to check against a specific context.</param>
+ /// <returns>
+ /// A flag indicating whether authorization has succeeded.
+ /// Returns a flag indicating whether the user, and optional resource has fulfilled the policy.
+ /// <value>true</value> when the policy has been fulfilled; otherwise <value>false</value>.
+ /// </returns>
+ /// <remarks>
+ /// Resource is an optional parameter and may be null. Please ensure that you check it is not
+ /// null before acting upon it.
+ /// </remarks>
+ Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName);
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizeData.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizeData.cs
new file mode 100644
index 0000000000..1196db82d4
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/IAuthorizeData.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.
+
+namespace Microsoft.AspNetCore.Authorization
+{
+ /// <summary>
+ /// Defines the set of data required to apply authorization rules to a resource.
+ /// </summary>
+ public interface IAuthorizeData
+ {
+ /// <summary>
+ /// Gets or sets the policy name that determines access to the resource.
+ /// </summary>
+ string Policy { get; set; }
+
+ /// <summary>
+ /// Gets or sets a comma delimited list of roles that are allowed to access the resource.
+ /// </summary>
+ string Roles { get; set; }
+
+ /// <summary>
+ /// Gets or sets a comma delimited list of schemes from which user information is constructed.
+ /// </summary>
+ string AuthenticationSchemes { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/AssertionRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/AssertionRequirement.cs
new file mode 100644
index 0000000000..5fa452b733
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/AssertionRequirement.cs
@@ -0,0 +1,60 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authorization.Infrastructure
+{
+ /// <summary>
+ /// Implements an <see cref="IAuthorizationHandler"/> and <see cref="IAuthorizationRequirement"/>
+ /// that takes a user specified assertion.
+ /// </summary>
+ public class AssertionRequirement : IAuthorizationHandler, IAuthorizationRequirement
+ {
+ /// <summary>
+ /// Function that is called to handle this requirement.
+ /// </summary>
+ public Func<AuthorizationHandlerContext, Task<bool>> Handler { get; }
+
+ /// <summary>
+ /// Creates a new instance of <see cref="AssertionRequirement"/>.
+ /// </summary>
+ /// <param name="handler">Function that is called to handle this requirement.</param>
+ public AssertionRequirement(Func<AuthorizationHandlerContext, bool> handler)
+ {
+ if (handler == null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ Handler = context => Task.FromResult(handler(context));
+ }
+
+ /// <summary>
+ /// Creates a new instance of <see cref="AssertionRequirement"/>.
+ /// </summary>
+ /// <param name="handler">Function that is called to handle this requirement.</param>
+ public AssertionRequirement(Func<AuthorizationHandlerContext, Task<bool>> handler)
+ {
+ if (handler == null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ Handler = handler;
+ }
+
+ /// <summary>
+ /// Calls <see cref="AssertionRequirement.Handler"/> to see if authorization is allowed.
+ /// </summary>
+ /// <param name="context">The authorization information.</param>
+ public async Task HandleAsync(AuthorizationHandlerContext context)
+ {
+ if (await Handler(context))
+ {
+ context.Succeed(this);
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/ClaimsAuthorizationRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/ClaimsAuthorizationRequirement.cs
new file mode 100644
index 0000000000..93b1deea6d
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/ClaimsAuthorizationRequirement.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 System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authorization.Infrastructure
+{
+ /// <summary>
+ /// Implements an <see cref="IAuthorizationHandler"/> and <see cref="IAuthorizationRequirement"/>
+ /// which requires at least one instance of the specified claim type, and, if allowed values are specified,
+ /// the claim value must be any of the allowed values.
+ /// </summary>
+ public class ClaimsAuthorizationRequirement : AuthorizationHandler<ClaimsAuthorizationRequirement>, IAuthorizationRequirement
+ {
+ /// <summary>
+ /// Creates a new instance of <see cref="ClaimsAuthorizationRequirement"/>.
+ /// </summary>
+ /// <param name="claimType">The claim type that must be present.</param>
+ /// <param name="allowedValues">The optional list of claim values, which, if present,
+ /// the claim must match.</param>
+ public ClaimsAuthorizationRequirement(string claimType, IEnumerable<string> allowedValues)
+ {
+ if (claimType == null)
+ {
+ throw new ArgumentNullException(nameof(claimType));
+ }
+
+ ClaimType = claimType;
+ AllowedValues = allowedValues;
+ }
+
+ /// <summary>
+ /// Gets the claim type that must be present.
+ /// </summary>
+ public string ClaimType { get; }
+
+ /// <summary>
+ /// Gets the optional list of claim values, which, if present,
+ /// the claim must match.
+ /// </summary>
+ public IEnumerable<string> AllowedValues { get; }
+
+ /// <summary>
+ /// Makes a decision if authorization is allowed based on the claims requirements specified.
+ /// </summary>
+ /// <param name="context">The authorization context.</param>
+ /// <param name="requirement">The requirement to evaluate.</param>
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ClaimsAuthorizationRequirement requirement)
+ {
+ if (context.User != null)
+ {
+ var found = false;
+ if (requirement.AllowedValues == null || !requirement.AllowedValues.Any())
+ {
+ found = context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase));
+ }
+ else
+ {
+ found = context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase)
+ && requirement.AllowedValues.Contains(c.Value, StringComparer.Ordinal));
+ }
+ if (found)
+ {
+ context.Succeed(requirement);
+ }
+ }
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/DenyAnonymousAuthorizationRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/DenyAnonymousAuthorizationRequirement.cs
new file mode 100644
index 0000000000..e88cce7aac
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/DenyAnonymousAuthorizationRequirement.cs
@@ -0,0 +1,33 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authorization.Infrastructure
+{
+ /// <summary>
+ /// Implements an <see cref="IAuthorizationHandler"/> and <see cref="IAuthorizationRequirement"/>
+ /// which requires the current user must be authenticated.
+ /// </summary>
+ public class DenyAnonymousAuthorizationRequirement : AuthorizationHandler<DenyAnonymousAuthorizationRequirement>, IAuthorizationRequirement
+ {
+ /// <summary>
+ /// Makes a decision if authorization is allowed based on a specific requirement.
+ /// </summary>
+ /// <param name="context">The authorization context.</param>
+ /// <param name="requirement">The requirement to evaluate.</param>
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyAnonymousAuthorizationRequirement requirement)
+ {
+ var user = context.User;
+ var userIsAnonymous =
+ user?.Identity == null ||
+ !user.Identities.Any(i => i.IsAuthenticated);
+ if (!userIsAnonymous)
+ {
+ context.Succeed(requirement);
+ }
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/NameAuthorizationRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/NameAuthorizationRequirement.cs
new file mode 100644
index 0000000000..02ab946fad
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/NameAuthorizationRequirement.cs
@@ -0,0 +1,52 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authorization.Infrastructure
+{
+ /// <summary>
+ /// Implements an <see cref="IAuthorizationHandler"/> and <see cref="IAuthorizationRequirement"/>
+ /// which requires the current user name must match the specified value.
+ /// </summary>
+ public class NameAuthorizationRequirement : AuthorizationHandler<NameAuthorizationRequirement>, IAuthorizationRequirement
+ {
+ /// <summary>
+ /// Constructs a new instance of <see cref="NameAuthorizationRequirement"/>.
+ /// </summary>
+ /// <param name="requiredName">The required name that the current user must have.</param>
+ public NameAuthorizationRequirement(string requiredName)
+ {
+ if (requiredName == null)
+ {
+ throw new ArgumentNullException(nameof(requiredName));
+ }
+
+ RequiredName = requiredName;
+ }
+
+ /// <summary>
+ /// Gets the required name that the current user must have.
+ /// </summary>
+ public string RequiredName { get; }
+
+ /// <summary>
+ /// Makes a decision if authorization is allowed based on a specific requirement.
+ /// </summary>
+ /// <param name="context">The authorization context.</param>
+ /// <param name="requirement">The requirement to evaluate.</param>
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NameAuthorizationRequirement requirement)
+ {
+ if (context.User != null)
+ {
+ if (context.User.Identities.Any(i => string.Equals(i.Name, requirement.RequiredName)))
+ {
+ context.Succeed(requirement);
+ }
+ }
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/OperationAuthorizationRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/OperationAuthorizationRequirement.cs
new file mode 100644
index 0000000000..c3f16356d3
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/OperationAuthorizationRequirement.cs
@@ -0,0 +1,17 @@
+// 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.Authorization.Infrastructure
+{
+ /// <summary>
+ /// A helper class to provide a useful <see cref="IAuthorizationRequirement"/> which
+ /// contains a name.
+ /// </summary>
+ public class OperationAuthorizationRequirement : IAuthorizationRequirement
+ {
+ /// <summary>
+ /// The name of this instance of <see cref="IAuthorizationRequirement"/>.
+ /// </summary>
+ public string Name { get; set; }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/PassThroughAuthorizationHandler.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/PassThroughAuthorizationHandler.cs
new file mode 100644
index 0000000000..6f0b8293f8
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/PassThroughAuthorizationHandler.cs
@@ -0,0 +1,27 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authorization.Infrastructure
+{
+ /// <summary>
+ /// Infrastructure class which allows an <see cref="IAuthorizationRequirement"/> to
+ /// be its own <see cref="IAuthorizationHandler"/>.
+ /// </summary>
+ public class PassThroughAuthorizationHandler : IAuthorizationHandler
+ {
+ /// <summary>
+ /// Makes a decision if authorization is allowed.
+ /// </summary>
+ /// <param name="context">The authorization context.</param>
+ public async Task HandleAsync(AuthorizationHandlerContext context)
+ {
+ foreach (var handler in context.Requirements.OfType<IAuthorizationHandler>())
+ {
+ await handler.HandleAsync(context);
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/RolesAuthorizationRequirement.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/RolesAuthorizationRequirement.cs
new file mode 100644
index 0000000000..811e17aacd
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Infrastructure/RolesAuthorizationRequirement.cs
@@ -0,0 +1,68 @@
+// 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.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authorization.Infrastructure
+{
+ /// <summary>
+ /// Implements an <see cref="IAuthorizationHandler"/> and <see cref="IAuthorizationRequirement"/>
+ /// which requires at least one role claim whose value must be any of the allowed roles.
+ /// </summary>
+ public class RolesAuthorizationRequirement : AuthorizationHandler<RolesAuthorizationRequirement>, IAuthorizationRequirement
+ {
+ /// <summary>
+ /// Creates a new instance of <see cref="RolesAuthorizationRequirement"/>.
+ /// </summary>
+ /// <param name="allowedRoles">A collection of allowed roles.</param>
+ public RolesAuthorizationRequirement(IEnumerable<string> allowedRoles)
+ {
+ if (allowedRoles == null)
+ {
+ throw new ArgumentNullException(nameof(allowedRoles));
+ }
+
+ if (allowedRoles.Count() == 0)
+ {
+ throw new InvalidOperationException(Resources.Exception_RoleRequirementEmpty);
+ }
+ AllowedRoles = allowedRoles;
+ }
+
+ /// <summary>
+ /// Gets the collection of allowed roles.
+ /// </summary>
+ public IEnumerable<string> AllowedRoles { get; }
+
+ /// <summary>
+ /// Makes a decision if authorization is allowed based on a specific requirement.
+ /// </summary>
+ /// <param name="context">The authorization context.</param>
+ /// <param name="requirement">The requirement to evaluate.</param>
+
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement)
+ {
+ if (context.User != null)
+ {
+ bool found = false;
+ if (requirement.AllowedRoles == null || !requirement.AllowedRoles.Any())
+ {
+ // Review: What do we want to do here? No roles requested is auto success?
+ }
+ else
+ {
+ found = requirement.AllowedRoles.Any(r => context.User.IsInRole(r));
+ }
+ if (found)
+ {
+ context.Succeed(requirement);
+ }
+ }
+ return Task.CompletedTask;
+ }
+
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/LoggingExtensions.cs
new file mode 100644
index 0000000000..386df85e09
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/LoggingExtensions.cs
@@ -0,0 +1,31 @@
+// 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.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static Action<ILogger, Exception> _userAuthorizationFailed;
+ private static Action<ILogger, Exception> _userAuthorizationSucceeded;
+
+ static LoggingExtensions()
+ {
+ _userAuthorizationSucceeded = LoggerMessage.Define(
+ eventId: 1,
+ logLevel: LogLevel.Information,
+ formatString: "Authorization was successful.");
+ _userAuthorizationFailed = LoggerMessage.Define(
+ eventId: 2,
+ logLevel: LogLevel.Information,
+ formatString: "Authorization failed.");
+ }
+
+ public static void UserAuthorizationSucceeded(this ILogger logger)
+ => _userAuthorizationSucceeded(logger, null);
+
+ public static void UserAuthorizationFailed(this ILogger logger)
+ => _userAuthorizationFailed(logger, null);
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj b/src/Security/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj
new file mode 100644
index 0000000000..ac4aa6c320
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core authorization classes.
+Commonly used types:
+Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute
+Microsoft.AspNetCore.Authorization.AuthorizeAttribute</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;authorization</PackageTags>
+ <EnableApiCheck>false</EnableApiCheck>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/Properties/Resources.Designer.cs b/src/Security/src/Microsoft.AspNetCore.Authorization/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..c83fa9ea5e
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Properties/Resources.Designer.cs
@@ -0,0 +1,72 @@
+// <auto-generated />
+namespace Microsoft.AspNetCore.Authorization
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.AspNetCore.Authorization.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ /// <summary>
+ /// AuthorizationPolicy must have at least one requirement.
+ /// </summary>
+ internal static string Exception_AuthorizationPolicyEmpty
+ {
+ get => GetString("Exception_AuthorizationPolicyEmpty");
+ }
+
+ /// <summary>
+ /// AuthorizationPolicy must have at least one requirement.
+ /// </summary>
+ internal static string FormatException_AuthorizationPolicyEmpty()
+ => GetString("Exception_AuthorizationPolicyEmpty");
+
+ /// <summary>
+ /// The AuthorizationPolicy named: '{0}' was not found.
+ /// </summary>
+ internal static string Exception_AuthorizationPolicyNotFound
+ {
+ get => GetString("Exception_AuthorizationPolicyNotFound");
+ }
+
+ /// <summary>
+ /// The AuthorizationPolicy named: '{0}' was not found.
+ /// </summary>
+ internal static string FormatException_AuthorizationPolicyNotFound(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Exception_AuthorizationPolicyNotFound"), p0);
+
+ /// <summary>
+ /// At least one role must be specified.
+ /// </summary>
+ internal static string Exception_RoleRequirementEmpty
+ {
+ get => GetString("Exception_RoleRequirementEmpty");
+ }
+
+ /// <summary>
+ /// At least one role must be specified.
+ /// </summary>
+ internal static string FormatException_RoleRequirementEmpty()
+ => GetString("Exception_RoleRequirementEmpty");
+
+ 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/Security/src/Microsoft.AspNetCore.Authorization/Resources.resx b/src/Security/src/Microsoft.AspNetCore.Authorization/Resources.resx
new file mode 100644
index 0000000000..a36e55d6b0
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/Resources.resx
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Exception_AuthorizationPolicyEmpty" xml:space="preserve">
+ <value>AuthorizationPolicy must have at least one requirement.</value>
+ </data>
+ <data name="Exception_AuthorizationPolicyNotFound" xml:space="preserve">
+ <value>The AuthorizationPolicy named: '{0}' was not found.</value>
+ </data>
+ <data name="Exception_RoleRequirementEmpty" xml:space="preserve">
+ <value>At least one role must be specified.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json
new file mode 100644
index 0000000000..9910c93f6a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json
@@ -0,0 +1,1947 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.Authorization, Version=2.0.3.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.AuthorizationServiceCollectionExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddAuthorization",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddAuthorization",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "configure",
+ "Type": "System.Action<Microsoft.AspNetCore.Authorization.AuthorizationOptions>"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "System.Attribute",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAllowAnonymous"
+ ],
+ "Members": [
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.AuthorizationFailure",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_FailCalled",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_FailedRequirements",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ExplicitFail",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationFailure",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Failed",
+ "Parameters": [
+ {
+ "Name": "failed",
+ "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationFailure",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.AuthorizationHandler<T0>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationHandler"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "HandleAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleRequirementAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ },
+ {
+ "Name": "requirement",
+ "Type": "T0"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Abstract": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Protected",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TRequirement",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement"
+ ]
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.AuthorizationHandler<T0, T1>",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationHandler"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "HandleAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleRequirementAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ },
+ {
+ "Name": "requirement",
+ "Type": "T0"
+ },
+ {
+ "Name": "resource",
+ "Type": "T1"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Abstract": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Protected",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TRequirement",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement"
+ ]
+ },
+ {
+ "ParameterName": "TResource",
+ "ParameterPosition": 1,
+ "BaseTypeOrInterfaces": []
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Requirements",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement>",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_User",
+ "Parameters": [],
+ "ReturnType": "System.Security.Claims.ClaimsPrincipal",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Resource",
+ "Parameters": [],
+ "ReturnType": "System.Object",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_PendingRequirements",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement>",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_HasFailed",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_HasSucceeded",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Fail",
+ "Parameters": [],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Succeed",
+ "Parameters": [
+ {
+ "Name": "requirement",
+ "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "requirements",
+ "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement>"
+ },
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "resource",
+ "Type": "System.Object"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.AuthorizationOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_InvokeHandlersAfterFailure",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_InvokeHandlersAfterFailure",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_DefaultPolicy",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_DefaultPolicy",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddPolicy",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddPolicy",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "configurePolicy",
+ "Type": "System.Action<Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetPolicy",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Requirements",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AuthenticationSchemes",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IReadOnlyList<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Combine",
+ "Parameters": [
+ {
+ "Name": "policies",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Combine",
+ "Parameters": [
+ {
+ "Name": "policies",
+ "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.AuthorizationPolicy>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CombineAsync",
+ "Parameters": [
+ {
+ "Name": "policyProvider",
+ "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider"
+ },
+ {
+ "Name": "authorizeData",
+ "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizeData>"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationPolicy>",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "requirements",
+ "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement>"
+ },
+ {
+ "Name": "authenticationSchemes",
+ "Type": "System.Collections.Generic.IEnumerable<System.String>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Requirements",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Requirements",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Collections.Generic.IList<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AuthenticationSchemes",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IList<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AuthenticationSchemes",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Collections.Generic.IList<System.String>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddAuthenticationSchemes",
+ "Parameters": [
+ {
+ "Name": "schemes",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddRequirements",
+ "Parameters": [
+ {
+ "Name": "requirements",
+ "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Combine",
+ "Parameters": [
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RequireClaim",
+ "Parameters": [
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "requiredValues",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RequireClaim",
+ "Parameters": [
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "requiredValues",
+ "Type": "System.Collections.Generic.IEnumerable<System.String>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RequireClaim",
+ "Parameters": [
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RequireRole",
+ "Parameters": [
+ {
+ "Name": "roles",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RequireRole",
+ "Parameters": [
+ {
+ "Name": "roles",
+ "Type": "System.Collections.Generic.IEnumerable<System.String>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RequireUserName",
+ "Parameters": [
+ {
+ "Name": "userName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RequireAuthenticatedUser",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RequireAssertion",
+ "Parameters": [
+ {
+ "Name": "handler",
+ "Type": "System.Func<Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext, System.Boolean>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "RequireAssertion",
+ "Parameters": [
+ {
+ "Name": "handler",
+ "Type": "System.Func<Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext, System.Threading.Tasks.Task<System.Boolean>>"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Build",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "authenticationSchemes",
+ "Type": "System.String[]",
+ "IsParams": true
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.AuthorizationResult",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Succeeded",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Failure",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationFailure",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Success",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Failed",
+ "Parameters": [
+ {
+ "Name": "failure",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationFailure"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Failed",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.AuthorizationServiceExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AuthorizeAsync",
+ "Parameters": [
+ {
+ "Name": "service",
+ "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService"
+ },
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "resource",
+ "Type": "System.Object"
+ },
+ {
+ "Name": "requirement",
+ "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationResult>",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AuthorizeAsync",
+ "Parameters": [
+ {
+ "Name": "service",
+ "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService"
+ },
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "resource",
+ "Type": "System.Object"
+ },
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationResult>",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AuthorizeAsync",
+ "Parameters": [
+ {
+ "Name": "service",
+ "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService"
+ },
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "policy",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationResult>",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AuthorizeAsync",
+ "Parameters": [
+ {
+ "Name": "service",
+ "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService"
+ },
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "policyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationResult>",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.AuthorizeAttribute",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "System.Attribute",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizeData"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Policy",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Policy",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Roles",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Roles",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AuthenticationSchemes",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AuthenticationSchemes",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ActiveAuthenticationSchemes",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ActiveAuthenticationSchemes",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "policy",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationEvaluator",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Evaluate",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationHandlerContextFactory",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateContext",
+ "Parameters": [
+ {
+ "Name": "requirements",
+ "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement>"
+ },
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "resource",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationHandlerProvider",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetHandlersAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationHandler>>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "handlers",
+ "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationHandler>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationPolicyProvider",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetDefaultPolicyAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationPolicy>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetPolicyAsync",
+ "Parameters": [
+ {
+ "Name": "policyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationPolicy>",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Authorization.AuthorizationOptions>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationService",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationService"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AuthorizeAsync",
+ "Parameters": [
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "resource",
+ "Type": "System.Object"
+ },
+ {
+ "Name": "requirements",
+ "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement>"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationResult>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationService",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AuthorizeAsync",
+ "Parameters": [
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "resource",
+ "Type": "System.Object"
+ },
+ {
+ "Name": "policyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationResult>",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationService",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "policyProvider",
+ "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider"
+ },
+ {
+ "Name": "handlers",
+ "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Authorization.DefaultAuthorizationService>"
+ },
+ {
+ "Name": "contextFactory",
+ "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory"
+ },
+ {
+ "Name": "evaluator",
+ "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Authorization.AuthorizationOptions>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.IAllowAnonymous",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Evaluate",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "HandleAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "CreateContext",
+ "Parameters": [
+ {
+ "Name": "requirements",
+ "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement>"
+ },
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "resource",
+ "Type": "System.Object"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetHandlersAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationHandler>>",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetPolicyAsync",
+ "Parameters": [
+ {
+ "Name": "policyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationPolicy>",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetDefaultPolicyAsync",
+ "Parameters": [],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationPolicy>",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationService",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AuthorizeAsync",
+ "Parameters": [
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "resource",
+ "Type": "System.Object"
+ },
+ {
+ "Name": "requirements",
+ "Type": "System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Authorization.IAuthorizationRequirement>"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationResult>",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AuthorizeAsync",
+ "Parameters": [
+ {
+ "Name": "user",
+ "Type": "System.Security.Claims.ClaimsPrincipal"
+ },
+ {
+ "Name": "resource",
+ "Type": "System.Object"
+ },
+ {
+ "Name": "policyName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.Authorization.AuthorizationResult>",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.IAuthorizeData",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Policy",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Policy",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Roles",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Roles",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AuthenticationSchemes",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_AuthenticationSchemes",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.AssertionRequirement",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationHandler",
+ "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Handler",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext, System.Threading.Tasks.Task<System.Boolean>>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "handler",
+ "Type": "System.Func<Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext, System.Boolean>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "handler",
+ "Type": "System.Func<Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext, System.Threading.Tasks.Task<System.Boolean>>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authorization.AuthorizationHandler<Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement>",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ClaimType",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_AllowedValues",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IEnumerable<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleRequirementAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ },
+ {
+ "Name": "requirement",
+ "Type": "Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "claimType",
+ "Type": "System.String"
+ },
+ {
+ "Name": "allowedValues",
+ "Type": "System.Collections.Generic.IEnumerable<System.String>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.DenyAnonymousAuthorizationRequirement",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authorization.AuthorizationHandler<Microsoft.AspNetCore.Authorization.Infrastructure.DenyAnonymousAuthorizationRequirement>",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "HandleRequirementAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ },
+ {
+ "Name": "requirement",
+ "Type": "Microsoft.AspNetCore.Authorization.Infrastructure.DenyAnonymousAuthorizationRequirement"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.NameAuthorizationRequirement",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authorization.AuthorizationHandler<Microsoft.AspNetCore.Authorization.Infrastructure.NameAuthorizationRequirement>",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_RequiredName",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleRequirementAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ },
+ {
+ "Name": "requirement",
+ "Type": "Microsoft.AspNetCore.Authorization.Infrastructure.NameAuthorizationRequirement"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "requiredName",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.OperationAuthorizationRequirement",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Name",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Name",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.PassThroughAuthorizationHandler",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationHandler"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "HandleAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.RolesAuthorizationRequirement",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.AspNetCore.Authorization.AuthorizationHandler<Microsoft.AspNetCore.Authorization.Infrastructure.RolesAuthorizationRequirement>",
+ "ImplementedInterfaces": [
+ "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_AllowedRoles",
+ "Parameters": [],
+ "ReturnType": "System.Collections.Generic.IEnumerable<System.String>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "HandleRequirementAsync",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext"
+ },
+ {
+ "Name": "requirement",
+ "Type": "Microsoft.AspNetCore.Authorization.Infrastructure.RolesAuthorizationRequirement"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "allowedRoles",
+ "Type": "System.Collections.Generic.IEnumerable<System.String>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs
new file mode 100644
index 0000000000..bbb4899c04
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.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.CookiePolicy
+{
+ public class AppendCookieContext
+ {
+ public AppendCookieContext(HttpContext context, CookieOptions options, string name, string value)
+ {
+ Context = context;
+ CookieOptions = options;
+ CookieName = name;
+ CookieValue = value;
+ }
+
+ public HttpContext Context { get; }
+ public CookieOptions CookieOptions { get; }
+ public string CookieName { get; set; }
+ public string CookieValue { get; set; }
+ public bool IsConsentNeeded { get; internal set; }
+ public bool HasConsent { get; internal set; }
+ public bool IssueCookie { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyAppBuilderExtensions.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyAppBuilderExtensions.cs
new file mode 100644
index 0000000000..1564193b9e
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyAppBuilderExtensions.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 System;
+using Microsoft.AspNetCore.CookiePolicy;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Extension methods to add cookie policy capabilities to an HTTP application pipeline.
+ /// </summary>
+ public static class CookiePolicyAppBuilderExtensions
+ {
+ /// <summary>
+ /// Adds the <see cref="CookiePolicyMiddleware"/> handler to the specified <see cref="IApplicationBuilder"/>, which enables cookie policy capabilities.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public static IApplicationBuilder UseCookiePolicy(this IApplicationBuilder app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseMiddleware<CookiePolicyMiddleware>();
+ }
+
+ /// <summary>
+ /// Adds the <see cref="CookiePolicyMiddleware"/> handler to the specified <see cref="IApplicationBuilder"/>, which enables cookie policy capabilities.
+ /// </summary>
+ /// <param name="app">The <see cref="IApplicationBuilder"/> to add the handler to.</param>
+ /// <param name="options">A <see cref="CookiePolicyOptions"/> that specifies options for the handler.</param>
+ /// <returns>A reference to this instance after the operation has completed.</returns>
+ public static IApplicationBuilder UseCookiePolicy(this IApplicationBuilder app, CookiePolicyOptions options)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ return app.UseMiddleware<CookiePolicyMiddleware>(Options.Create(options));
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs
new file mode 100644
index 0000000000..1a810b7d55
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs
@@ -0,0 +1,56 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.CookiePolicy
+{
+ public class CookiePolicyMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+
+ public CookiePolicyMiddleware(RequestDelegate next, IOptions<CookiePolicyOptions> options, ILoggerFactory factory)
+ {
+ Options = options.Value;
+ _next = next ?? throw new ArgumentNullException(nameof(next));
+ _logger = factory.CreateLogger<CookiePolicyMiddleware>();
+ }
+
+ public CookiePolicyMiddleware(RequestDelegate next, IOptions<CookiePolicyOptions> options)
+ {
+ Options = options.Value;
+ _next = next;
+ _logger = NullLogger.Instance;
+ }
+
+ public CookiePolicyOptions Options { get; set; }
+
+ public Task Invoke(HttpContext context)
+ {
+ var feature = context.Features.Get<IResponseCookiesFeature>() ?? new ResponseCookiesFeature(context.Features);
+ var wrapper = new ResponseCookiesWrapper(context, Options, feature, _logger);
+ context.Features.Set<IResponseCookiesFeature>(new CookiesWrapperFeature(wrapper));
+ context.Features.Set<ITrackingConsentFeature>(wrapper);
+
+ return _next(context);
+ }
+
+ private class CookiesWrapperFeature : IResponseCookiesFeature
+ {
+ public CookiesWrapperFeature(ResponseCookiesWrapper wrapper)
+ {
+ Cookies = wrapper;
+ }
+
+ public IResponseCookies Cookies { get; }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs
new file mode 100644
index 0000000000..32d047297a
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs
@@ -0,0 +1,52 @@
+// 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.CookiePolicy;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ /// <summary>
+ /// Provides programmatic configuration for the <see cref="CookiePolicyMiddleware"/>.
+ /// </summary>
+ public class CookiePolicyOptions
+ {
+ /// <summary>
+ /// Affects the cookie's same site attribute.
+ /// </summary>
+ public SameSiteMode MinimumSameSitePolicy { get; set; } = SameSiteMode.Lax;
+
+ /// <summary>
+ /// Affects whether cookies must be HttpOnly.
+ /// </summary>
+ public HttpOnlyPolicy HttpOnly { get; set; } = HttpOnlyPolicy.None;
+
+ /// <summary>
+ /// Affects whether cookies must be Secure.
+ /// </summary>
+ public CookieSecurePolicy Secure { get; set; } = CookieSecurePolicy.None;
+
+ public CookieBuilder ConsentCookie { get; set; } = new CookieBuilder()
+ {
+ Name = ".AspNet.Consent",
+ Expiration = TimeSpan.FromDays(365),
+ IsEssential = true,
+ };
+
+ /// <summary>
+ /// Checks if consent policies should be evaluated on this request. The default is false.
+ /// </summary>
+ public Func<HttpContext, bool> CheckConsentNeeded { get; set; }
+
+ /// <summary>
+ /// Called when a cookie is appended.
+ /// </summary>
+ public Action<AppendCookieContext> OnAppendCookie { get; set; }
+
+ /// <summary>
+ /// Called when a cookie is deleted.
+ /// </summary>
+ public Action<DeleteCookieContext> OnDeleteCookie { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs
new file mode 100644
index 0000000000..fd79ea8d4b
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs
@@ -0,0 +1,24 @@
+// 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.CookiePolicy
+{
+ public class DeleteCookieContext
+ {
+ public DeleteCookieContext(HttpContext context, CookieOptions options, string name)
+ {
+ Context = context;
+ CookieOptions = options;
+ CookieName = name;
+ }
+
+ public HttpContext Context { get; }
+ public CookieOptions CookieOptions { get; }
+ public string CookieName { get; set; }
+ public bool IsConsentNeeded { get; internal set; }
+ public bool HasConsent { get; internal set; }
+ public bool IssueCookie { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/HttpOnlyPolicy.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/HttpOnlyPolicy.cs
new file mode 100644
index 0000000000..82305f4754
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/HttpOnlyPolicy.cs
@@ -0,0 +1,11 @@
+// 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.CookiePolicy
+{
+ public enum HttpOnlyPolicy
+ {
+ None,
+ Always
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/LoggingExtensions.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/LoggingExtensions.cs
new file mode 100644
index 0000000000..21b04facc9
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/LoggingExtensions.cs
@@ -0,0 +1,105 @@
+// 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.Extensions.Logging
+{
+ internal static class LoggingExtensions
+ {
+ private static Action<ILogger, bool, Exception> _needsConsent;
+ private static Action<ILogger, bool, Exception> _hasConsent;
+ private static Action<ILogger, Exception> _consentGranted;
+ private static Action<ILogger, Exception> _consentWithdrawn;
+ private static Action<ILogger, string, Exception> _cookieSuppressed;
+ private static Action<ILogger, string, Exception> _deleteCookieSuppressed;
+ private static Action<ILogger, string, Exception> _upgradedToSecure;
+ private static Action<ILogger, string, string, Exception> _upgradedSameSite;
+ private static Action<ILogger, string, Exception> _upgradedToHttpOnly;
+
+ static LoggingExtensions()
+ {
+ _needsConsent = LoggerMessage.Define<bool>(
+ eventId: 1,
+ logLevel: LogLevel.Trace,
+ formatString: "Needs consent: {needsConsent}.");
+ _hasConsent = LoggerMessage.Define<bool>(
+ eventId: 2,
+ logLevel: LogLevel.Trace,
+ formatString: "Has consent: {hasConsent}.");
+ _consentGranted = LoggerMessage.Define(
+ eventId: 3,
+ logLevel: LogLevel.Debug,
+ formatString: "Consent granted.");
+ _consentWithdrawn = LoggerMessage.Define(
+ eventId: 4,
+ logLevel: LogLevel.Debug,
+ formatString: "Consent withdrawn.");
+ _cookieSuppressed = LoggerMessage.Define<string>(
+ eventId: 5,
+ logLevel: LogLevel.Debug,
+ formatString: "Cookie '{key}' suppressed due to consent policy.");
+ _deleteCookieSuppressed = LoggerMessage.Define<string>(
+ eventId: 6,
+ logLevel: LogLevel.Debug,
+ formatString: "Delete cookie '{key}' suppressed due to developer policy.");
+ _upgradedToSecure = LoggerMessage.Define<string>(
+ eventId: 7,
+ logLevel: LogLevel.Debug,
+ formatString: "Cookie '{key}' upgraded to 'secure'.");
+ _upgradedSameSite = LoggerMessage.Define<string, string>(
+ eventId: 8,
+ logLevel: LogLevel.Debug,
+ formatString: "Cookie '{key}' same site mode upgraded to '{mode}'.");
+ _upgradedToHttpOnly = LoggerMessage.Define<string>(
+ eventId: 9,
+ logLevel: LogLevel.Debug,
+ formatString: "Cookie '{key}' upgraded to 'httponly'.");
+ }
+
+ public static void NeedsConsent(this ILogger logger, bool needsConsent)
+ {
+ _needsConsent(logger, needsConsent, null);
+ }
+
+ public static void HasConsent(this ILogger logger, bool hasConsent)
+ {
+ _hasConsent(logger, hasConsent, null);
+ }
+
+ public static void ConsentGranted(this ILogger logger)
+ {
+ _consentGranted(logger, null);
+ }
+
+ public static void ConsentWithdrawn(this ILogger logger)
+ {
+ _consentWithdrawn(logger, null);
+ }
+
+ public static void CookieSuppressed(this ILogger logger, string key)
+ {
+ _cookieSuppressed(logger, key, null);
+ }
+
+ public static void DeleteCookieSuppressed(this ILogger logger, string key)
+ {
+ _deleteCookieSuppressed(logger, key, null);
+ }
+
+ public static void CookieUpgradedToSecure(this ILogger logger, string key)
+ {
+ _upgradedToSecure(logger, key, null);
+ }
+
+ public static void CookieSameSiteUpgraded(this ILogger logger, string key, string mode)
+ {
+ _upgradedSameSite(logger, key, mode, null);
+ }
+
+ public static void CookieUpgradedToHttpOnly(this ILogger logger, string key)
+ {
+ _upgradedToHttpOnly(logger, key, null);
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.csproj b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.csproj
new file mode 100644
index 0000000000..40f97633ae
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>ASP.NET Core cookie policy classes to control the behavior of cookies.</Description>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs
new file mode 100644
index 0000000000..126c4d7bd5
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs
@@ -0,0 +1,281 @@
+// 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.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.CookiePolicy
+{
+ internal class ResponseCookiesWrapper : IResponseCookies, ITrackingConsentFeature
+ {
+ private const string ConsentValue = "yes";
+ private readonly ILogger _logger;
+ private bool? _isConsentNeeded;
+ private bool? _hasConsent;
+
+ public ResponseCookiesWrapper(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature, ILogger logger)
+ {
+ Context = context;
+ Feature = feature;
+ Options = options;
+ _logger = logger;
+ }
+
+ private HttpContext Context { get; }
+
+ private IResponseCookiesFeature Feature { get; }
+
+ private IResponseCookies Cookies => Feature.Cookies;
+
+ private CookiePolicyOptions Options { get; }
+
+ public bool IsConsentNeeded
+ {
+ get
+ {
+ if (!_isConsentNeeded.HasValue)
+ {
+ _isConsentNeeded = Options.CheckConsentNeeded == null ? false
+ : Options.CheckConsentNeeded(Context);
+ _logger.NeedsConsent(_isConsentNeeded.Value);
+ }
+
+ return _isConsentNeeded.Value;
+ }
+ }
+
+ public bool HasConsent
+ {
+ get
+ {
+ if (!_hasConsent.HasValue)
+ {
+ var cookie = Context.Request.Cookies[Options.ConsentCookie.Name];
+ _hasConsent = string.Equals(cookie, ConsentValue, StringComparison.Ordinal);
+ _logger.HasConsent(_hasConsent.Value);
+ }
+
+ return _hasConsent.Value;
+ }
+ }
+
+ public bool CanTrack => !IsConsentNeeded || HasConsent;
+
+ public void GrantConsent()
+ {
+ if (!HasConsent && !Context.Response.HasStarted)
+ {
+ var cookieOptions = Options.ConsentCookie.Build(Context);
+ // Note policy will be applied. We don't want to bypass policy because we want HttpOnly, Secure, etc. to apply.
+ Append(Options.ConsentCookie.Name, ConsentValue, cookieOptions);
+ _logger.ConsentGranted();
+ }
+ _hasConsent = true;
+ }
+
+ public void WithdrawConsent()
+ {
+ if (HasConsent && !Context.Response.HasStarted)
+ {
+ var cookieOptions = Options.ConsentCookie.Build(Context);
+ // Note policy will be applied. We don't want to bypass policy because we want HttpOnly, Secure, etc. to apply.
+ Delete(Options.ConsentCookie.Name, cookieOptions);
+ _logger.ConsentWithdrawn();
+ }
+ _hasConsent = false;
+ }
+
+ // Note policy will be applied. We don't want to bypass policy because we want HttpOnly, Secure, etc. to apply.
+ public string CreateConsentCookie()
+ {
+ var key = Options.ConsentCookie.Name;
+ var value = ConsentValue;
+ var options = Options.ConsentCookie.Build(Context);
+ ApplyAppendPolicy(ref key, ref value, options);
+
+ var setCookieHeaderValue = new Net.Http.Headers.SetCookieHeaderValue(
+ Uri.EscapeDataString(key),
+ Uri.EscapeDataString(value))
+ {
+ Domain = options.Domain,
+ Path = options.Path,
+ Expires = options.Expires,
+ MaxAge = options.MaxAge,
+ Secure = options.Secure,
+ SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite,
+ HttpOnly = options.HttpOnly
+ };
+
+ return setCookieHeaderValue.ToString();
+ }
+
+ private bool CheckPolicyRequired()
+ {
+ return !CanTrack
+ || Options.MinimumSameSitePolicy != SameSiteMode.None
+ || Options.HttpOnly != HttpOnlyPolicy.None
+ || Options.Secure != CookieSecurePolicy.None;
+ }
+
+ public void Append(string key, string value)
+ {
+ if (CheckPolicyRequired() || Options.OnAppendCookie != null)
+ {
+ Append(key, value, new CookieOptions());
+ }
+ else
+ {
+ Cookies.Append(key, value);
+ }
+ }
+
+ public void Append(string key, string value, CookieOptions options)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ if (ApplyAppendPolicy(ref key, ref value, options))
+ {
+ Cookies.Append(key, value, options);
+ }
+ else
+ {
+ _logger.CookieSuppressed(key);
+ }
+ }
+
+ private bool ApplyAppendPolicy(ref string key, ref string value, CookieOptions options)
+ {
+ var issueCookie = CanTrack || options.IsEssential;
+ ApplyPolicy(key, options);
+ if (Options.OnAppendCookie != null)
+ {
+ var context = new AppendCookieContext(Context, options, key, value)
+ {
+ IsConsentNeeded = IsConsentNeeded,
+ HasConsent = HasConsent,
+ IssueCookie = issueCookie,
+ };
+ Options.OnAppendCookie(context);
+
+ key = context.CookieName;
+ value = context.CookieValue;
+ issueCookie = context.IssueCookie;
+ }
+
+ return issueCookie;
+ }
+
+ public void Delete(string key)
+ {
+ if (CheckPolicyRequired() || Options.OnDeleteCookie != null)
+ {
+ Delete(key, new CookieOptions());
+ }
+ else
+ {
+ Cookies.Delete(key);
+ }
+ }
+
+ public void Delete(string key, CookieOptions options)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ // Assume you can always delete cookies unless directly overridden in the user event.
+ var issueCookie = true;
+ ApplyPolicy(key, options);
+ if (Options.OnDeleteCookie != null)
+ {
+ var context = new DeleteCookieContext(Context, options, key)
+ {
+ IsConsentNeeded = IsConsentNeeded,
+ HasConsent = HasConsent,
+ IssueCookie = issueCookie,
+ };
+ Options.OnDeleteCookie(context);
+
+ key = context.CookieName;
+ issueCookie = context.IssueCookie;
+ }
+
+ if (issueCookie)
+ {
+ Cookies.Delete(key, options);
+ }
+ else
+ {
+ _logger.DeleteCookieSuppressed(key);
+ }
+ }
+
+ private void ApplyPolicy(string key, CookieOptions options)
+ {
+ switch (Options.Secure)
+ {
+ case CookieSecurePolicy.Always:
+ if (!options.Secure)
+ {
+ options.Secure = true;
+ _logger.CookieUpgradedToSecure(key);
+ }
+ break;
+ case CookieSecurePolicy.SameAsRequest:
+ // Never downgrade a cookie
+ if (Context.Request.IsHttps && !options.Secure)
+ {
+ options.Secure = true;
+ _logger.CookieUpgradedToSecure(key);
+ }
+ break;
+ case CookieSecurePolicy.None:
+ break;
+ default:
+ throw new InvalidOperationException();
+ }
+ switch (Options.MinimumSameSitePolicy)
+ {
+ case SameSiteMode.None:
+ break;
+ case SameSiteMode.Lax:
+ if (options.SameSite == SameSiteMode.None)
+ {
+ options.SameSite = SameSiteMode.Lax;
+ _logger.CookieSameSiteUpgraded(key, "lax");
+ }
+ break;
+ case SameSiteMode.Strict:
+ if (options.SameSite != SameSiteMode.Strict)
+ {
+ options.SameSite = SameSiteMode.Strict;
+ _logger.CookieSameSiteUpgraded(key, "strict");
+ }
+ break;
+ default:
+ throw new InvalidOperationException($"Unrecognized {nameof(SameSiteMode)} value {Options.MinimumSameSitePolicy.ToString()}");
+ }
+ switch (Options.HttpOnly)
+ {
+ case HttpOnlyPolicy.Always:
+ if (!options.HttpOnly)
+ {
+ options.HttpOnly = true;
+ _logger.CookieUpgradedToHttpOnly(key);
+ }
+ break;
+ case HttpOnlyPolicy.None:
+ break;
+ default:
+ throw new InvalidOperationException($"Unrecognized {nameof(HttpOnlyPolicy)} value {Options.HttpOnly.ToString()}");
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json
new file mode 100644
index 0000000000..01a16c57a9
--- /dev/null
+++ b/src/Security/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json
@@ -0,0 +1,548 @@
+{
+ "AssemblyIdentity": "Microsoft.AspNetCore.CookiePolicy, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.AspNetCore.Builder.CookiePolicyAppBuilderExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "UseCookiePolicy",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "UseCookiePolicy",
+ "Parameters": [
+ {
+ "Name": "app",
+ "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Builder.CookiePolicyOptions"
+ }
+ ],
+ "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.Builder.CookiePolicyOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_MinimumSameSitePolicy",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.SameSiteMode",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_MinimumSameSitePolicy",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.SameSiteMode"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_HttpOnly",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_HttpOnly",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Secure",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieSecurePolicy",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Secure",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.CookieSecurePolicy"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ConsentCookie",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ConsentCookie",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Http.CookieBuilder"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CheckConsentNeeded",
+ "Parameters": [],
+ "ReturnType": "System.Func<Microsoft.AspNetCore.Http.HttpContext, System.Boolean>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CheckConsentNeeded",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Func<Microsoft.AspNetCore.Http.HttpContext, System.Boolean>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnAppendCookie",
+ "Parameters": [],
+ "ReturnType": "System.Action<Microsoft.AspNetCore.CookiePolicy.AppendCookieContext>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnAppendCookie",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Action<Microsoft.AspNetCore.CookiePolicy.AppendCookieContext>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_OnDeleteCookie",
+ "Parameters": [],
+ "ReturnType": "System.Action<Microsoft.AspNetCore.CookiePolicy.DeleteCookieContext>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_OnDeleteCookie",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Action<Microsoft.AspNetCore.CookiePolicy.DeleteCookieContext>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.CookiePolicy.AppendCookieContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Context",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.HttpContext",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookieOptions",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions",
+ "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_CookieValue",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_CookieValue",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IsConsentNeeded",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_HasConsent",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IssueCookie",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_IssueCookie",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Http.CookieOptions"
+ },
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.CookiePolicy.CookiePolicyMiddleware",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Options",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Builder.CookiePolicyOptions",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_Options",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "Microsoft.AspNetCore.Builder.CookiePolicyOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Invoke",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ }
+ ],
+ "ReturnType": "System.Threading.Tasks.Task",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "next",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Builder.CookiePolicyOptions>"
+ },
+ {
+ "Name": "factory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "next",
+ "Type": "Microsoft.AspNetCore.Http.RequestDelegate"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Builder.CookiePolicyOptions>"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.CookiePolicy.DeleteCookieContext",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Context",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.HttpContext",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_CookieOptions",
+ "Parameters": [],
+ "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions",
+ "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_IsConsentNeeded",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_HasConsent",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_IssueCookie",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_IssueCookie",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.AspNetCore.Http.HttpContext"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.AspNetCore.Http.CookieOptions"
+ },
+ {
+ "Name": "name",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy",
+ "Visibility": "Public",
+ "Kind": "Enumeration",
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Field",
+ "Name": "None",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "0"
+ },
+ {
+ "Kind": "Field",
+ "Name": "Always",
+ "Parameters": [],
+ "GenericParameter": [],
+ "Literal": "1"
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketDataFormat.cs b/src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketDataFormat.cs
new file mode 100644
index 0000000000..f1a07c5bf7
--- /dev/null
+++ b/src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketDataFormat.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.Owin.Security.DataHandler;
+using Microsoft.Owin.Security.DataHandler.Encoder;
+using Microsoft.Owin.Security.DataProtection;
+
+namespace Microsoft.Owin.Security.Interop
+{
+ public class AspNetTicketDataFormat : SecureDataFormat<AuthenticationTicket>
+ {
+ public AspNetTicketDataFormat(IDataProtector protector)
+ : base(AspNetTicketSerializer.Default, protector, TextEncodings.Base64Url)
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketSerializer.cs b/src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketSerializer.cs
new file mode 100644
index 0000000000..6a1019fbc8
--- /dev/null
+++ b/src/Security/src/Microsoft.Owin.Security.Interop/AspNetTicketSerializer.cs
@@ -0,0 +1,220 @@
+// Copyright (c) Microsoft Open Technologies, Inc. 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 System.Linq;
+using System.Security.Claims;
+using Microsoft.Owin.Security.DataHandler.Serializer;
+
+namespace Microsoft.Owin.Security.Interop
+{
+ // This MUST be kept in sync with Microsoft.AspNetCore.Authentication.DataHandler.TicketSerializer
+ public class AspNetTicketSerializer : IDataSerializer<AuthenticationTicket>
+ {
+ private const string DefaultStringPlaceholder = "\0";
+ private const int FormatVersion = 5;
+
+ public static AspNetTicketSerializer Default { get; } = new AspNetTicketSerializer();
+
+ public virtual byte[] Serialize(AuthenticationTicket ticket)
+ {
+ using (var memory = new MemoryStream())
+ {
+ using (var writer = new BinaryWriter(memory))
+ {
+ Write(writer, ticket);
+ }
+ return memory.ToArray();
+ }
+ }
+
+ public virtual AuthenticationTicket Deserialize(byte[] data)
+ {
+ using (var memory = new MemoryStream(data))
+ {
+ using (var reader = new BinaryReader(memory))
+ {
+ return Read(reader);
+ }
+ }
+ }
+
+ public virtual void Write(BinaryWriter writer, AuthenticationTicket ticket)
+ {
+ writer.Write(FormatVersion);
+ writer.Write(ticket.Identity.AuthenticationType);
+
+ var identity = ticket.Identity;
+ if (identity == null)
+ {
+ throw new ArgumentNullException("ticket.Identity");
+ }
+
+ // There is always a single identity
+ writer.Write(1);
+ WriteIdentity(writer, identity);
+ PropertiesSerializer.Write(writer, ticket.Properties);
+ }
+
+ protected virtual void WriteIdentity(BinaryWriter writer, ClaimsIdentity identity)
+ {
+ var authenticationType = identity.AuthenticationType ?? string.Empty;
+
+ writer.Write(authenticationType);
+ WriteWithDefault(writer, identity.NameClaimType, ClaimsIdentity.DefaultNameClaimType);
+ WriteWithDefault(writer, identity.RoleClaimType, ClaimsIdentity.DefaultRoleClaimType);
+
+ // Write the number of claims contained in the identity.
+ writer.Write(identity.Claims.Count());
+
+ foreach (var claim in identity.Claims)
+ {
+ WriteClaim(writer, claim);
+ }
+
+ var bootstrap = identity.BootstrapContext as string;
+ if (!string.IsNullOrEmpty(bootstrap))
+ {
+ writer.Write(true);
+ writer.Write(bootstrap);
+ }
+ else
+ {
+ writer.Write(false);
+ }
+
+ if (identity.Actor != null)
+ {
+ writer.Write(true);
+ WriteIdentity(writer, identity.Actor);
+ }
+ else
+ {
+ writer.Write(false);
+ }
+ }
+
+ protected virtual void WriteClaim(BinaryWriter writer, Claim claim)
+ {
+ WriteWithDefault(writer, claim.Type, claim.Subject?.NameClaimType ?? ClaimsIdentity.DefaultNameClaimType);
+ writer.Write(claim.Value);
+ WriteWithDefault(writer, claim.ValueType, ClaimValueTypes.String);
+ WriteWithDefault(writer, claim.Issuer, ClaimsIdentity.DefaultIssuer);
+ WriteWithDefault(writer, claim.OriginalIssuer, claim.Issuer);
+
+ // Write the number of properties contained in the claim.
+ writer.Write(claim.Properties.Count);
+
+ foreach (var property in claim.Properties)
+ {
+ writer.Write(property.Key ?? string.Empty);
+ writer.Write(property.Value ?? string.Empty);
+ }
+ }
+
+ public virtual AuthenticationTicket Read(BinaryReader reader)
+ {
+ if (reader.ReadInt32() != FormatVersion)
+ {
+ return null;
+ }
+
+ var scheme = reader.ReadString();
+
+ // Any identities after the first will be ignored.
+ var count = reader.ReadInt32();
+ if (count < 0)
+ {
+ return null;
+ }
+
+ var identity = ReadIdentity(reader);
+ var properties = PropertiesSerializer.Read(reader);
+
+ return new AuthenticationTicket(identity, properties);
+ }
+
+ protected virtual ClaimsIdentity ReadIdentity(BinaryReader reader)
+ {
+ var authenticationType = reader.ReadString();
+ var nameClaimType = ReadWithDefault(reader, ClaimsIdentity.DefaultNameClaimType);
+ var roleClaimType = ReadWithDefault(reader, ClaimsIdentity.DefaultRoleClaimType);
+
+ // Read the number of claims contained
+ // in the serialized identity.
+ var count = reader.ReadInt32();
+
+ var identity = new ClaimsIdentity(authenticationType, nameClaimType, roleClaimType);
+
+ for (int index = 0; index != count; ++index)
+ {
+ var claim = ReadClaim(reader, identity);
+
+ identity.AddClaim(claim);
+ }
+
+ // Determine whether the identity
+ // has a bootstrap context attached.
+ if (reader.ReadBoolean())
+ {
+ identity.BootstrapContext = reader.ReadString();
+ }
+
+ // Determine whether the identity
+ // has an actor identity attached.
+ if (reader.ReadBoolean())
+ {
+ identity.Actor = ReadIdentity(reader);
+ }
+
+ return identity;
+ }
+
+ protected virtual Claim ReadClaim(BinaryReader reader, ClaimsIdentity identity)
+ {
+ var type = ReadWithDefault(reader, identity.NameClaimType);
+ var value = reader.ReadString();
+ var valueType = ReadWithDefault(reader, ClaimValueTypes.String);
+ var issuer = ReadWithDefault(reader, ClaimsIdentity.DefaultIssuer);
+ var originalIssuer = ReadWithDefault(reader, issuer);
+
+ var claim = new Claim(type, value, valueType, issuer, originalIssuer, identity);
+
+ // Read the number of properties stored in the claim.
+ var count = reader.ReadInt32();
+
+ for (var index = 0; index != count; ++index)
+ {
+ var key = reader.ReadString();
+ var propertyValue = reader.ReadString();
+
+ claim.Properties.Add(key, propertyValue);
+ }
+
+ return claim;
+ }
+
+ private static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue)
+ {
+ if (string.Equals(value, defaultValue, StringComparison.Ordinal))
+ {
+ writer.Write(DefaultStringPlaceholder);
+ }
+ else
+ {
+ writer.Write(value);
+ }
+ }
+
+ private static string ReadWithDefault(BinaryReader reader, string defaultValue)
+ {
+ var value = reader.ReadString();
+ if (string.Equals(value, DefaultStringPlaceholder, StringComparison.Ordinal))
+ {
+ return defaultValue;
+ }
+ return value;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/ChunkingCookieManager.cs b/src/Security/src/Microsoft.Owin.Security.Interop/ChunkingCookieManager.cs
new file mode 100644
index 0000000000..1ae4f00cdb
--- /dev/null
+++ b/src/Security/src/Microsoft.Owin.Security.Interop/ChunkingCookieManager.cs
@@ -0,0 +1,280 @@
+// 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.Globalization;
+using System.Linq;
+using Microsoft.Owin.Infrastructure;
+
+namespace Microsoft.Owin.Security.Interop
+{
+ // This MUST be kept in sync with Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager
+ /// <summary>
+ /// This handles cookies that are limited by per cookie length. It breaks down long cookies for responses, and reassembles them
+ /// from requests.
+ /// </summary>
+ public class ChunkingCookieManager : ICookieManager
+ {
+ private const string ChunkKeySuffix = "C";
+ private const string ChunkCountPrefix = "chunks-";
+
+ public ChunkingCookieManager()
+ {
+ // Lowest common denominator. Safari has the lowest known limit (4093), and we leave little extra just in case.
+ // See http://browsercookielimits.x64.me/.
+ // Leave at least 20 in case CookiePolicy tries to add 'secure' and/or 'httponly'.
+ ChunkSize = 4070;
+ }
+
+ /// <summary>
+ /// The maximum size of cookie to send back to the client. If a cookie exceeds this size it will be broken down into multiple
+ /// cookies. Set this value to null to disable this behavior. The default is 4090 characters, which is supported by all
+ /// common browsers.
+ ///
+ /// Note that browsers may also have limits on the total size of all cookies per domain, and on the number of cookies per domain.
+ /// </summary>
+ public int? ChunkSize { get; set; }
+
+ /// <summary>
+ /// Throw if not all chunks of a cookie are available on a request for re-assembly.
+ /// </summary>
+ public bool ThrowForPartialCookies { get; set; }
+
+ // Parse the "chunks-XX" to determine how many chunks there should be.
+ private static int ParseChunksCount(string value)
+ {
+ if (value != null && value.StartsWith(ChunkCountPrefix, StringComparison.Ordinal))
+ {
+ var chunksCountString = value.Substring(ChunkCountPrefix.Length);
+ int chunksCount;
+ if (int.TryParse(chunksCountString, NumberStyles.None, CultureInfo.InvariantCulture, out chunksCount))
+ {
+ return chunksCount;
+ }
+ }
+ return 0;
+ }
+
+ /// <summary>
+ /// Get the reassembled cookie. Non chunked cookies are returned normally.
+ /// Cookies with missing chunks just have their "chunks-XX" header returned.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="key"></param>
+ /// <returns>The reassembled cookie, if any, or null.</returns>
+ public string GetRequestCookie(IOwinContext context, string key)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ var requestCookies = context.Request.Cookies;
+ var value = requestCookies[key];
+ var chunksCount = ParseChunksCount(value);
+ if (chunksCount > 0)
+ {
+ var chunks = new string[chunksCount];
+ for (var chunkId = 1; chunkId <= chunksCount; chunkId++)
+ {
+ var chunk = requestCookies[key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture)];
+ if (string.IsNullOrEmpty(chunk))
+ {
+ if (ThrowForPartialCookies)
+ {
+ var totalSize = 0;
+ for (int i = 0; i < chunkId - 1; i++)
+ {
+ totalSize += chunks[i].Length;
+ }
+ throw new FormatException(
+ string.Format(CultureInfo.CurrentCulture,
+ "The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded.",
+ chunkId - 1, chunksCount, totalSize));
+ }
+ // Missing chunk, abort by returning the original cookie value. It may have been a false positive?
+ return value;
+ }
+
+ chunks[chunkId - 1] = chunk;
+ }
+
+ return string.Join(string.Empty, chunks);
+ }
+ return value;
+ }
+
+ /// <summary>
+ /// Appends a new response cookie to the Set-Cookie header. If the cookie is larger than the given size limit
+ /// then it will be broken down into multiple cookies as follows:
+ /// Set-Cookie: CookieName=chunks-3; path=/
+ /// Set-Cookie: CookieNameC1=Segment1; path=/
+ /// Set-Cookie: CookieNameC2=Segment2; path=/
+ /// Set-Cookie: CookieNameC3=Segment3; path=/
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="key"></param>
+ /// <param name="value"></param>
+ /// <param name="options"></param>
+ public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ var domainHasValue = !string.IsNullOrEmpty(options.Domain);
+ var pathHasValue = !string.IsNullOrEmpty(options.Path);
+ var expiresHasValue = options.Expires.HasValue;
+
+ var templateLength = key.Length + "=".Length
+ + (domainHasValue ? "; domain=".Length + options.Domain.Length : 0)
+ + (pathHasValue ? "; path=".Length + options.Path.Length : 0)
+ + (expiresHasValue ? "; expires=ddd, dd-MMM-yyyy HH:mm:ss GMT".Length : 0)
+ + (options.Secure ? "; secure".Length : 0)
+ + (options.HttpOnly ? "; HttpOnly".Length : 0);
+
+ // Normal cookie
+ var responseCookies = context.Response.Cookies;
+ if (!ChunkSize.HasValue || ChunkSize.Value > templateLength + value.Length)
+ {
+ responseCookies.Append(key, value, options);
+ }
+ else if (ChunkSize.Value < templateLength + 10)
+ {
+ // 10 is the minimum data we want to put in an individual cookie, including the cookie chunk identifier "CXX".
+ // No room for data, we can't chunk the options and name
+ throw new InvalidOperationException("The cookie key and options are larger than ChunksSize, leaving no room for data.");
+ }
+ else
+ {
+ // Break the cookie down into multiple cookies.
+ // Key = CookieName, value = "Segment1Segment2Segment2"
+ // Set-Cookie: CookieName=chunks-3; path=/
+ // Set-Cookie: CookieNameC1="Segment1"; path=/
+ // Set-Cookie: CookieNameC2="Segment2"; path=/
+ // Set-Cookie: CookieNameC3="Segment3"; path=/
+ var dataSizePerCookie = ChunkSize.Value - templateLength - 3; // Budget 3 chars for the chunkid.
+ var cookieChunkCount = (int)Math.Ceiling(value.Length * 1.0 / dataSizePerCookie);
+
+ responseCookies.Append(key, ChunkCountPrefix + cookieChunkCount.ToString(CultureInfo.InvariantCulture), options);
+
+ var offset = 0;
+ for (var chunkId = 1; chunkId <= cookieChunkCount; chunkId++)
+ {
+ var remainingLength = value.Length - offset;
+ var length = Math.Min(dataSizePerCookie, remainingLength);
+ var segment = value.Substring(offset, length);
+ offset += length;
+
+ responseCookies.Append(key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture), segment, options);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Deletes the cookie with the given key by setting an expired state. If a matching chunked cookie exists on
+ /// the request, delete each chunk.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="key"></param>
+ /// <param name="options"></param>
+ public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ var keys = new List<string>();
+ keys.Add(key + "=");
+
+ var requestCookie = context.Request.Cookies[key];
+ var chunks = ParseChunksCount(requestCookie);
+ if (chunks > 0)
+ {
+ for (int i = 1; i <= chunks + 1; i++)
+ {
+ var subkey = key + ChunkKeySuffix + i.ToString(CultureInfo.InvariantCulture);
+ keys.Add(subkey + "=");
+ }
+ }
+
+ var domainHasValue = !string.IsNullOrEmpty(options.Domain);
+ var pathHasValue = !string.IsNullOrEmpty(options.Path);
+
+ Func<string, bool> rejectPredicate;
+ Func<string, bool> predicate = value => keys.Any(k => value.StartsWith(k, StringComparison.OrdinalIgnoreCase));
+ if (domainHasValue)
+ {
+ rejectPredicate = value => predicate(value) && value.IndexOf("domain=" + options.Domain, StringComparison.OrdinalIgnoreCase) != -1;
+ }
+ else if (pathHasValue)
+ {
+ rejectPredicate = value => predicate(value) && value.IndexOf("path=" + options.Path, StringComparison.OrdinalIgnoreCase) != -1;
+ }
+ else
+ {
+ rejectPredicate = value => predicate(value);
+ }
+
+ var responseHeaders = context.Response.Headers;
+ string[] existingValues;
+ if (responseHeaders.TryGetValue(Constants.Headers.SetCookie, out existingValues) && existingValues != null)
+ {
+ responseHeaders.SetValues(Constants.Headers.SetCookie, existingValues.Where(value => !rejectPredicate(value)).ToArray());
+ }
+
+ AppendResponseCookie(
+ context,
+ key,
+ string.Empty,
+ new CookieOptions()
+ {
+ Path = options.Path,
+ Domain = options.Domain,
+ Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ });
+
+ for (int i = 1; i <= chunks; i++)
+ {
+ AppendResponseCookie(
+ context,
+ key + "C" + i.ToString(CultureInfo.InvariantCulture),
+ string.Empty,
+ new CookieOptions()
+ {
+ Path = options.Path,
+ Domain = options.Domain,
+ Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ });
+ }
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/Constants.cs b/src/Security/src/Microsoft.Owin.Security.Interop/Constants.cs
new file mode 100644
index 0000000000..1e75761b70
--- /dev/null
+++ b/src/Security/src/Microsoft.Owin.Security.Interop/Constants.cs
@@ -0,0 +1,13 @@
+// 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.Owin.Security.Interop
+{
+ internal static class Constants
+ {
+ internal static class Headers
+ {
+ internal const string SetCookie = "Set-Cookie";
+ }
+ }
+}
diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/DataProtectorShim.cs b/src/Security/src/Microsoft.Owin.Security.Interop/DataProtectorShim.cs
new file mode 100644
index 0000000000..7313588948
--- /dev/null
+++ b/src/Security/src/Microsoft.Owin.Security.Interop/DataProtectorShim.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Open Technologies, Inc. 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;
+
+namespace Microsoft.Owin.Security.Interop
+{
+ /// <summary>
+ /// Converts an <see cref="IDataProtector"/> to an
+ /// <see cref="Microsoft.Owin.Security.DataProtection.IDataProtector"/>.
+ /// </summary>
+ public sealed class DataProtectorShim : Microsoft.Owin.Security.DataProtection.IDataProtector
+ {
+ private readonly IDataProtector _protector;
+
+ public DataProtectorShim(IDataProtector protector)
+ {
+ _protector = protector;
+ }
+
+ public byte[] Protect(byte[] userData)
+ {
+ return _protector.Protect(userData);
+ }
+
+ public byte[] Unprotect(byte[] protectedData)
+ {
+ return _protector.Unprotect(protectedData);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/Microsoft.Owin.Security.Interop.csproj b/src/Security/src/Microsoft.Owin.Security.Interop/Microsoft.Owin.Security.Interop.csproj
new file mode 100644
index 0000000000..a12bc65637
--- /dev/null
+++ b/src/Security/src/Microsoft.Owin.Security.Interop/Microsoft.Owin.Security.Interop.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>A compatibility layer for sharing authentication tickets between Microsoft.Owin.Security and Microsoft.AspNetCore.Authentication.</Description>
+ <TargetFramework>net461</TargetFramework>
+ <NoWarn>$(NoWarn);CS1591</NoWarn>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageTags>aspnetcore;katana;owin;security</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="$(MicrosoftAspNetCoreDataProtectionExtensionsPackageVersion)" />
+ <PackageReference Include="Microsoft.Owin.Security" Version="$(MicrosoftOwinSecurityPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/Properties/AssemblyInfo.cs b/src/Security/src/Microsoft.Owin.Security.Interop/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..490fa7cb2a
--- /dev/null
+++ b/src/Security/src/Microsoft.Owin.Security.Interop/Properties/AssemblyInfo.cs
@@ -0,0 +1,8 @@
+// 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.InteropServices;
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("a7922dd8-09f1-43e4-938b-cc523ea08898")]
+
diff --git a/src/Security/src/Microsoft.Owin.Security.Interop/baseline.netframework.json b/src/Security/src/Microsoft.Owin.Security.Interop/baseline.netframework.json
new file mode 100644
index 0000000000..bfc0c0076d
--- /dev/null
+++ b/src/Security/src/Microsoft.Owin.Security.Interop/baseline.netframework.json
@@ -0,0 +1,372 @@
+{
+ "AssemblyIdentity": "Microsoft.Owin.Security.Interop, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Owin.Security.Interop.AspNetTicketDataFormat",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.Owin.Security.DataHandler.SecureDataFormat<Microsoft.Owin.Security.AuthenticationTicket>",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "protector",
+ "Type": "Microsoft.Owin.Security.DataProtection.IDataProtector"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Owin.Security.Interop.AspNetTicketSerializer",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.Owin.Security.DataHandler.Serializer.IDataSerializer<Microsoft.Owin.Security.AuthenticationTicket>"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Default",
+ "Parameters": [],
+ "ReturnType": "Microsoft.Owin.Security.Interop.AspNetTicketSerializer",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Serialize",
+ "Parameters": [
+ {
+ "Name": "ticket",
+ "Type": "Microsoft.Owin.Security.AuthenticationTicket"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Owin.Security.DataHandler.Serializer.IDataSerializer<Microsoft.Owin.Security.AuthenticationTicket>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Deserialize",
+ "Parameters": [
+ {
+ "Name": "data",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "Microsoft.Owin.Security.AuthenticationTicket",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Owin.Security.DataHandler.Serializer.IDataSerializer<Microsoft.Owin.Security.AuthenticationTicket>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Write",
+ "Parameters": [
+ {
+ "Name": "writer",
+ "Type": "System.IO.BinaryWriter"
+ },
+ {
+ "Name": "ticket",
+ "Type": "Microsoft.Owin.Security.AuthenticationTicket"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "WriteIdentity",
+ "Parameters": [
+ {
+ "Name": "writer",
+ "Type": "System.IO.BinaryWriter"
+ },
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "WriteClaim",
+ "Parameters": [
+ {
+ "Name": "writer",
+ "Type": "System.IO.BinaryWriter"
+ },
+ {
+ "Name": "claim",
+ "Type": "System.Security.Claims.Claim"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Read",
+ "Parameters": [
+ {
+ "Name": "reader",
+ "Type": "System.IO.BinaryReader"
+ }
+ ],
+ "ReturnType": "Microsoft.Owin.Security.AuthenticationTicket",
+ "Virtual": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ReadIdentity",
+ "Parameters": [
+ {
+ "Name": "reader",
+ "Type": "System.IO.BinaryReader"
+ }
+ ],
+ "ReturnType": "System.Security.Claims.ClaimsIdentity",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ReadClaim",
+ "Parameters": [
+ {
+ "Name": "reader",
+ "Type": "System.IO.BinaryReader"
+ },
+ {
+ "Name": "identity",
+ "Type": "System.Security.Claims.ClaimsIdentity"
+ }
+ ],
+ "ReturnType": "System.Security.Claims.Claim",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Owin.Security.Interop.ChunkingCookieManager",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.Owin.Infrastructure.ICookieManager"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ChunkSize",
+ "Parameters": [],
+ "ReturnType": "System.Nullable<System.Int32>",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ChunkSize",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Nullable<System.Int32>"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ThrowForPartialCookies",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ThrowForPartialCookies",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetRequestCookie",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.Owin.IOwinContext"
+ },
+ {
+ "Name": "key",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Owin.Infrastructure.ICookieManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AppendResponseCookie",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.Owin.IOwinContext"
+ },
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.String"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.Owin.CookieOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Owin.Infrastructure.ICookieManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "DeleteCookie",
+ "Parameters": [
+ {
+ "Name": "context",
+ "Type": "Microsoft.Owin.IOwinContext"
+ },
+ {
+ "Name": "key",
+ "Type": "System.String"
+ },
+ {
+ "Name": "options",
+ "Type": "Microsoft.Owin.CookieOptions"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Owin.Infrastructure.ICookieManager",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Owin.Security.Interop.DataProtectorShim",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Sealed": true,
+ "ImplementedInterfaces": [
+ "Microsoft.Owin.Security.DataProtection.IDataProtector"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Protect",
+ "Parameters": [
+ {
+ "Name": "userData",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Owin.Security.DataProtection.IDataProtector",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Unprotect",
+ "Parameters": [
+ {
+ "Name": "protectedData",
+ "Type": "System.Byte[]"
+ }
+ ],
+ "ReturnType": "System.Byte[]",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Owin.Security.DataProtection.IDataProtector",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "protector",
+ "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/test/Directory.Build.props b/src/Security/test/Directory.Build.props
new file mode 100644
index 0000000000..b842a48317
--- /dev/null
+++ b/src/Security/test/Directory.Build.props
@@ -0,0 +1,19 @@
+<Project>
+ <Import Project="..\Directory.Build.props" />
+
+ <PropertyGroup>
+ <DeveloperBuildTestTfms>netcoreapp2.1</DeveloperBuildTestTfms>
+ <StandardTestTfms>$(DeveloperBuildTestTfms)</StandardTestTfms>
+ <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' ">$(StandardTestTfms);netcoreapp2.0</StandardTestTfms>
+ <StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' AND '$(OS)' == 'Windows_NT' ">$(StandardTestTfms);net461</StandardTestTfms>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
+ <PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
+ <PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
+ <PackageReference Include="xunit.analyzers" Version="$(XunitAnalyzersPackageVersion)" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
+ </ItemGroup>
+</Project>
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/AuthenticationMiddlewareTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/AuthenticationMiddlewareTests.cs
new file mode 100644
index 0000000000..b09f13cab9
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/AuthenticationMiddlewareTests.cs
@@ -0,0 +1,196 @@
+// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class AuthenticationMiddlewareTests
+ {
+ [Fact]
+ public async Task OnlyInvokesCanHandleRequestHandlers()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ })
+ .ConfigureServices(services => services.AddAuthentication(o =>
+ {
+ o.AddScheme("Skip", s =>
+ {
+ s.HandlerType = typeof(SkipHandler);
+ });
+ // Won't get hit since CanHandleRequests is false
+ o.AddScheme("throws", s =>
+ {
+ s.HandlerType = typeof(ThrowsHandler);
+ });
+ o.AddScheme("607", s =>
+ {
+ s.HandlerType = typeof(SixOhSevenHandler);
+ });
+ // Won't get run since 607 will finish
+ o.AddScheme("305", s =>
+ {
+ s.HandlerType = typeof(ThreeOhFiveHandler);
+ });
+ }));
+ var server = new TestServer(builder);
+ var response = await server.CreateClient().GetAsync("http://example.com/");
+ Assert.Equal(607, (int)response.StatusCode);
+ }
+
+ private class ThreeOhFiveHandler : StatusCodeHandler {
+ public ThreeOhFiveHandler() : base(305) { }
+ }
+
+ private class SixOhSevenHandler : StatusCodeHandler
+ {
+ public SixOhSevenHandler() : base(607) { }
+ }
+
+ private class SevenOhSevenHandler : StatusCodeHandler
+ {
+ public SevenOhSevenHandler() : base(707) { }
+ }
+
+ private class StatusCodeHandler : IAuthenticationRequestHandler
+ {
+ private HttpContext _context;
+ private int _code;
+
+ public StatusCodeHandler(int code)
+ {
+ _code = code;
+ }
+
+ public Task<AuthenticateResult> AuthenticateAsync()
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task ChallengeAsync(AuthenticationProperties properties)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task ForbidAsync(AuthenticationProperties properties)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task<bool> HandleRequestAsync()
+ {
+ _context.Response.StatusCode = _code;
+ return Task.FromResult(true);
+ }
+
+ public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
+ {
+ _context = context;
+ return Task.FromResult(0);
+ }
+
+ public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task SignOutAsync(AuthenticationProperties properties)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class ThrowsHandler : IAuthenticationHandler
+ {
+ private HttpContext _context;
+
+ public Task<AuthenticateResult> AuthenticateAsync()
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task ChallengeAsync(AuthenticationProperties properties)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task ForbidAsync(AuthenticationProperties properties)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task<bool> HandleRequestAsync()
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
+ {
+ _context = context;
+ return Task.FromResult(0);
+ }
+
+ public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task SignOutAsync(AuthenticationProperties properties)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class SkipHandler : IAuthenticationRequestHandler
+ {
+ private HttpContext _context;
+
+ public Task<AuthenticateResult> AuthenticateAsync()
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task ChallengeAsync(AuthenticationProperties properties)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task ForbidAsync(AuthenticationProperties properties)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task<bool> HandleRequestAsync()
+ {
+ return Task.FromResult(false);
+ }
+
+ public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
+ {
+ _context = context;
+ return Task.FromResult(0);
+ }
+
+ public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task SignOutAsync(AuthenticationProperties properties)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Base64UrlTextEncoderTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Base64UrlTextEncoderTests.cs
new file mode 100644
index 0000000000..3195298c0d
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Base64UrlTextEncoderTests.cs
@@ -0,0 +1,30 @@
+// 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.Authentication
+{
+ public class Base64UrlTextEncoderTests
+ {
+ [Fact]
+ public void DataOfVariousLengthRoundTripCorrectly()
+ {
+ for (int length = 0; length != 256; ++length)
+ {
+ var data = new byte[length];
+ for (int index = 0; index != length; ++index)
+ {
+ data[index] = (byte)(5 + length + (index * 23));
+ }
+ string text = Base64UrlTextEncoder.Encode(data);
+ byte[] result = Base64UrlTextEncoder.Decode(text);
+
+ for (int index = 0; index != length; ++index)
+ {
+ Assert.Equal(data[index], result[index]);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs
new file mode 100644
index 0000000000..b083e9d76d
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs
@@ -0,0 +1,112 @@
+// 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 System.Linq;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+using Microsoft.AspNetCore.Authentication.OAuth.Claims;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class ClaimActionTests
+ {
+ [Fact]
+ public void CanMapSingleValueUserDataToClaim()
+ {
+ var userData = new JObject
+ {
+ ["name"] = "test"
+ };
+
+ var identity = new ClaimsIdentity();
+
+ var action = new JsonKeyClaimAction("name", "name", "name");
+ action.Run(userData, identity, "iss");
+
+ Assert.Equal("name", identity.FindFirst("name").Type);
+ Assert.Equal("test", identity.FindFirst("name").Value);
+ }
+
+ [Fact]
+ public void CanMapArrayValueUserDataToClaims()
+ {
+ var userData = new JObject
+ {
+ ["role"] = new JArray { "role1", "role2" }
+ };
+
+ var identity = new ClaimsIdentity();
+
+ var action = new JsonKeyClaimAction("role", "role", "role");
+ action.Run(userData, identity, "iss");
+
+ var roleClaims = identity.FindAll("role").ToList();
+ Assert.Equal(2, roleClaims.Count);
+ Assert.Equal("role", roleClaims[0].Type);
+ Assert.Equal("role1", roleClaims[0].Value);
+ Assert.Equal("role", roleClaims[1].Type);
+ Assert.Equal("role2", roleClaims[1].Value);
+ }
+
+ [Fact]
+ public void MapAllSucceeds()
+ {
+ var userData = new JObject
+ {
+ ["name0"] = "value0",
+ ["name1"] = "value1",
+ };
+
+ var identity = new ClaimsIdentity();
+ var action = new MapAllClaimsAction();
+ action.Run(userData, identity, "iss");
+
+ Assert.Equal("name0", identity.FindFirst("name0").Type);
+ Assert.Equal("value0", identity.FindFirst("name0").Value);
+ Assert.Equal("name1", identity.FindFirst("name1").Type);
+ Assert.Equal("value1", identity.FindFirst("name1").Value);
+ }
+
+ [Fact]
+ public void MapAllAllowesDulicateKeysWithUniqueValues()
+ {
+ var userData = new JObject
+ {
+ ["name0"] = "value0",
+ ["name1"] = "value1",
+ };
+
+ var identity = new ClaimsIdentity();
+ identity.AddClaim(new Claim("name0", "value2"));
+ identity.AddClaim(new Claim("name1", "value3"));
+ var action = new MapAllClaimsAction();
+ action.Run(userData, identity, "iss");
+
+ Assert.Equal(2, identity.FindAll("name0").Count());
+ Assert.Equal(2, identity.FindAll("name1").Count());
+ }
+
+ [Fact]
+ public void MapAllSkipsDuplicateValues()
+ {
+ var userData = new JObject
+ {
+ ["name0"] = "value0",
+ ["name1"] = "value1",
+ };
+
+ var identity = new ClaimsIdentity();
+ identity.AddClaim(new Claim("name0", "value0"));
+ identity.AddClaim(new Claim("name1", "value1"));
+ var action = new MapAllClaimsAction();
+ action.Run(userData, identity, "iss");
+
+ Assert.Single(identity.FindAll("name0"));
+ Assert.Single(identity.FindAll("name1"));
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/CookieTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/CookieTests.cs
new file mode 100644
index 0000000000..766d1e2e53
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/CookieTests.cs
@@ -0,0 +1,1989 @@
+// 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.Net;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Security.Principal;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Authentication.Tests;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.Cookies
+{
+ public class CookieTests
+ {
+ private TestClock _clock = new TestClock();
+
+ [Fact]
+ public async Task CanForwardDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ })
+ .AddCookie(o => o.ForwardDefault = "auth1");
+
+ var forwardDefault = new TestHandler();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await context.SignOutAsync();
+ Assert.Equal(1, forwardDefault.SignOutCount);
+
+ await context.SignInAsync(new ClaimsPrincipal());
+ Assert.Equal(1, forwardDefault.SignInCount);
+ }
+
+ [Fact]
+ public async Task ForwardSignInWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddCookie(o =>
+ {
+ o.ForwardDefault = "auth1";
+ o.ForwardSignIn = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.SignInAsync(new ClaimsPrincipal());
+ Assert.Equal(1, specific.SignInCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignOutCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardSignOutWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddCookie(o =>
+ {
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.SignOutAsync();
+ Assert.Equal(1, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardForbidWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddCookie(o =>
+ {
+ o.ForwardDefault = "auth1";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ForbidAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(1, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardAuthenticateWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddCookie(o =>
+ {
+ o.ForwardDefault = "auth1";
+ o.ForwardAuthenticate = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(1, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardChallengeWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler>("specific", "specific");
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ })
+ .AddCookie(o =>
+ {
+ o.ForwardDefault = "auth1";
+ o.ForwardChallenge = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ChallengeAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(1, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardSelectorWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddCookie(o =>
+ {
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, selector.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, selector.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, selector.ChallengeCount);
+
+ await context.SignOutAsync();
+ Assert.Equal(1, selector.SignOutCount);
+
+ await context.SignInAsync(new ClaimsPrincipal());
+ Assert.Equal(1, selector.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task NullForwardSelectorUsesDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddCookie(o =>
+ {
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => null;
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await context.SignOutAsync();
+ Assert.Equal(1, forwardDefault.SignOutCount);
+
+ await context.SignInAsync(new ClaimsPrincipal());
+ Assert.Equal(1, forwardDefault.SignInCount);
+
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task SpecificForwardWinsOverSelectorAndDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddCookie(o =>
+ {
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ o.ForwardAuthenticate = "specific";
+ o.ForwardChallenge = "specific";
+ o.ForwardSignIn = "specific";
+ o.ForwardSignOut = "specific";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, specific.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, specific.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, specific.ChallengeCount);
+
+ await context.SignOutAsync();
+ Assert.Equal(1, specific.SignOutCount);
+
+ await context.SignInAsync(new ClaimsPrincipal());
+ Assert.Equal(1, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ }
+
+ [Fact]
+ public async Task VerifySchemeDefaults()
+ {
+ var services = new ServiceCollection();
+ services.AddAuthentication().AddCookie();
+ var sp = services.BuildServiceProvider();
+ var schemeProvider = sp.GetRequiredService<IAuthenticationSchemeProvider>();
+ var scheme = await schemeProvider.GetSchemeAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ Assert.NotNull(scheme);
+ Assert.Equal("CookieAuthenticationHandler", scheme.HandlerType.Name);
+ Assert.Null(scheme.DisplayName);
+ }
+
+ [Fact]
+ public async Task NormalRequestPassesThrough()
+ {
+ var server = CreateServer(s => { });
+ var response = await server.CreateClient().GetAsync("http://example.com/normal");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task AjaxLoginRedirectToReturnUrlTurnsInto200WithLocationHeader()
+ {
+ var server = CreateServer(o => o.LoginPath = "/login");
+ var transaction = await SendAsync(server, "http://example.com/challenge?X-Requested-With=XMLHttpRequest");
+ Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode);
+ var responded = transaction.Response.Headers.GetValues("Location");
+ Assert.Single(responded);
+ Assert.StartsWith("http://example.com/login", responded.Single());
+ }
+
+ [Fact]
+ public async Task AjaxForbidTurnsInto403WithLocationHeader()
+ {
+ var server = CreateServer(o => o.AccessDeniedPath = "/denied");
+ var transaction = await SendAsync(server, "http://example.com/forbid?X-Requested-With=XMLHttpRequest");
+ Assert.Equal(HttpStatusCode.Forbidden, transaction.Response.StatusCode);
+ var responded = transaction.Response.Headers.GetValues("Location");
+ Assert.Single(responded);
+ Assert.StartsWith("http://example.com/denied", responded.Single());
+ }
+
+ [Fact]
+ public async Task AjaxLogoutRedirectToReturnUrlTurnsInto200WithLocationHeader()
+ {
+ var server = CreateServer(o => o.LogoutPath = "/signout");
+ var transaction = await SendAsync(server, "http://example.com/signout?X-Requested-With=XMLHttpRequest&ReturnUrl=/");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ var responded = transaction.Response.Headers.GetValues("Location");
+ Assert.Single(responded);
+ Assert.StartsWith("/", responded.Single());
+ }
+
+ [Fact]
+ public async Task AjaxChallengeRedirectTurnsInto200WithLocationHeader()
+ {
+ var server = CreateServer(s => { });
+ var transaction = await SendAsync(server, "http://example.com/challenge?X-Requested-With=XMLHttpRequest&ReturnUrl=/");
+ Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode);
+ var responded = transaction.Response.Headers.GetValues("Location");
+ Assert.Single(responded);
+ Assert.StartsWith("http://example.com/Account/Login", responded.Single());
+ }
+
+ [Fact]
+ public async Task ProtectedCustomRequestShouldRedirectToCustomRedirectUri()
+ {
+ var server = CreateServer(s => { });
+
+ var transaction = await SendAsync(server, "http://example.com/protected/CustomRedirect");
+
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var location = transaction.Response.Headers.Location;
+ Assert.Equal("http://example.com/Account/Login?ReturnUrl=%2FCustomRedirect", location.ToString());
+ }
+
+ private Task SignInAsAlice(HttpContext context)
+ {
+ var user = new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"));
+ user.AddClaim(new Claim("marker", "true"));
+ return context.SignInAsync("Cookies",
+ new ClaimsPrincipal(user),
+ new AuthenticationProperties());
+ }
+
+ private Task SignInAsWrong(HttpContext context)
+ {
+ return context.SignInAsync("Oops",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))),
+ new AuthenticationProperties());
+ }
+
+ private Task SignOutAsWrong(HttpContext context)
+ {
+ return context.SignOutAsync("Oops");
+ }
+
+ [Fact]
+ public async Task SignInCausesDefaultCookieToBeCreated()
+ {
+ var server = CreateServerWithServices(s => s.AddAuthentication().AddCookie(o =>
+ {
+ o.LoginPath = new PathString("/login");
+ o.Cookie.Name = "TestCookie";
+ }), SignInAsAlice);
+
+ var transaction = await SendAsync(server, "http://example.com/testpath");
+
+ var setCookie = transaction.SetCookie;
+ Assert.StartsWith("TestCookie=", setCookie);
+ Assert.Contains("; path=/", setCookie);
+ Assert.Contains("; httponly", setCookie);
+ Assert.Contains("; samesite=", setCookie);
+ Assert.DoesNotContain("; expires=", setCookie);
+ Assert.DoesNotContain("; domain=", setCookie);
+ Assert.DoesNotContain("; secure", setCookie);
+ }
+
+ [Fact]
+ public async Task CookieExpirationOptionIsIgnored()
+ {
+ var server = CreateServerWithServices(s => s.AddAuthentication().AddCookie(o =>
+ {
+ o.Cookie.Name = "TestCookie";
+ // this is currently ignored. Users should set o.ExpireTimeSpan instead
+ o.Cookie.Expiration = TimeSpan.FromDays(10);
+ }), SignInAsAlice);
+
+ var transaction = await SendAsync(server, "http://example.com/testpath");
+
+ var setCookie = transaction.SetCookie;
+ Assert.StartsWith("TestCookie=", setCookie);
+ Assert.DoesNotContain("; expires=", setCookie);
+ }
+
+ [Fact]
+ public async Task SignInWrongAuthTypeThrows()
+ {
+ var server = CreateServer(o =>
+ {
+ o.LoginPath = new PathString("/login");
+ o.Cookie.Name = "TestCookie";
+ }, SignInAsWrong);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(async () => await SendAsync(server, "http://example.com/testpath"));
+ }
+
+ [Fact]
+ public async Task SignOutWrongAuthTypeThrows()
+ {
+ var server = CreateServer(o =>
+ {
+ o.LoginPath = new PathString("/login");
+ o.Cookie.Name = "TestCookie";
+ }, SignOutAsWrong);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(async () => await SendAsync(server, "http://example.com/testpath"));
+ }
+
+ [Theory]
+ [InlineData(CookieSecurePolicy.Always, "http://example.com/testpath", true)]
+ [InlineData(CookieSecurePolicy.Always, "https://example.com/testpath", true)]
+ [InlineData(CookieSecurePolicy.None, "http://example.com/testpath", false)]
+ [InlineData(CookieSecurePolicy.None, "https://example.com/testpath", false)]
+ [InlineData(CookieSecurePolicy.SameAsRequest, "http://example.com/testpath", false)]
+ [InlineData(CookieSecurePolicy.SameAsRequest, "https://example.com/testpath", true)]
+ public async Task SecureSignInCausesSecureOnlyCookieByDefault(
+ CookieSecurePolicy cookieSecurePolicy,
+ string requestUri,
+ bool shouldBeSecureOnly)
+ {
+ var server = CreateServer(o =>
+ {
+ o.LoginPath = new PathString("/login");
+ o.Cookie.Name = "TestCookie";
+ o.Cookie.SecurePolicy = cookieSecurePolicy;
+ }, SignInAsAlice);
+
+ var transaction = await SendAsync(server, requestUri);
+ var setCookie = transaction.SetCookie;
+
+ if (shouldBeSecureOnly)
+ {
+ Assert.Contains("; secure", setCookie);
+ }
+ else
+ {
+ Assert.DoesNotContain("; secure", setCookie);
+ }
+ }
+
+ [Fact]
+ public async Task CookieOptionsAlterSetCookieHeader()
+ {
+ var server1 = CreateServer(o =>
+ {
+ o.Cookie.Name = "TestCookie";
+ o.Cookie.Path = "/foo";
+ o.Cookie.Domain = "another.com";
+ o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
+ o.Cookie.SameSite = SameSiteMode.None;
+ o.Cookie.HttpOnly = true;
+ }, SignInAsAlice, baseAddress: new Uri("http://example.com/base"));
+
+ var transaction1 = await SendAsync(server1, "http://example.com/base/testpath");
+
+ var setCookie1 = transaction1.SetCookie;
+
+ Assert.Contains("TestCookie=", setCookie1);
+ Assert.Contains(" path=/foo", setCookie1);
+ Assert.Contains(" domain=another.com", setCookie1);
+ Assert.Contains(" secure", setCookie1);
+ Assert.DoesNotContain(" samesite", setCookie1);
+ Assert.Contains(" httponly", setCookie1);
+
+ var server2 = CreateServer(o =>
+ {
+ o.Cookie.Name = "SecondCookie";
+ o.Cookie.SecurePolicy = CookieSecurePolicy.None;
+ o.Cookie.SameSite = SameSiteMode.Strict;
+ o.Cookie.HttpOnly = false;
+ }, SignInAsAlice, baseAddress: new Uri("http://example.com/base"));
+
+ var transaction2 = await SendAsync(server2, "http://example.com/base/testpath");
+
+ var setCookie2 = transaction2.SetCookie;
+
+ Assert.Contains("SecondCookie=", setCookie2);
+ Assert.Contains(" path=/base", setCookie2);
+ Assert.Contains(" samesite=strict", setCookie2);
+ Assert.DoesNotContain(" domain=", setCookie2);
+ Assert.DoesNotContain(" secure", setCookie2);
+ Assert.DoesNotContain(" httponly", setCookie2);
+ }
+
+ [Fact]
+ public async Task CookieContainsIdentity()
+ {
+ var server = CreateServer(o => { }, SignInAsAlice);
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+ }
+
+ [Fact]
+ public async Task CookieAppliesClaimsTransform()
+ {
+ var server = CreateServer(o => { },
+ SignInAsAlice,
+ baseAddress: null,
+ claimsTransform: true);
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+ Assert.Equal("yup", FindClaimValue(transaction2, "xform"));
+ Assert.Null(FindClaimValue(transaction2, "sync"));
+ }
+
+ [Fact]
+ public async Task CookieStopsWorkingAfterExpiration()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.SlidingExpiration = false;
+ }, SignInAsAlice);
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+
+ _clock.Add(TimeSpan.FromMinutes(7));
+
+ var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+
+ _clock.Add(TimeSpan.FromMinutes(7));
+
+ var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+
+ Assert.Null(transaction2.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+ Assert.Null(transaction3.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));
+ Assert.Null(transaction4.SetCookie);
+ Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name));
+ }
+
+ [Fact]
+ public async Task CookieExpirationCanBeOverridenInSignin()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.SlidingExpiration = false;
+ },
+ context =>
+ context.SignInAsync("Cookies",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))),
+ new AuthenticationProperties() { ExpiresUtc = _clock.UtcNow.Add(TimeSpan.FromMinutes(5)) }));
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+
+ _clock.Add(TimeSpan.FromMinutes(3));
+
+ var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+
+ _clock.Add(TimeSpan.FromMinutes(3));
+
+ var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+
+ Assert.Null(transaction2.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+ Assert.Null(transaction3.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));
+ Assert.Null(transaction4.SetCookie);
+ Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name));
+ }
+
+ [Fact]
+ public async Task ExpiredCookieWithValidatorStillExpired()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.Events = new CookieAuthenticationEvents
+ {
+ OnValidatePrincipal = ctx =>
+ {
+ ctx.ShouldRenew = true;
+ return Task.FromResult(0);
+ }
+ };
+ },
+ context =>
+ context.SignInAsync("Cookies",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ _clock.Add(TimeSpan.FromMinutes(11));
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.Null(transaction2.SetCookie);
+ Assert.Null(FindClaimValue(transaction2, ClaimTypes.Name));
+ }
+
+ [Fact]
+ public async Task CookieCanBeRejectedAndSignedOutByValidator()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.SlidingExpiration = false;
+ o.Events = new CookieAuthenticationEvents
+ {
+ OnValidatePrincipal = ctx =>
+ {
+ ctx.RejectPrincipal();
+ ctx.HttpContext.SignOutAsync("Cookies");
+ return Task.FromResult(0);
+ }
+ };
+ },
+ context =>
+ context.SignInAsync("Cookies",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.Contains(".AspNetCore.Cookies=; expires=", transaction2.SetCookie);
+ Assert.Null(FindClaimValue(transaction2, ClaimTypes.Name));
+ }
+
+ [Fact]
+ public async Task CookieNotRenewedAfterSignOut()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.SlidingExpiration = false;
+ o.Events = new CookieAuthenticationEvents
+ {
+ OnValidatePrincipal = ctx =>
+ {
+ ctx.ShouldRenew = true;
+ return Task.FromResult(0);
+ }
+ };
+ },
+ context =>
+ context.SignInAsync("Cookies",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ // renews on every request
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.NotNull(transaction2.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+
+ var transaction3 = await server.SendAsync("http://example.com/normal", transaction1.CookieNameValue);
+ Assert.NotNull(transaction3.SetCookie[0]);
+
+ // signout wins over renew
+ var transaction4 = await server.SendAsync("http://example.com/signout", transaction3.SetCookie[0]);
+ Assert.Single(transaction4.SetCookie);
+ Assert.Contains(".AspNetCore.Cookies=; expires=", transaction4.SetCookie[0]);
+ }
+
+ [Fact]
+ public async Task CookieCanBeRenewedByValidator()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.SlidingExpiration = false;
+ o.Events = new CookieAuthenticationEvents
+ {
+ OnValidatePrincipal = ctx =>
+ {
+ ctx.ShouldRenew = true;
+ return Task.FromResult(0);
+ }
+ };
+ },
+ context =>
+ context.SignInAsync("Cookies",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.NotNull(transaction2.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(5));
+
+ var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
+ Assert.NotNull(transaction3.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(6));
+
+ var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.Null(transaction4.SetCookie);
+ Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(5));
+
+ var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
+ Assert.Null(transaction5.SetCookie);
+ Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name));
+ }
+
+ [Fact]
+ public async Task CookieCanBeReplacedByValidator()
+ {
+ var server = CreateServer(o =>
+ {
+ o.Events = new CookieAuthenticationEvents
+ {
+ OnValidatePrincipal = ctx =>
+ {
+ ctx.ShouldRenew = true;
+ ctx.ReplacePrincipal(new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice2", "Cookies2"))));
+ return Task.FromResult(0);
+ }
+ };
+ },
+ context =>
+ context.SignInAsync("Cookies",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.NotNull(transaction2.SetCookie);
+ Assert.Equal("Alice2", FindClaimValue(transaction2, ClaimTypes.Name));
+ }
+
+ [Fact]
+ public async Task CookieCanBeUpdatedByValidatorDuringRefresh()
+ {
+ var replace = false;
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.Events = new CookieAuthenticationEvents
+ {
+ OnValidatePrincipal = ctx =>
+ {
+ if (replace)
+ {
+ ctx.ShouldRenew = true;
+ ctx.ReplacePrincipal(new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice2", "Cookies2"))));
+ ctx.Properties.Items["updated"] = "yes";
+ }
+ return Task.FromResult(0);
+ }
+ };
+ },
+ context =>
+ context.SignInAsync("Cookies",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+
+ var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+ Assert.Null(FindPropertiesValue(transaction3, "updated"));
+
+ replace = true;
+
+ var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.NotNull(transaction4.SetCookie);
+ Assert.Equal("Alice2", FindClaimValue(transaction4, ClaimTypes.Name));
+ Assert.Equal("yes", FindPropertiesValue(transaction4, "updated"));
+
+ replace = false;
+
+ var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
+ Assert.Equal("Alice2", FindClaimValue(transaction5, ClaimTypes.Name));
+ Assert.Equal("yes", FindPropertiesValue(transaction4, "updated"));
+ }
+
+ [Fact]
+ public async Task CookieCanBeRenewedByValidatorWithSlidingExpiry()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.Events = new CookieAuthenticationEvents
+ {
+ OnValidatePrincipal = ctx =>
+ {
+ ctx.ShouldRenew = true;
+ return Task.FromResult(0);
+ }
+ };
+ },
+ context =>
+ context.SignInAsync("Cookies",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.NotNull(transaction2.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(5));
+
+ var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
+ Assert.NotNull(transaction3.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(6));
+
+ var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue);
+ Assert.NotNull(transaction4.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(11));
+
+ var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
+ Assert.Null(transaction5.SetCookie);
+ Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name));
+ }
+
+ [Fact]
+ public async Task CookieCanBeRenewedByValidatorWithModifiedProperties()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.Events = new CookieAuthenticationEvents
+ {
+ OnValidatePrincipal = ctx =>
+ {
+ ctx.ShouldRenew = true;
+ var id = ctx.Principal.Identities.First();
+ var claim = id.FindFirst("counter");
+ if (claim == null)
+ {
+ id.AddClaim(new Claim("counter", "1"));
+ }
+ else
+ {
+ id.RemoveClaim(claim);
+ id.AddClaim(new Claim("counter", claim.Value + "1"));
+ }
+ return Task.FromResult(0);
+ }
+ };
+ },
+ context =>
+ context.SignInAsync("Cookies",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.NotNull(transaction2.SetCookie);
+ Assert.Equal("1", FindClaimValue(transaction2, "counter"));
+
+ _clock.Add(TimeSpan.FromMinutes(5));
+
+ var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
+ Assert.NotNull(transaction3.SetCookie);
+ Assert.Equal("11", FindClaimValue(transaction3, "counter"));
+
+ _clock.Add(TimeSpan.FromMinutes(6));
+
+ var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue);
+ Assert.NotNull(transaction4.SetCookie);
+ Assert.Equal("111", FindClaimValue(transaction4, "counter"));
+
+ _clock.Add(TimeSpan.FromMinutes(11));
+
+ var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
+ Assert.Null(transaction5.SetCookie);
+ Assert.Null(FindClaimValue(transaction5, "counter"));
+ }
+
+ [Fact]
+ public async Task CookieValidatorOnlyCalledOnce()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.SlidingExpiration = false;
+ o.Events = new CookieAuthenticationEvents
+ {
+ OnValidatePrincipal = ctx =>
+ {
+ ctx.ShouldRenew = true;
+ return Task.FromResult(0);
+ }
+ };
+ },
+ context =>
+ context.SignInAsync("Cookies",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.NotNull(transaction2.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(5));
+
+ var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
+ Assert.NotNull(transaction3.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(6));
+
+ var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.Null(transaction4.SetCookie);
+ Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(5));
+
+ var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
+ Assert.Null(transaction5.SetCookie);
+ Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name));
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ShouldRenewUpdatesIssuedExpiredUtc(bool sliding)
+ {
+ DateTimeOffset? lastValidateIssuedDate = null;
+ DateTimeOffset? lastExpiresDate = null;
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.SlidingExpiration = sliding;
+ o.Events = new CookieAuthenticationEvents
+ {
+ OnValidatePrincipal = ctx =>
+ {
+ lastValidateIssuedDate = ctx.Properties.IssuedUtc;
+ lastExpiresDate = ctx.Properties.ExpiresUtc;
+ ctx.ShouldRenew = true;
+ return Task.FromResult(0);
+ }
+ };
+ },
+ context =>
+ context.SignInAsync("Cookies",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.NotNull(transaction2.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+
+ Assert.NotNull(lastValidateIssuedDate);
+ Assert.NotNull(lastExpiresDate);
+
+ var firstIssueDate = lastValidateIssuedDate;
+ var firstExpiresDate = lastExpiresDate;
+
+ _clock.Add(TimeSpan.FromMinutes(1));
+
+ var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
+ Assert.NotNull(transaction3.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(2));
+
+ var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue);
+ Assert.NotNull(transaction4.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name));
+
+ Assert.NotEqual(lastValidateIssuedDate, firstIssueDate);
+ Assert.NotEqual(firstExpiresDate, lastExpiresDate);
+ }
+
+ [Fact]
+ public async Task CookieExpirationCanBeOverridenInEvent()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.SlidingExpiration = false;
+ o.Events = new CookieAuthenticationEvents()
+ {
+ OnSigningIn = context =>
+ {
+ context.Properties.ExpiresUtc = _clock.UtcNow.Add(TimeSpan.FromMinutes(5));
+ return Task.FromResult(0);
+ }
+ };
+ },
+ SignInAsAlice);
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.Null(transaction2.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(3));
+
+ var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.Null(transaction3.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(3));
+
+ var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.Null(transaction4.SetCookie);
+ Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name));
+ }
+
+ [Fact]
+ public async Task CookieIsRenewedWithSlidingExpiration()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.SlidingExpiration = true;
+ },
+ SignInAsAlice);
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.Null(transaction2.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(4));
+
+ var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.Null(transaction3.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(4));
+
+ // transaction4 should arrive with a new SetCookie value
+ var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.NotNull(transaction4.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(4));
+
+ var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
+ Assert.Null(transaction5.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction5, ClaimTypes.Name));
+ }
+
+ [Fact]
+ public async Task CookieIsRenewedWithSlidingExpirationWithoutTransformations()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(10);
+ o.SlidingExpiration = true;
+ o.Events.OnValidatePrincipal = c =>
+ {
+ // https://github.com/aspnet/Security/issues/1607
+ // On sliding refresh the transformed principal should not be serialized into the cookie, only the original principal.
+ Assert.Single(c.Principal.Identities);
+ Assert.True(c.Principal.Identities.First().HasClaim("marker", "true"));
+ return Task.CompletedTask;
+ };
+ },
+ SignInAsAlice,
+ claimsTransform: true);
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.Null(transaction2.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(4));
+
+ var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.Null(transaction3.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(4));
+
+ // transaction4 should arrive with a new SetCookie value
+ var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
+ Assert.NotNull(transaction4.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name));
+
+ _clock.Add(TimeSpan.FromMinutes(4));
+
+ var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
+ Assert.Null(transaction5.SetCookie);
+ Assert.Equal("Alice", FindClaimValue(transaction5, ClaimTypes.Name));
+ }
+
+ [Fact]
+ public async Task CookieUsesPathBaseByDefault()
+ {
+ var server = CreateServer(o => { },
+ context =>
+ {
+ Assert.Equal(new PathString("/base"), context.Request.PathBase);
+ return context.SignInAsync("Cookies",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))));
+ },
+ new Uri("http://example.com/base"));
+
+ var transaction1 = await SendAsync(server, "http://example.com/base/testpath");
+ Assert.Contains("path=/base", transaction1.SetCookie);
+ }
+
+ [Fact]
+ public async Task CookieChallengeRedirectsToLoginWithoutCookie()
+ {
+ var server = CreateServer(o => { }, SignInAsAlice);
+
+ var url = "http://example.com/challenge";
+ var transaction = await SendAsync(server, url);
+
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var location = transaction.Response.Headers.Location;
+ Assert.Equal("/Account/Login", location.LocalPath);
+ }
+
+ [Fact]
+ public async Task CookieForbidRedirectsWithoutCookie()
+ {
+ var server = CreateServer(o => { }, SignInAsAlice);
+
+ var url = "http://example.com/forbid";
+ var transaction = await SendAsync(server, url);
+
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var location = transaction.Response.Headers.Location;
+ Assert.Equal("/Account/AccessDenied", location.LocalPath);
+ }
+
+ [Fact]
+ public async Task CookieChallengeRedirectsWithLoginPath()
+ {
+ var server = CreateServer(o =>
+ {
+ o.LoginPath = new PathString("/page");
+ });
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/challenge", transaction1.CookieNameValue);
+
+ Assert.Equal(HttpStatusCode.Redirect, transaction2.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task CookieChallengeWithUnauthorizedRedirectsToLoginIfNotAuthenticated()
+ {
+ var server = CreateServer(o =>
+ {
+ o.LoginPath = new PathString("/page");
+ });
+
+ var transaction1 = await SendAsync(server, "http://example.com/testpath");
+
+ var transaction2 = await SendAsync(server, "http://example.com/unauthorized", transaction1.CookieNameValue);
+
+ Assert.Equal(HttpStatusCode.Redirect, transaction2.Response.StatusCode);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task MapWillAffectChallengeOnlyWithUseAuth(bool useAuth)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ if (useAuth)
+ {
+ app.UseAuthentication();
+ }
+ app.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" })));
+ })
+ .ConfigureServices(s => s.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/page")));
+ var server = new TestServer(builder);
+
+ var transaction = await server.SendAsync("http://example.com/login");
+
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+
+ var location = transaction.Response.Headers.Location;
+ if (useAuth)
+ {
+ Assert.Equal("/page", location.LocalPath);
+ }
+ else
+ {
+ Assert.Equal("/login/page", location.LocalPath);
+ }
+ Assert.Equal("?ReturnUrl=%2F", location.Query);
+ }
+
+ [ConditionalFact(Skip = "Revisit, exception no longer thrown")]
+ public async Task ChallengeDoesNotSet401OnUnauthorized()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Run(async context =>
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme));
+ });
+ })
+ .ConfigureServices(services => services.AddAuthentication().AddCookie());
+ var server = new TestServer(builder);
+
+ var transaction = await server.SendAsync("http://example.com");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task CanConfigureDefaultCookieInstance()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Run(context => context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity())));
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddAuthentication().AddCookie();
+ services.Configure<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme,
+ o => o.Cookie.Name = "One");
+ });
+ var server = new TestServer(builder);
+
+ var transaction = await server.SendAsync("http://example.com");
+
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ Assert.StartsWith("One=", transaction.SetCookie[0]);
+ }
+
+ [Fact]
+ public async Task CanConfigureNamedCookieInstance()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Run(context => context.SignInAsync("Cookie1", new ClaimsPrincipal(new ClaimsIdentity())));
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddAuthentication().AddCookie("Cookie1");
+ services.Configure<CookieAuthenticationOptions>("Cookie1",
+ o => o.Cookie.Name = "One");
+ });
+ var server = new TestServer(builder);
+
+ var transaction = await server.SendAsync("http://example.com");
+
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ Assert.StartsWith("One=", transaction.SetCookie[0]);
+ }
+
+ [Fact]
+ public async Task MapWithSignInOnlyRedirectToReturnUrlOnLoginPath()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Map("/notlogin", signoutApp => signoutApp.Run(context => context.SignInAsync("Cookies",
+ new ClaimsPrincipal())));
+ })
+ .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/login")));
+ var server = new TestServer(builder);
+
+ var transaction = await server.SendAsync("http://example.com/notlogin?ReturnUrl=%2Fpage");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ Assert.NotNull(transaction.SetCookie);
+ }
+
+ [Fact]
+ public async Task MapWillNotAffectSignInRedirectToReturnUrl()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Map("/login", signoutApp => signoutApp.Run(context => context.SignInAsync("Cookies", new ClaimsPrincipal())));
+ })
+ .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/login")));
+
+ var server = new TestServer(builder);
+
+ var transaction = await server.SendAsync("http://example.com/login?ReturnUrl=%2Fpage");
+
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.NotNull(transaction.SetCookie);
+
+ var location = transaction.Response.Headers.Location;
+ Assert.Equal("/page", location.OriginalString);
+ }
+
+ [Fact]
+ public async Task MapWithSignOutOnlyRedirectToReturnUrlOnLogoutPath()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Map("/notlogout", signoutApp => signoutApp.Run(context => context.SignOutAsync("Cookies")));
+ })
+ .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LogoutPath = new PathString("/logout")));
+ var server = new TestServer(builder);
+
+ var transaction = await server.SendAsync("http://example.com/notlogout?ReturnUrl=%2Fpage");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ Assert.Contains(".AspNetCore.Cookies=; expires=", transaction.SetCookie[0]);
+ }
+
+ [Fact]
+ public async Task MapWillNotAffectSignOutRedirectToReturnUrl()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Map("/logout", signoutApp => signoutApp.Run(context => context.SignOutAsync("Cookies")));
+ })
+ .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LogoutPath = new PathString("/logout")));
+ var server = new TestServer(builder);
+
+ var transaction = await server.SendAsync("http://example.com/logout?ReturnUrl=%2Fpage");
+
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Contains(".AspNetCore.Cookies=; expires=", transaction.SetCookie[0]);
+
+ var location = transaction.Response.Headers.Location;
+ Assert.Equal("/page", location.OriginalString);
+ }
+
+ [Fact]
+ public async Task MapWillNotAffectAccessDenied()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Map("/forbid", signoutApp => signoutApp.Run(context => context.ForbidAsync("Cookies")));
+ })
+ .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.AccessDeniedPath = new PathString("/denied")));
+ var server = new TestServer(builder);
+ var transaction = await server.SendAsync("http://example.com/forbid");
+
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+
+ var location = transaction.Response.Headers.Location;
+ Assert.Equal("/denied", location.LocalPath);
+ }
+
+ [Fact]
+ public async Task NestedMapWillNotAffectLogin()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ app.Map("/base", map =>
+ {
+ map.UseAuthentication();
+ map.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" })));
+ }))
+ .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/page")));
+ var server = new TestServer(builder);
+ var transaction = await server.SendAsync("http://example.com/base/login");
+
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+
+ var location = transaction.Response.Headers.Location;
+ Assert.Equal("/base/page", location.LocalPath);
+ Assert.Equal("?ReturnUrl=%2F", location.Query);
+ }
+
+ [Theory]
+ [InlineData("/redirect_test")]
+ [InlineData("http://example.com/redirect_to")]
+ public async Task RedirectUriIsHoneredAfterSignin(string redirectUrl)
+ {
+ var server = CreateServer(o =>
+ {
+ o.LoginPath = "/testpath";
+ o.Cookie.Name = "TestCookie";
+ },
+ async context =>
+ await context.SignInAsync(
+ CookieAuthenticationDefaults.AuthenticationScheme,
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))),
+ new AuthenticationProperties { RedirectUri = redirectUrl })
+ );
+ var transaction = await SendAsync(server, "http://example.com/testpath");
+
+ Assert.NotEmpty(transaction.SetCookie);
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal(redirectUrl, transaction.Response.Headers.Location.ToString());
+ }
+
+ [Fact]
+ public async Task RedirectUriInQueryIsHoneredAfterSignin()
+ {
+ var server = CreateServer(o =>
+ {
+ o.LoginPath = "/testpath";
+ o.ReturnUrlParameter = "return";
+ o.Cookie.Name = "TestCookie";
+ },
+ async context =>
+ {
+ await context.SignInAsync(
+ CookieAuthenticationDefaults.AuthenticationScheme,
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))));
+ });
+ var transaction = await SendAsync(server, "http://example.com/testpath?return=%2Fret_path_2");
+
+ Assert.NotEmpty(transaction.SetCookie);
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/ret_path_2", transaction.Response.Headers.Location.ToString());
+ }
+
+ [Fact]
+ public async Task AbsoluteRedirectUriInQueryStringIsRejected()
+ {
+ var server = CreateServer(o =>
+ {
+ o.LoginPath = "/testpath";
+ o.ReturnUrlParameter = "return";
+ o.Cookie.Name = "TestCookie";
+ },
+ async context =>
+ {
+ await context.SignInAsync(
+ CookieAuthenticationDefaults.AuthenticationScheme,
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))));
+ });
+ var transaction = await SendAsync(server, "http://example.com/testpath?return=http%3A%2F%2Fexample.com%2Fredirect_to");
+
+ Assert.NotEmpty(transaction.SetCookie);
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task EnsurePrecedenceOfRedirectUriAfterSignin()
+ {
+ var server = CreateServer(o =>
+ {
+ o.LoginPath = "/testpath";
+ o.ReturnUrlParameter = "return";
+ o.Cookie.Name = "TestCookie";
+ },
+ async context =>
+ {
+ await context.SignInAsync(
+ CookieAuthenticationDefaults.AuthenticationScheme,
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))),
+ new AuthenticationProperties { RedirectUri = "/redirect_test" });
+ });
+ var transaction = await SendAsync(server, "http://example.com/testpath?return=%2Fret_path_2");
+
+ Assert.NotEmpty(transaction.SetCookie);
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/redirect_test", transaction.Response.Headers.Location.ToString());
+ }
+
+ [Fact]
+ public async Task NestedMapWillNotAffectAccessDenied()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ app.Map("/base", map =>
+ {
+ map.UseAuthentication();
+ map.Map("/forbid", signoutApp => signoutApp.Run(context => context.ForbidAsync("Cookies")));
+ }))
+ .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.AccessDeniedPath = new PathString("/denied")));
+ var server = new TestServer(builder);
+ var transaction = await server.SendAsync("http://example.com/base/forbid");
+
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+
+ var location = transaction.Response.Headers.Location;
+ Assert.Equal("/base/denied", location.LocalPath);
+ }
+
+ [Fact]
+ public async Task CanSpecifyAndShareDataProtector()
+ {
+
+ var dp = new NoOpDataProtector();
+ var builder1 = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Use((context, next) =>
+ context.SignInAsync("Cookies",
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))),
+ new AuthenticationProperties()));
+ })
+ .ConfigureServices(services => services.AddAuthentication().AddCookie(o =>
+ {
+ o.TicketDataFormat = new TicketDataFormat(dp);
+ o.Cookie.Name = "Cookie";
+ }));
+ var server1 = new TestServer(builder1);
+
+ var transaction = await SendAsync(server1, "http://example.com/stuff");
+ Assert.NotNull(transaction.SetCookie);
+
+ var builder2 = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Use(async (context, next) =>
+ {
+ var result = await context.AuthenticateAsync("Cookies");
+ Describe(context.Response, result);
+ });
+ })
+ .ConfigureServices(services => services.AddAuthentication().AddCookie("Cookies", o =>
+ {
+ o.Cookie.Name = "Cookie";
+ o.TicketDataFormat = new TicketDataFormat(dp);
+ }));
+ var server2 = new TestServer(builder2);
+ var transaction2 = await SendAsync(server2, "http://example.com/stuff", transaction.CookieNameValue);
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+ }
+
+ // Issue: https://github.com/aspnet/Security/issues/949
+ [Fact]
+ public async Task NullExpiresUtcPropertyIsGuarded()
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services => services.AddAuthentication().AddCookie(o =>
+ {
+ o.Events = new CookieAuthenticationEvents
+ {
+ OnValidatePrincipal = context =>
+ {
+ context.Properties.ExpiresUtc = null;
+ context.ShouldRenew = true;
+ return Task.FromResult(0);
+ }
+ };
+ }))
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+
+ app.Run(async context =>
+ {
+ if (context.Request.Path == "/signin")
+ {
+ await context.SignInAsync(
+ CookieAuthenticationDefaults.AuthenticationScheme,
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))));
+ }
+ else
+ {
+ await context.Response.WriteAsync("ha+1");
+ }
+ });
+ });
+
+ var server = new TestServer(builder);
+
+ var cookie = (await server.SendAsync("http://www.example.com/signin")).SetCookie.FirstOrDefault();
+ Assert.NotNull(cookie);
+
+ var transaction = await server.SendAsync("http://www.example.com/", cookie);
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ private class NoOpDataProtector : IDataProtector
+ {
+ public IDataProtector CreateProtector(string purpose)
+ {
+ return this;
+ }
+
+ public byte[] Protect(byte[] plaintext)
+ {
+ return plaintext;
+ }
+
+ public byte[] Unprotect(byte[] protectedData)
+ {
+ return protectedData;
+ }
+ }
+
+ private static string FindClaimValue(Transaction transaction, string claimType)
+ {
+ var claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType);
+ if (claim == null)
+ {
+ return null;
+ }
+ return claim.Attribute("value").Value;
+ }
+
+ private static string FindPropertiesValue(Transaction transaction, string key)
+ {
+ var property = transaction.ResponseElement.Elements("extra").SingleOrDefault(elt => elt.Attribute("type").Value == key);
+ if (property == null)
+ {
+ return null;
+ }
+ return property.Attribute("value").Value;
+ }
+
+ private static async Task<XElement> GetAuthData(TestServer server, string url, string cookie)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, url);
+ request.Headers.Add("Cookie", cookie);
+
+ var response2 = await server.CreateClient().SendAsync(request);
+ var text = await response2.Content.ReadAsStringAsync();
+ var me = XElement.Parse(text);
+ return me;
+ }
+
+ private class ClaimsTransformer : IClaimsTransformation
+ {
+ public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal p)
+ {
+ var firstId = p.Identities.First();
+ if (firstId.HasClaim("marker", "true"))
+ {
+ firstId.RemoveClaim(firstId.FindFirst("marker"));
+ }
+ // TransformAsync could be called twice on one request if you have a default scheme and also
+ // call AuthenticateAsync.
+ if (!p.Identities.Any(i => i.AuthenticationType == "xform"))
+ {
+ var id = new ClaimsIdentity("xform");
+ id.AddClaim(new Claim("xform", "yup"));
+ p.AddIdentity(id);
+ }
+ return Task.FromResult(p);
+ }
+ }
+
+ private TestServer CreateServer(Action<CookieAuthenticationOptions> configureOptions, Func<HttpContext, Task> testpath = null, Uri baseAddress = null, bool claimsTransform = false)
+ => CreateServerWithServices(s =>
+ {
+ s.AddSingleton<ISystemClock>(_clock);
+ s.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(configureOptions);
+ if (claimsTransform)
+ {
+ s.AddSingleton<IClaimsTransformation, ClaimsTransformer>();
+ }
+ }, testpath, baseAddress);
+
+ private static TestServer CreateServerWithServices(Action<IServiceCollection> configureServices, Func<HttpContext, Task> testpath = null, Uri baseAddress = null)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Use(async (context, next) =>
+ {
+ var req = context.Request;
+ var res = context.Response;
+ PathString remainder;
+ if (req.Path == new PathString("/normal"))
+ {
+ res.StatusCode = 200;
+ }
+ else if (req.Path == new PathString("/forbid")) // Simulate forbidden
+ {
+ await context.ForbidAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ }
+ else if (req.Path == new PathString("/challenge"))
+ {
+ await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ }
+ else if (req.Path == new PathString("/signout"))
+ {
+ await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ }
+ else if (req.Path == new PathString("/unauthorized"))
+ {
+ await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties());
+ }
+ else if (req.Path == new PathString("/protected/CustomRedirect"))
+ {
+ await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties() { RedirectUri = "/CustomRedirect" });
+ }
+ else if (req.Path == new PathString("/me"))
+ {
+ Describe(res, AuthenticateResult.Success(new AuthenticationTicket(context.User, new AuthenticationProperties(), CookieAuthenticationDefaults.AuthenticationScheme)));
+ }
+ else if (req.Path.StartsWithSegments(new PathString("/me"), out remainder))
+ {
+ var ticket = await context.AuthenticateAsync(remainder.Value.Substring(1));
+ Describe(res, ticket);
+ }
+ else if (req.Path == new PathString("/testpath") && testpath != null)
+ {
+ await testpath(context);
+ }
+ else if (req.Path == new PathString("/checkforerrors"))
+ {
+ var result = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); // this used to be "Automatic"
+ if (result.Failure != null)
+ {
+ throw new Exception("Failed to authenticate", result.Failure);
+ }
+ return;
+ }
+ else
+ {
+ await next();
+ }
+ });
+ })
+ .ConfigureServices(configureServices);
+ var server = new TestServer(builder);
+ server.BaseAddress = baseAddress;
+ return server;
+ }
+
+ private static void Describe(HttpResponse res, AuthenticateResult result)
+ {
+ res.StatusCode = 200;
+ res.ContentType = "text/xml";
+ var xml = new XElement("xml");
+ if (result?.Ticket?.Principal != null)
+ {
+ xml.Add(result.Ticket.Principal.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value))));
+ }
+ if (result?.Ticket?.Properties != null)
+ {
+ xml.Add(result.Ticket.Properties.Items.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value))));
+ }
+ var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString());
+ res.Body.Write(xmlBytes, 0, xmlBytes.Length);
+ }
+
+ private static async Task<Transaction> SendAsync(TestServer server, string uri, string cookieHeader = null)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, uri);
+ if (!string.IsNullOrEmpty(cookieHeader))
+ {
+ request.Headers.Add("Cookie", cookieHeader);
+ }
+ var transaction = new Transaction
+ {
+ Request = request,
+ Response = await server.CreateClient().SendAsync(request),
+ };
+ if (transaction.Response.Headers.Contains("Set-Cookie"))
+ {
+ transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").SingleOrDefault();
+ }
+ if (!string.IsNullOrEmpty(transaction.SetCookie))
+ {
+ transaction.CookieNameValue = transaction.SetCookie.Split(new[] { ';' }, 2).First();
+ }
+ transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync();
+
+ if (transaction.Response.Content != null &&
+ transaction.Response.Content.Headers.ContentType != null &&
+ transaction.Response.Content.Headers.ContentType.MediaType == "text/xml")
+ {
+ transaction.ResponseElement = XElement.Parse(transaction.ResponseText);
+ }
+ return transaction;
+ }
+
+ private class Transaction
+ {
+ public HttpRequestMessage Request { get; set; }
+ public HttpResponseMessage Response { get; set; }
+
+ public string SetCookie { get; set; }
+ public string CookieNameValue { get; set; }
+
+ public string ResponseText { get; set; }
+ public XElement ResponseElement { get; set; }
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/DynamicSchemeTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/DynamicSchemeTests.cs
new file mode 100644
index 0000000000..d658609b04
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/DynamicSchemeTests.cs
@@ -0,0 +1,170 @@
+// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information.
+
+using System;
+using System.Net;
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class DynamicSchemeTests
+ {
+ [Fact]
+ public async Task OptionsAreConfiguredOnce()
+ {
+ var server = CreateServer(s =>
+ {
+ s.Configure<TestOptions>("One", o => o.Instance = new Singleton());
+ s.Configure<TestOptions>("Two", o => o.Instance = new Singleton());
+ });
+ // Add One scheme
+ var response = await server.CreateClient().GetAsync("http://example.com/add/One");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var transaction = await server.SendAsync("http://example.com/auth/One");
+ Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One"));
+ Assert.Equal("1", transaction.FindClaimValue("Count"));
+
+ // Verify option is not recreated
+ transaction = await server.SendAsync("http://example.com/auth/One");
+ Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One"));
+ Assert.Equal("1", transaction.FindClaimValue("Count"));
+
+ // Add Two scheme
+ response = await server.CreateClient().GetAsync("http://example.com/add/Two");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ transaction = await server.SendAsync("http://example.com/auth/Two");
+ Assert.Equal("Two", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "Two"));
+ Assert.Equal("2", transaction.FindClaimValue("Count"));
+
+ // Verify options are not recreated
+ transaction = await server.SendAsync("http://example.com/auth/One");
+ Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One"));
+ Assert.Equal("1", transaction.FindClaimValue("Count"));
+ transaction = await server.SendAsync("http://example.com/auth/Two");
+ Assert.Equal("Two", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "Two"));
+ Assert.Equal("2", transaction.FindClaimValue("Count"));
+ }
+
+ [Fact]
+ public async Task CanAddAndRemoveSchemes()
+ {
+ var server = CreateServer();
+ await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("http://example.com/auth/One"));
+
+ // Add One scheme
+ var response = await server.CreateClient().GetAsync("http://example.com/add/One");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var transaction = await server.SendAsync("http://example.com/auth/One");
+ Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One"));
+
+ // Add Two scheme
+ response = await server.CreateClient().GetAsync("http://example.com/add/Two");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ transaction = await server.SendAsync("http://example.com/auth/Two");
+ Assert.Equal("Two", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "Two"));
+
+ // Remove Two
+ response = await server.CreateClient().GetAsync("http://example.com/remove/Two");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("http://example.com/auth/Two"));
+ transaction = await server.SendAsync("http://example.com/auth/One");
+ Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One"));
+
+ // Remove One
+ response = await server.CreateClient().GetAsync("http://example.com/remove/One");
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("http://example.com/auth/Two"));
+ await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("http://example.com/auth/One"));
+ }
+
+ public class TestOptions : AuthenticationSchemeOptions
+ {
+ public Singleton Instance { get; set; }
+ }
+
+ public class Singleton
+ {
+ public static int _count;
+
+ public Singleton()
+ {
+ _count++;
+ Count = _count;
+ }
+
+ public int Count { get; }
+ }
+
+ private class TestHandler : AuthenticationHandler<TestOptions>
+ {
+ public TestHandler(IOptionsMonitor<TestOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
+ {
+ }
+
+ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
+ {
+ var principal = new ClaimsPrincipal();
+ var id = new ClaimsIdentity();
+ id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name));
+ if (Options.Instance != null)
+ {
+ id.AddClaim(new Claim("Count", Options.Instance.Count.ToString()));
+ }
+ principal.AddIdentity(id);
+ return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name)));
+ }
+ }
+
+ private static TestServer CreateServer(Action<IServiceCollection> configureServices = null)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Use(async (context, next) =>
+ {
+ var req = context.Request;
+ var res = context.Response;
+ if (req.Path.StartsWithSegments(new PathString("/add"), out var remainder))
+ {
+ var name = remainder.Value.Substring(1);
+ var auth = context.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>();
+ var scheme = new AuthenticationScheme(name, name, typeof(TestHandler));
+ auth.AddScheme(scheme);
+ }
+ else if (req.Path.StartsWithSegments(new PathString("/auth"), out remainder))
+ {
+ var name = (remainder.Value.Length > 0) ? remainder.Value.Substring(1) : null;
+ var result = await context.AuthenticateAsync(name);
+ res.Describe(result?.Ticket?.Principal);
+ }
+ else if (req.Path.StartsWithSegments(new PathString("/remove"), out remainder))
+ {
+ var name = remainder.Value.Substring(1);
+ var auth = context.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>();
+ auth.RemoveScheme(name);
+ }
+ else
+ {
+ await next();
+ }
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ configureServices?.Invoke(services);
+ services.AddAuthentication();
+ });
+ return new TestServer(builder);
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs
new file mode 100644
index 0000000000..b909be9fdc
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs
@@ -0,0 +1,836 @@
+// 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.Net;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.AspNetCore.Authentication.Tests;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.Facebook
+{
+ public class FacebookTests
+ {
+ private void ConfigureDefaults(FacebookOptions o)
+ {
+ o.AppId = "whatever";
+ o.AppSecret = "whatever";
+ o.SignInScheme = "auth1";
+ }
+
+ [Fact]
+ public async Task CanForwardDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = FacebookDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ })
+ .AddFacebook(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ });
+
+ var forwardDefault = new TestHandler();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignInThrows()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = FacebookDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddFacebook(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignOutThrows()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = FacebookDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddFacebook(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ }
+
+ [Fact]
+ public async Task ForwardForbidWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = FacebookDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddFacebook(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ForbidAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(1, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardAuthenticateWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = FacebookDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddFacebook(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardAuthenticate = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(1, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardChallengeWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = FacebookDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler>("specific", "specific");
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ })
+ .AddFacebook(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardChallenge = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ChallengeAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(1, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardSelectorWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = FacebookDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddFacebook(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, selector.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, selector.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, selector.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task NullForwardSelectorUsesDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = FacebookDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddFacebook(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => null;
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task SpecificForwardWinsOverSelectorAndDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = FacebookDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddFacebook(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ o.ForwardAuthenticate = "specific";
+ o.ForwardChallenge = "specific";
+ o.ForwardSignIn = "specific";
+ o.ForwardSignOut = "specific";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, specific.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, specific.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, specific.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ }
+
+ [Fact]
+ public async Task VerifySignInSchemeCannotBeSetToSelf()
+ {
+ var server = CreateServer(
+ app => { },
+ services => services.AddAuthentication().AddFacebook(o =>
+ {
+ o.AppId = "whatever";
+ o.AppSecret = "whatever";
+ o.SignInScheme = FacebookDefaults.AuthenticationScheme;
+ }),
+ async context =>
+ {
+ await context.ChallengeAsync("Facebook");
+ return true;
+ });
+ var error = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("https://example.com/challenge"));
+ Assert.Contains("cannot be set to itself", error.Message);
+ }
+
+ [Fact]
+ public async Task VerifySignInSchemeCannotBeSetToSelfUsingDefaultScheme()
+ {
+ var server = CreateServer(
+ app => { },
+ services => services.AddAuthentication(o => o.DefaultScheme = FacebookDefaults.AuthenticationScheme).AddFacebook(o =>
+ {
+ o.AppId = "whatever";
+ o.AppSecret = "whatever";
+ }),
+ async context =>
+ {
+ await context.ChallengeAsync("Facebook");
+ return true;
+ });
+ var error = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("https://example.com/challenge"));
+ Assert.Contains("cannot be set to itself", error.Message);
+ }
+
+ [Fact]
+ public async Task VerifySignInSchemeCannotBeSetToSelfUsingDefaultSignInScheme()
+ {
+ var server = CreateServer(
+ app => { },
+ services => services.AddAuthentication(o => o.DefaultSignInScheme = FacebookDefaults.AuthenticationScheme).AddFacebook(o =>
+ {
+ o.AppId = "whatever";
+ o.AppSecret = "whatever";
+ }),
+ async context =>
+ {
+ await context.ChallengeAsync("Facebook");
+ return true;
+ });
+ var error = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("https://example.com/challenge"));
+ Assert.Contains("cannot be set to itself", error.Message);
+ }
+
+ [Fact]
+ public async Task VerifySchemeDefaults()
+ {
+ var services = new ServiceCollection();
+ services.AddAuthentication().AddFacebook();
+ var sp = services.BuildServiceProvider();
+ var schemeProvider = sp.GetRequiredService<IAuthenticationSchemeProvider>();
+ var scheme = await schemeProvider.GetSchemeAsync(FacebookDefaults.AuthenticationScheme);
+ Assert.NotNull(scheme);
+ Assert.Equal("FacebookHandler", scheme.HandlerType.Name);
+ Assert.Equal(FacebookDefaults.AuthenticationScheme, scheme.DisplayName);
+ }
+
+ [Fact]
+ public async Task ThrowsIfAppIdMissing()
+ {
+ var server = CreateServer(
+ app => { },
+ services => services.AddAuthentication().AddFacebook(o => o.SignInScheme = "Whatever"),
+ async context =>
+ {
+ await Assert.ThrowsAsync<ArgumentException>("AppId", () => context.ChallengeAsync("Facebook"));
+ return true;
+ });
+ var transaction = await server.SendAsync("http://example.com/challenge");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ThrowsIfAppSecretMissing()
+ {
+ var server = CreateServer(
+ app => { },
+ services => services.AddAuthentication().AddFacebook(o => o.AppId = "Whatever"),
+ async context =>
+ {
+ await Assert.ThrowsAsync<ArgumentException>("AppSecret", () => context.ChallengeAsync("Facebook"));
+ return true;
+ });
+ var transaction = await server.SendAsync("http://example.com/challenge");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ChallengeWillTriggerApplyRedirectEvent()
+ {
+ var server = CreateServer(
+ app =>
+ {
+ app.UseAuthentication();
+ },
+ services =>
+ {
+ services.AddAuthentication("External")
+ .AddCookie("External", o => { })
+ .AddFacebook(o =>
+ {
+ o.AppId = "Test App Id";
+ o.AppSecret = "Test App Secret";
+ o.Events = new OAuthEvents
+ {
+ OnRedirectToAuthorizationEndpoint = context =>
+ {
+ context.Response.Redirect(context.RedirectUri + "&custom=test");
+ return Task.FromResult(0);
+ }
+ };
+ });
+ },
+ async context =>
+ {
+ await context.ChallengeAsync("Facebook");
+ return true;
+ });
+ var transaction = await server.SendAsync("http://example.com/challenge");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var query = transaction.Response.Headers.Location.Query;
+ Assert.Contains("custom=test", query);
+ }
+
+ [Fact]
+ public async Task ChallengeWillIncludeScopeAsConfigured()
+ {
+ var server = CreateServer(
+ app => app.UseAuthentication(),
+ services =>
+ {
+ services.AddAuthentication().AddFacebook(o =>
+ {
+ o.AppId = "Test App Id";
+ o.AppSecret = "Test App Secret";
+ o.Scope.Clear();
+ o.Scope.Add("foo");
+ o.Scope.Add("bar");
+ });
+ },
+ async context =>
+ {
+ await context.ChallengeAsync(FacebookDefaults.AuthenticationScheme);
+ return true;
+ });
+
+ var transaction = await server.SendAsync("http://example.com/challenge");
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.Contains("scope=foo,bar", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task ChallengeWillIncludeScopeAsOverwritten()
+ {
+ var server = CreateServer(
+ app => app.UseAuthentication(),
+ services =>
+ {
+ services.AddAuthentication().AddFacebook(o =>
+ {
+ o.AppId = "Test App Id";
+ o.AppSecret = "Test App Secret";
+ o.Scope.Clear();
+ o.Scope.Add("foo");
+ o.Scope.Add("bar");
+ });
+ },
+ async context =>
+ {
+ var properties = new OAuthChallengeProperties();
+ properties.SetScope("baz", "qux");
+ await context.ChallengeAsync(FacebookDefaults.AuthenticationScheme, properties);
+ return true;
+ });
+
+ var transaction = await server.SendAsync("http://example.com/challenge");
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.Contains("scope=baz,qux", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task ChallengeWillIncludeScopeAsOverwrittenWithBaseAuthenticationProperties()
+ {
+ var server = CreateServer(
+ app => app.UseAuthentication(),
+ services =>
+ {
+ services.AddAuthentication().AddFacebook(o =>
+ {
+ o.AppId = "Test App Id";
+ o.AppSecret = "Test App Secret";
+ o.Scope.Clear();
+ o.Scope.Add("foo");
+ o.Scope.Add("bar");
+ });
+ },
+ async context =>
+ {
+ var properties = new AuthenticationProperties();
+ properties.SetParameter(OAuthChallengeProperties.ScopeKey, new string[] { "baz", "qux" });
+ await context.ChallengeAsync(FacebookDefaults.AuthenticationScheme, properties);
+ return true;
+ });
+
+ var transaction = await server.SendAsync("http://example.com/challenge");
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.Contains("scope=baz,qux", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task NestedMapWillNotAffectRedirect()
+ {
+ var server = CreateServer(app => app.Map("/base", map =>
+ {
+ map.UseAuthentication();
+ map.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Facebook", new AuthenticationProperties() { RedirectUri = "/" })));
+ }),
+ services =>
+ {
+ services.AddAuthentication()
+ .AddCookie("External", o => { })
+ .AddFacebook(o =>
+ {
+ o.AppId = "Test App Id";
+ o.AppSecret = "Test App Secret";
+ });
+ },
+ handler: null);
+
+ var transaction = await server.SendAsync("http://example.com/base/login");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var location = transaction.Response.Headers.Location.AbsoluteUri;
+ Assert.Contains("https://www.facebook.com/v2.12/dialog/oauth", location);
+ Assert.Contains("response_type=code", location);
+ Assert.Contains("client_id=", location);
+ Assert.Contains("redirect_uri=" + UrlEncoder.Default.Encode("http://example.com/base/signin-facebook"), location);
+ Assert.Contains("scope=", location);
+ Assert.Contains("state=", location);
+ }
+
+ [Fact]
+ public async Task MapWillNotAffectRedirect()
+ {
+ var server = CreateServer(
+ app =>
+ {
+ app.UseAuthentication();
+ app.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Facebook", new AuthenticationProperties() { RedirectUri = "/" })));
+ },
+ services =>
+ {
+ services.AddAuthentication()
+ .AddCookie("External", o => { })
+ .AddFacebook(o =>
+ {
+ o.AppId = "Test App Id";
+ o.AppSecret = "Test App Secret";
+ o.SignInScheme = "External";
+ });
+ },
+ handler: null);
+ var transaction = await server.SendAsync("http://example.com/login");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var location = transaction.Response.Headers.Location.AbsoluteUri;
+ Assert.Contains("https://www.facebook.com/v2.12/dialog/oauth", location);
+ Assert.Contains("response_type=code", location);
+ Assert.Contains("client_id=", location);
+ Assert.Contains("redirect_uri=" + UrlEncoder.Default.Encode("http://example.com/signin-facebook"), location);
+ Assert.Contains("scope=", location);
+ Assert.Contains("state=", location);
+ }
+
+ [Fact]
+ public async Task ChallengeWillTriggerRedirection()
+ {
+ var server = CreateServer(
+ app => app.UseAuthentication(),
+ services =>
+ {
+ services.AddAuthentication(options =>
+ {
+ options.DefaultSignInScheme = "External";
+ })
+ .AddCookie()
+ .AddFacebook(o =>
+ {
+ o.AppId = "Test App Id";
+ o.AppSecret = "Test App Secret";
+ });
+ },
+ async context =>
+ {
+ await context.ChallengeAsync("Facebook");
+ return true;
+ });
+ var transaction = await server.SendAsync("http://example.com/challenge");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var location = transaction.Response.Headers.Location.AbsoluteUri;
+ Assert.Contains("https://www.facebook.com/v2.12/dialog/oauth", location);
+ Assert.Contains("response_type=code", location);
+ Assert.Contains("client_id=", location);
+ Assert.Contains("redirect_uri=", location);
+ Assert.Contains("scope=", location);
+ Assert.Contains("state=", location);
+ }
+
+ [Fact]
+ public async Task CustomUserInfoEndpointHasValidGraphQuery()
+ {
+ var customUserInfoEndpoint = "https://graph.facebook.com/me?fields=email,timezone,picture";
+ var finalUserInfoEndpoint = string.Empty;
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("FacebookTest"));
+ var server = CreateServer(
+ app => app.UseAuthentication(),
+ services =>
+ {
+ services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
+ .AddCookie()
+ .AddFacebook(o =>
+ {
+ o.AppId = "Test App Id";
+ o.AppSecret = "Test App Secret";
+ o.StateDataFormat = stateFormat;
+ o.UserInformationEndpoint = customUserInfoEndpoint;
+ o.BackchannelHttpHandler = new TestHttpMessageHandler
+ {
+ Sender = req =>
+ {
+ if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == FacebookDefaults.TokenEndpoint)
+ {
+ var res = new HttpResponseMessage(HttpStatusCode.OK);
+ var graphResponse = JsonConvert.SerializeObject(new
+ {
+ access_token = "TestAuthToken"
+ });
+ res.Content = new StringContent(graphResponse, Encoding.UTF8);
+ return res;
+ }
+ if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) ==
+ new Uri(customUserInfoEndpoint).GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped))
+ {
+ finalUserInfoEndpoint = req.RequestUri.ToString();
+ var res = new HttpResponseMessage(HttpStatusCode.OK);
+ var graphResponse = JsonConvert.SerializeObject(new
+ {
+ id = "TestProfileId",
+ name = "TestName"
+ });
+ res.Content = new StringContent(graphResponse, Encoding.UTF8);
+ return res;
+ }
+ return null;
+ }
+ };
+ });
+ },
+ handler: null);
+
+ var properties = new AuthenticationProperties();
+ var correlationKey = ".xsrf";
+ var correlationValue = "TestCorrelationId";
+ properties.Items.Add(correlationKey, correlationValue);
+ properties.RedirectUri = "/me";
+ var state = stateFormat.Protect(properties);
+ var transaction = await server.SendAsync(
+ "https://example.com/signin-facebook?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
+ $".AspNetCore.Correlation.Facebook.{correlationValue}=N");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
+ Assert.Equal(1, finalUserInfoEndpoint.Count(c => c == '?'));
+ Assert.Contains("fields=email,timezone,picture", finalUserInfoEndpoint);
+ Assert.Contains("&access_token=", finalUserInfoEndpoint);
+ }
+
+ private static TestServer CreateServer(Action<IApplicationBuilder> configure, Action<IServiceCollection> configureServices, Func<HttpContext, Task<bool>> handler)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ configure?.Invoke(app);
+ app.Use(async (context, next) =>
+ {
+ if (handler == null || !await handler(context))
+ {
+ await next();
+ }
+ });
+ })
+ .ConfigureServices(configureServices);
+ return new TestServer(builder);
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs
new file mode 100644
index 0000000000..511a658ff4
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs
@@ -0,0 +1,1622 @@
+// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.AspNetCore.Authentication.Tests;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.Google
+{
+ public class GoogleTests
+ {
+ private void ConfigureDefaults(GoogleOptions o)
+ {
+ o.ClientId = "whatever";
+ o.ClientSecret = "whatever";
+ o.SignInScheme = "auth1";
+ }
+
+ [Fact]
+ public async Task CanForwardDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = GoogleDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ })
+ .AddGoogle(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ });
+
+ var forwardDefault = new TestHandler();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignInThrows()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = GoogleDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddGoogle(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignOutThrows()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = GoogleDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddGoogle(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ }
+
+ [Fact]
+ public async Task ForwardForbidWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = GoogleDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddGoogle(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ForbidAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(1, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardAuthenticateWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = GoogleDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddGoogle(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardAuthenticate = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(1, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardChallengeWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = GoogleDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler>("specific", "specific");
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ })
+ .AddGoogle(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardChallenge = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ChallengeAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(1, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardSelectorWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = GoogleDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddGoogle(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, selector.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, selector.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, selector.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task NullForwardSelectorUsesDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = GoogleDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddGoogle(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => null;
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task SpecificForwardWinsOverSelectorAndDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = GoogleDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddGoogle(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ o.ForwardAuthenticate = "specific";
+ o.ForwardChallenge = "specific";
+ o.ForwardSignIn = "specific";
+ o.ForwardSignOut = "specific";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, specific.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, specific.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, specific.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ }
+
+ [Fact]
+ public async Task VerifySignInSchemeCannotBeSetToSelf()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.SignInScheme = GoogleDefaults.AuthenticationScheme;
+ });
+ var error = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("https://example.com/challenge"));
+ Assert.Contains("cannot be set to itself", error.Message);
+ }
+
+ [Fact]
+ public async Task VerifySchemeDefaults()
+ {
+ var services = new ServiceCollection();
+ services.AddAuthentication().AddGoogle();
+ var sp = services.BuildServiceProvider();
+ var schemeProvider = sp.GetRequiredService<IAuthenticationSchemeProvider>();
+ var scheme = await schemeProvider.GetSchemeAsync(GoogleDefaults.AuthenticationScheme);
+ Assert.NotNull(scheme);
+ Assert.Equal("GoogleHandler", scheme.HandlerType.Name);
+ Assert.Equal(GoogleDefaults.AuthenticationScheme, scheme.DisplayName);
+ }
+
+ [Fact]
+ public async Task ChallengeWillTriggerRedirection()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ });
+ var transaction = await server.SendAsync("https://example.com/challenge");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var location = transaction.Response.Headers.Location.ToString();
+ Assert.Contains("https://accounts.google.com/o/oauth2/v2/auth?response_type=code", location);
+ Assert.Contains("&client_id=", location);
+ Assert.Contains("&redirect_uri=", location);
+ Assert.Contains("&scope=", location);
+ Assert.Contains("&state=", location);
+
+ Assert.DoesNotContain("access_type=", location);
+ Assert.DoesNotContain("prompt=", location);
+ Assert.DoesNotContain("approval_prompt=", location);
+ Assert.DoesNotContain("login_hint=", location);
+ Assert.DoesNotContain("include_granted_scopes=", location);
+ }
+
+ [Fact]
+ public async Task SignInThrows()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ });
+ var transaction = await server.SendAsync("https://example.com/signIn");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task SignOutThrows()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ });
+ var transaction = await server.SendAsync("https://example.com/signOut");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ForbidThrows()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ });
+ var transaction = await server.SendAsync("https://example.com/signOut");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task Challenge401WillNotTriggerRedirection()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ });
+ var transaction = await server.SendAsync("https://example.com/401");
+ Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ChallengeWillSetCorrelationCookie()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ });
+ var transaction = await server.SendAsync("https://example.com/challenge");
+ Assert.Contains(transaction.SetCookie, cookie => cookie.StartsWith(".AspNetCore.Correlation.Google."));
+ }
+
+ [Fact]
+ public async Task ChallengeWillSetDefaultScope()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ });
+ var transaction = await server.SendAsync("https://example.com/challenge");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var query = transaction.Response.Headers.Location.Query;
+ Assert.Contains("&scope=" + UrlEncoder.Default.Encode("openid profile email"), query);
+ }
+
+ [Fact]
+ public async Task ChallengeWillUseAuthenticationPropertiesParametersAsQueryArguments()
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.StateDataFormat = stateFormat;
+ },
+ context =>
+ {
+ var req = context.Request;
+ var res = context.Response;
+ if (req.Path == new PathString("/challenge2"))
+ {
+ return context.ChallengeAsync("Google", new GoogleChallengeProperties
+ {
+ Scope = new string[] { "openid", "https://www.googleapis.com/auth/plus.login" },
+ AccessType = "offline",
+ ApprovalPrompt = "force",
+ Prompt = "consent",
+ LoginHint = "test@example.com",
+ IncludeGrantedScopes = false,
+ });
+ }
+
+ return Task.FromResult<object>(null);
+ });
+ var transaction = await server.SendAsync("https://example.com/challenge2");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+
+ // verify query arguments
+ var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query);
+ Assert.Equal("openid https://www.googleapis.com/auth/plus.login", query["scope"]);
+ Assert.Equal("offline", query["access_type"]);
+ Assert.Equal("force", query["approval_prompt"]);
+ Assert.Equal("consent", query["prompt"]);
+ Assert.Equal("false", query["include_granted_scopes"]);
+ Assert.Equal("test@example.com", query["login_hint"]);
+
+ // verify that the passed items were not serialized
+ var stateProperties = stateFormat.Unprotect(query["state"]);
+ Assert.DoesNotContain("scope", stateProperties.Items.Keys);
+ Assert.DoesNotContain("access_type", stateProperties.Items.Keys);
+ Assert.DoesNotContain("include_granted_scopes", stateProperties.Items.Keys);
+ Assert.DoesNotContain("approval_prompt", stateProperties.Items.Keys);
+ Assert.DoesNotContain("prompt", stateProperties.Items.Keys);
+ Assert.DoesNotContain("login_hint", stateProperties.Items.Keys);
+ }
+
+ [Fact]
+ public async Task ChallengeWillUseAuthenticationPropertiesItemsAsParameters()
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.StateDataFormat = stateFormat;
+ },
+ context =>
+ {
+ var req = context.Request;
+ var res = context.Response;
+ if (req.Path == new PathString("/challenge2"))
+ {
+ return context.ChallengeAsync("Google", new AuthenticationProperties(new Dictionary<string, string>()
+ {
+ { "scope", "https://www.googleapis.com/auth/plus.login" },
+ { "access_type", "offline" },
+ { "approval_prompt", "force" },
+ { "prompt", "consent" },
+ { "login_hint", "test@example.com" },
+ { "include_granted_scopes", "false" }
+ }));
+ }
+
+ return Task.FromResult<object>(null);
+ });
+ var transaction = await server.SendAsync("https://example.com/challenge2");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+
+ // verify query arguments
+ var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query);
+ Assert.Equal("https://www.googleapis.com/auth/plus.login", query["scope"]);
+ Assert.Equal("offline", query["access_type"]);
+ Assert.Equal("force", query["approval_prompt"]);
+ Assert.Equal("consent", query["prompt"]);
+ Assert.Equal("false", query["include_granted_scopes"]);
+ Assert.Equal("test@example.com", query["login_hint"]);
+
+ // verify that the passed items were not serialized
+ var stateProperties = stateFormat.Unprotect(query["state"]);
+ Assert.DoesNotContain("scope", stateProperties.Items.Keys);
+ Assert.DoesNotContain("access_type", stateProperties.Items.Keys);
+ Assert.DoesNotContain("include_granted_scopes", stateProperties.Items.Keys);
+ Assert.DoesNotContain("approval_prompt", stateProperties.Items.Keys);
+ Assert.DoesNotContain("prompt", stateProperties.Items.Keys);
+ Assert.DoesNotContain("login_hint", stateProperties.Items.Keys);
+ }
+
+ [Fact]
+ public async Task ChallengeWillUseAuthenticationPropertiesItemsAsQueryArgumentsButParametersWillOverwrite()
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.StateDataFormat = stateFormat;
+ },
+ context =>
+ {
+ var req = context.Request;
+ var res = context.Response;
+ if (req.Path == new PathString("/challenge2"))
+ {
+ return context.ChallengeAsync("Google", new GoogleChallengeProperties(new Dictionary<string, string>
+ {
+ ["scope"] = "https://www.googleapis.com/auth/plus.login",
+ ["access_type"] = "offline",
+ ["include_granted_scopes"] = "false",
+ ["approval_prompt"] = "force",
+ ["prompt"] = "login",
+ ["login_hint"] = "this-will-be-overwritten@example.com",
+ })
+ {
+ Prompt = "consent",
+ LoginHint = "test@example.com",
+ });
+ }
+
+ return Task.FromResult<object>(null);
+ });
+ var transaction = await server.SendAsync("https://example.com/challenge2");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+
+ // verify query arguments
+ var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query);
+ Assert.Equal("https://www.googleapis.com/auth/plus.login", query["scope"]);
+ Assert.Equal("offline", query["access_type"]);
+ Assert.Equal("force", query["approval_prompt"]);
+ Assert.Equal("consent", query["prompt"]);
+ Assert.Equal("false", query["include_granted_scopes"]);
+ Assert.Equal("test@example.com", query["login_hint"]);
+
+ // verify that the passed items were not serialized
+ var stateProperties = stateFormat.Unprotect(query["state"]);
+ Assert.DoesNotContain("scope", stateProperties.Items.Keys);
+ Assert.DoesNotContain("access_type", stateProperties.Items.Keys);
+ Assert.DoesNotContain("include_granted_scopes", stateProperties.Items.Keys);
+ Assert.DoesNotContain("approval_prompt", stateProperties.Items.Keys);
+ Assert.DoesNotContain("prompt", stateProperties.Items.Keys);
+ Assert.DoesNotContain("login_hint", stateProperties.Items.Keys);
+ }
+
+ [Fact]
+ public async Task ChallengeWillTriggerApplyRedirectEvent()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.Events = new OAuthEvents
+ {
+ OnRedirectToAuthorizationEndpoint = context =>
+ {
+ context.Response.Redirect(context.RedirectUri + "&custom=test");
+ return Task.FromResult(0);
+ }
+ };
+ });
+ var transaction = await server.SendAsync("https://example.com/challenge");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var query = transaction.Response.Headers.Location.Query;
+ Assert.Contains("custom=test", query);
+ }
+
+ [Fact]
+ public async Task AuthenticateWithoutCookieWillFail()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ },
+ async context =>
+ {
+ var req = context.Request;
+ var res = context.Response;
+ if (req.Path == new PathString("/auth"))
+ {
+ var result = await context.AuthenticateAsync("Google");
+ Assert.NotNull(result.Failure);
+ }
+ });
+ var transaction = await server.SendAsync("https://example.com/auth");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ReplyPathWithoutStateQueryStringWillBeRejected()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ });
+ var error = await Assert.ThrowsAnyAsync<Exception>(() => server.SendAsync("https://example.com/signin-google?code=TestCode"));
+ Assert.Equal("The oauth state was missing or invalid.", error.GetBaseException().Message);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ReplyPathWithErrorFails(bool redirect)
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.StateDataFormat = new TestStateDataFormat();
+ o.Events = redirect ? new OAuthEvents()
+ {
+ OnRemoteFailure = ctx =>
+ {
+ ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message));
+ ctx.HandleResponse();
+ return Task.FromResult(0);
+ }
+ } : new OAuthEvents();
+ });
+ var sendTask = server.SendAsync("https://example.com/signin-google?error=OMG&error_description=SoBad&error_uri=foobar&state=protected_state",
+ ".AspNetCore.Correlation.Google.corrilationId=N");
+ if (redirect)
+ {
+ var transaction = await sendTask;
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/error?FailureMessage=OMG" + UrlEncoder.Default.Encode(";Description=SoBad;Uri=foobar"), transaction.Response.Headers.GetValues("Location").First());
+ }
+ else
+ {
+ var error = await Assert.ThrowsAnyAsync<Exception>(() => sendTask);
+ Assert.Equal("OMG;Description=SoBad;Uri=foobar", error.GetBaseException().Message);
+ }
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("CustomIssuer")]
+ public async Task ReplyPathWillAuthenticateValidAuthorizeCodeAndState(string claimsIssuer)
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.SaveTokens = true;
+ o.StateDataFormat = stateFormat;
+ if (claimsIssuer != null)
+ {
+ o.ClaimsIssuer = claimsIssuer;
+ }
+ o.BackchannelHttpHandler = new TestHttpMessageHandler
+ {
+ Sender = req =>
+ {
+ if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token")
+ {
+ return ReturnJsonResponse(new
+ {
+ access_token = "Test Access Token",
+ expires_in = 3600,
+ token_type = "Bearer"
+ });
+ }
+ else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me")
+ {
+ return ReturnJsonResponse(new
+ {
+ id = "Test User ID",
+ displayName = "Test Name",
+ name = new
+ {
+ familyName = "Test Family Name",
+ givenName = "Test Given Name"
+ },
+ url = "Profile link",
+ emails = new[]
+ {
+ new
+ {
+ value = "Test email",
+ type = "account"
+ }
+ }
+ });
+ }
+
+ throw new NotImplementedException(req.RequestUri.AbsoluteUri);
+ }
+ };
+ });
+
+ var properties = new AuthenticationProperties();
+ var correlationKey = ".xsrf";
+ var correlationValue = "TestCorrelationId";
+ properties.Items.Add(correlationKey, correlationValue);
+ properties.RedirectUri = "/me";
+ var state = stateFormat.Protect(properties);
+ var transaction = await server.SendAsync(
+ "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
+ $".AspNetCore.Correlation.Google.{correlationValue}=N");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
+ Assert.Equal(2, transaction.SetCookie.Count);
+ Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]);
+ Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
+
+ var authCookie = transaction.AuthenticationCookieValue;
+ transaction = await server.SendAsync("https://example.com/me", authCookie);
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ var expectedIssuer = claimsIssuer ?? GoogleDefaults.AuthenticationScheme;
+ Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name, expectedIssuer));
+ Assert.Equal("Test User ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier, expectedIssuer));
+ Assert.Equal("Test Given Name", transaction.FindClaimValue(ClaimTypes.GivenName, expectedIssuer));
+ Assert.Equal("Test Family Name", transaction.FindClaimValue(ClaimTypes.Surname, expectedIssuer));
+ Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email, expectedIssuer));
+
+ // Ensure claims transformation
+ Assert.Equal("yup", transaction.FindClaimValue("xform"));
+
+ transaction = await server.SendAsync("https://example.com/tokens", authCookie);
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ Assert.Equal("Test Access Token", transaction.FindTokenValue("access_token"));
+ Assert.Equal("Bearer", transaction.FindTokenValue("token_type"));
+ Assert.NotNull(transaction.FindTokenValue("expires_at"));
+ }
+
+ // REVIEW: Fix this once we revisit error handling to not blow up
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ReplyPathWillThrowIfCodeIsInvalid(bool redirect)
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.StateDataFormat = stateFormat;
+ o.BackchannelHttpHandler = new TestHttpMessageHandler
+ {
+ Sender = req =>
+ {
+ return ReturnJsonResponse(new { Error = "Error" },
+ HttpStatusCode.BadRequest);
+ }
+ };
+ o.Events = redirect ? new OAuthEvents()
+ {
+ OnRemoteFailure = ctx =>
+ {
+ ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message));
+ ctx.HandleResponse();
+ return Task.FromResult(0);
+ }
+ } : new OAuthEvents();
+ });
+ var properties = new AuthenticationProperties();
+ var correlationKey = ".xsrf";
+ var correlationValue = "TestCorrelationId";
+ properties.Items.Add(correlationKey, correlationValue);
+ properties.RedirectUri = "/me";
+
+ var state = stateFormat.Protect(properties);
+ var sendTask = server.SendAsync(
+ "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
+ $".AspNetCore.Correlation.Google.{correlationValue}=N");
+ if (redirect)
+ {
+ var transaction = await sendTask;
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};"),
+ transaction.Response.Headers.GetValues("Location").First());
+ }
+ else
+ {
+ var error = await Assert.ThrowsAnyAsync<Exception>(() => sendTask);
+ Assert.Equal("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};", error.GetBaseException().Message);
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ReplyPathWillRejectIfAccessTokenIsMissing(bool redirect)
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.StateDataFormat = stateFormat;
+ o.BackchannelHttpHandler = new TestHttpMessageHandler
+ {
+ Sender = req =>
+ {
+ return ReturnJsonResponse(new object());
+ }
+ };
+ o.Events = redirect ? new OAuthEvents()
+ {
+ OnRemoteFailure = ctx =>
+ {
+ ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message));
+ ctx.HandleResponse();
+ return Task.FromResult(0);
+ }
+ } : new OAuthEvents();
+ });
+ var properties = new AuthenticationProperties();
+ var correlationKey = ".xsrf";
+ var correlationValue = "TestCorrelationId";
+ properties.Items.Add(correlationKey, correlationValue);
+ properties.RedirectUri = "/me";
+ var state = stateFormat.Protect(properties);
+ var sendTask = server.SendAsync(
+ "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
+ $".AspNetCore.Correlation.Google.{correlationValue}=N");
+ if (redirect)
+ {
+ var transaction = await sendTask;
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("Failed to retrieve access token."),
+ transaction.Response.Headers.GetValues("Location").First());
+ }
+ else
+ {
+ var error = await Assert.ThrowsAnyAsync<Exception>(() => sendTask);
+ Assert.Equal("Failed to retrieve access token.", error.GetBaseException().Message);
+ }
+ }
+
+ [Fact]
+ public async Task AuthenticatedEventCanGetRefreshToken()
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.StateDataFormat = stateFormat;
+ o.BackchannelHttpHandler = new TestHttpMessageHandler
+ {
+ Sender = req =>
+ {
+ if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token")
+ {
+ return ReturnJsonResponse(new
+ {
+ access_token = "Test Access Token",
+ expires_in = 3600,
+ token_type = "Bearer",
+ refresh_token = "Test Refresh Token"
+ });
+ }
+ else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me")
+ {
+ return ReturnJsonResponse(new
+ {
+ id = "Test User ID",
+ displayName = "Test Name",
+ name = new
+ {
+ familyName = "Test Family Name",
+ givenName = "Test Given Name"
+ },
+ url = "Profile link",
+ emails = new[]
+ {
+ new
+ {
+ value = "Test email",
+ type = "account"
+ }
+ }
+ });
+ }
+
+ throw new NotImplementedException(req.RequestUri.AbsoluteUri);
+ }
+ };
+ o.Events = new OAuthEvents
+ {
+ OnCreatingTicket = context =>
+ {
+ var refreshToken = context.RefreshToken;
+ context.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "Google") }, "Google"));
+ return Task.FromResult(0);
+ }
+ };
+ });
+ var properties = new AuthenticationProperties();
+ var correlationKey = ".xsrf";
+ var correlationValue = "TestCorrelationId";
+ properties.Items.Add(correlationKey, correlationValue);
+ properties.RedirectUri = "/me";
+ var state = stateFormat.Protect(properties);
+ var transaction = await server.SendAsync(
+ "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
+ $".AspNetCore.Correlation.Google.{correlationValue}=N");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
+ Assert.Equal(2, transaction.SetCookie.Count);
+ Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]);
+ Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
+
+ var authCookie = transaction.AuthenticationCookieValue;
+ transaction = await server.SendAsync("https://example.com/me", authCookie);
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ Assert.Equal("Test Refresh Token", transaction.FindClaimValue("RefreshToken"));
+ }
+
+ [Fact]
+ public async Task NullRedirectUriWillRedirectToSlash()
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.StateDataFormat = stateFormat;
+ o.BackchannelHttpHandler = new TestHttpMessageHandler
+ {
+ Sender = req =>
+ {
+ if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token")
+ {
+ return ReturnJsonResponse(new
+ {
+ access_token = "Test Access Token",
+ expires_in = 3600,
+ token_type = "Bearer",
+ refresh_token = "Test Refresh Token"
+ });
+ }
+ else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me")
+ {
+ return ReturnJsonResponse(new
+ {
+ id = "Test User ID",
+ displayName = "Test Name",
+ name = new
+ {
+ familyName = "Test Family Name",
+ givenName = "Test Given Name"
+ },
+ url = "Profile link",
+ emails = new[]
+ {
+ new
+ {
+ value = "Test email",
+ type = "account"
+ }
+ }
+ });
+ }
+
+ throw new NotImplementedException(req.RequestUri.AbsoluteUri);
+ }
+ };
+ o.Events = new OAuthEvents
+ {
+ OnTicketReceived = context =>
+ {
+ context.Properties.RedirectUri = null;
+ return Task.FromResult(0);
+ }
+ };
+ });
+ var properties = new AuthenticationProperties();
+ var correlationKey = ".xsrf";
+ var correlationValue = "TestCorrelationId";
+ properties.Items.Add(correlationKey, correlationValue);
+ var state = stateFormat.Protect(properties);
+ var transaction = await server.SendAsync(
+ "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
+ $".AspNetCore.Correlation.Google.{correlationValue}=N");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/", transaction.Response.Headers.GetValues("Location").First());
+ Assert.Equal(2, transaction.SetCookie.Count);
+ Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]);
+ Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
+ }
+
+ [Fact]
+ public async Task ValidateAuthenticatedContext()
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.StateDataFormat = stateFormat;
+ o.AccessType = "offline";
+ o.Events = new OAuthEvents()
+ {
+ OnCreatingTicket = context =>
+ {
+ Assert.NotNull(context.User);
+ Assert.Equal("Test Access Token", context.AccessToken);
+ Assert.Equal("Test Refresh Token", context.RefreshToken);
+ Assert.Equal(TimeSpan.FromSeconds(3600), context.ExpiresIn);
+ Assert.Equal("Test email", context.Identity.FindFirst(ClaimTypes.Email)?.Value);
+ Assert.Equal("Test User ID", context.Identity.FindFirst(ClaimTypes.NameIdentifier)?.Value);
+ Assert.Equal("Test Name", context.Identity.FindFirst(ClaimTypes.Name)?.Value);
+ Assert.Equal("Test Family Name", context.Identity.FindFirst(ClaimTypes.Surname)?.Value);
+ Assert.Equal("Test Given Name", context.Identity.FindFirst(ClaimTypes.GivenName)?.Value);
+ return Task.FromResult(0);
+ }
+ };
+ o.BackchannelHttpHandler = new TestHttpMessageHandler
+ {
+ Sender = req =>
+ {
+ if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token")
+ {
+ return ReturnJsonResponse(new
+ {
+ access_token = "Test Access Token",
+ expires_in = 3600,
+ token_type = "Bearer",
+ refresh_token = "Test Refresh Token"
+ });
+ }
+ else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me")
+ {
+ return ReturnJsonResponse(new
+ {
+ id = "Test User ID",
+ displayName = "Test Name",
+ name = new
+ {
+ familyName = "Test Family Name",
+ givenName = "Test Given Name"
+ },
+ url = "Profile link",
+ emails = new[]
+ {
+ new
+ {
+ value = "Test email",
+ type = "account"
+ }
+ }
+ });
+ }
+
+ throw new NotImplementedException(req.RequestUri.AbsoluteUri);
+ }
+ };
+ });
+
+ var properties = new AuthenticationProperties();
+ var correlationKey = ".xsrf";
+ var correlationValue = "TestCorrelationId";
+ properties.Items.Add(correlationKey, correlationValue);
+ properties.RedirectUri = "/foo";
+ var state = stateFormat.Protect(properties);
+
+ //Post a message to the Google middleware
+ var transaction = await server.SendAsync(
+ "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
+ $".AspNetCore.Correlation.Google.{correlationValue}=N");
+
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/foo", transaction.Response.Headers.GetValues("Location").First());
+ }
+
+ [Fact]
+ public async Task NoStateCausesException()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ });
+
+ //Post a message to the Google middleware
+ var error = await Assert.ThrowsAnyAsync<Exception>(() => server.SendAsync("https://example.com/signin-google?code=TestCode"));
+ Assert.Equal("The oauth state was missing or invalid.", error.GetBaseException().Message);
+ }
+
+ [Fact]
+ public async Task CanRedirectOnError()
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.Events = new OAuthEvents()
+ {
+ OnRemoteFailure = ctx =>
+ {
+ ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message));
+ ctx.HandleResponse();
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ //Post a message to the Google middleware
+ var transaction = await server.SendAsync(
+ "https://example.com/signin-google?code=TestCode");
+
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("The oauth state was missing or invalid."),
+ transaction.Response.Headers.GetValues("Location").First());
+ }
+
+ [Fact]
+ public async Task AuthenticateAutomaticWhenAlreadySignedInSucceeds()
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.StateDataFormat = stateFormat;
+ o.SaveTokens = true;
+ o.BackchannelHttpHandler = CreateBackchannel();
+ });
+
+ // Skip the challenge step, go directly to the callback path
+
+ var properties = new AuthenticationProperties();
+ var correlationKey = ".xsrf";
+ var correlationValue = "TestCorrelationId";
+ properties.Items.Add(correlationKey, correlationValue);
+ properties.RedirectUri = "/me";
+ var state = stateFormat.Protect(properties);
+ var transaction = await server.SendAsync(
+ "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
+ $".AspNetCore.Correlation.Google.{correlationValue}=N");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
+ Assert.Equal(2, transaction.SetCookie.Count);
+ Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete
+ Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
+
+ var authCookie = transaction.AuthenticationCookieValue;
+ transaction = await server.SendAsync("https://example.com/authenticate", authCookie);
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name));
+ Assert.Equal("Test User ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier));
+ Assert.Equal("Test Given Name", transaction.FindClaimValue(ClaimTypes.GivenName));
+ Assert.Equal("Test Family Name", transaction.FindClaimValue(ClaimTypes.Surname));
+ Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email));
+
+ // Ensure claims transformation
+ Assert.Equal("yup", transaction.FindClaimValue("xform"));
+ }
+
+ [Fact]
+ public async Task AuthenticateGoogleWhenAlreadySignedInSucceeds()
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.StateDataFormat = stateFormat;
+ o.SaveTokens = true;
+ o.BackchannelHttpHandler = CreateBackchannel();
+ });
+
+ // Skip the challenge step, go directly to the callback path
+
+ var properties = new AuthenticationProperties();
+ var correlationKey = ".xsrf";
+ var correlationValue = "TestCorrelationId";
+ properties.Items.Add(correlationKey, correlationValue);
+ properties.RedirectUri = "/me";
+ var state = stateFormat.Protect(properties);
+ var transaction = await server.SendAsync(
+ "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
+ $".AspNetCore.Correlation.Google.{correlationValue}=N");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
+ Assert.Equal(2, transaction.SetCookie.Count);
+ Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete
+ Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
+
+ var authCookie = transaction.AuthenticationCookieValue;
+ transaction = await server.SendAsync("https://example.com/authenticateGoogle", authCookie);
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name));
+ Assert.Equal("Test User ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier));
+ Assert.Equal("Test Given Name", transaction.FindClaimValue(ClaimTypes.GivenName));
+ Assert.Equal("Test Family Name", transaction.FindClaimValue(ClaimTypes.Surname));
+ Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email));
+
+ // Ensure claims transformation
+ Assert.Equal("yup", transaction.FindClaimValue("xform"));
+ }
+
+ [Fact]
+ public async Task AuthenticateFacebookWhenAlreadySignedWithGoogleReturnsNull()
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.StateDataFormat = stateFormat;
+ o.SaveTokens = true;
+ o.BackchannelHttpHandler = CreateBackchannel();
+ });
+
+ // Skip the challenge step, go directly to the callback path
+
+ var properties = new AuthenticationProperties();
+ var correlationKey = ".xsrf";
+ var correlationValue = "TestCorrelationId";
+ properties.Items.Add(correlationKey, correlationValue);
+ properties.RedirectUri = "/me";
+ var state = stateFormat.Protect(properties);
+ var transaction = await server.SendAsync(
+ "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
+ $".AspNetCore.Correlation.Google.{correlationValue}=N");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
+ Assert.Equal(2, transaction.SetCookie.Count);
+ Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete
+ Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
+
+ var authCookie = transaction.AuthenticationCookieValue;
+ transaction = await server.SendAsync("https://example.com/authenticateFacebook", authCookie);
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ Assert.Null(transaction.FindClaimValue(ClaimTypes.Name));
+ }
+
+ [Fact]
+ public async Task ChallengeFacebookWhenAlreadySignedWithGoogleSucceeds()
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.StateDataFormat = stateFormat;
+ o.SaveTokens = true;
+ o.BackchannelHttpHandler = CreateBackchannel();
+ });
+
+ // Skip the challenge step, go directly to the callback path
+
+ var properties = new AuthenticationProperties();
+ var correlationKey = ".xsrf";
+ var correlationValue = "TestCorrelationId";
+ properties.Items.Add(correlationKey, correlationValue);
+ properties.RedirectUri = "/me";
+ var state = stateFormat.Protect(properties);
+ var transaction = await server.SendAsync(
+ "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
+ $".AspNetCore.Correlation.Google.{correlationValue}=N");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
+ Assert.Equal(2, transaction.SetCookie.Count);
+ Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete
+ Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
+
+ var authCookie = transaction.AuthenticationCookieValue;
+ transaction = await server.SendAsync("https://example.com/challengeFacebook", authCookie);
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.StartsWith("https://www.facebook.com/", transaction.Response.Headers.Location.OriginalString);
+ }
+
+ private HttpMessageHandler CreateBackchannel()
+ {
+ return new TestHttpMessageHandler()
+ {
+ Sender = req =>
+ {
+ if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token")
+ {
+ return ReturnJsonResponse(new
+ {
+ access_token = "Test Access Token",
+ expires_in = 3600,
+ token_type = "Bearer"
+ });
+ }
+ else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me")
+ {
+ return ReturnJsonResponse(new
+ {
+ id = "Test User ID",
+ displayName = "Test Name",
+ name = new
+ {
+ familyName = "Test Family Name",
+ givenName = "Test Given Name"
+ },
+ url = "Profile link",
+ emails = new[]
+ {
+ new
+ {
+ value = "Test email",
+ type = "account"
+ }
+ }
+ });
+ }
+
+ throw new NotImplementedException(req.RequestUri.AbsoluteUri);
+ }
+ };
+ }
+
+ private static HttpResponseMessage ReturnJsonResponse(object content, HttpStatusCode code = HttpStatusCode.OK)
+ {
+ var res = new HttpResponseMessage(code);
+ var text = JsonConvert.SerializeObject(content);
+ res.Content = new StringContent(text, Encoding.UTF8, "application/json");
+ return res;
+ }
+
+ private class ClaimsTransformer : IClaimsTransformation
+ {
+ public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal p)
+ {
+ if (!p.Identities.Any(i => i.AuthenticationType == "xform"))
+ {
+ var id = new ClaimsIdentity("xform");
+ id.AddClaim(new Claim("xform", "yup"));
+ p.AddIdentity(id);
+ }
+ return Task.FromResult(p);
+ }
+ }
+
+ private static TestServer CreateServer(Action<GoogleOptions> configureOptions, Func<HttpContext, Task> testpath = null)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Use(async (context, next) =>
+ {
+ var req = context.Request;
+ var res = context.Response;
+ if (req.Path == new PathString("/challenge"))
+ {
+ await context.ChallengeAsync();
+ }
+ else if (req.Path == new PathString("/challengeFacebook"))
+ {
+ await context.ChallengeAsync("Facebook");
+ }
+ else if (req.Path == new PathString("/tokens"))
+ {
+ var result = await context.AuthenticateAsync(TestExtensions.CookieAuthenticationScheme);
+ var tokens = result.Properties.GetTokens();
+ res.Describe(tokens);
+ }
+ else if (req.Path == new PathString("/me"))
+ {
+ res.Describe(context.User);
+ }
+ else if (req.Path == new PathString("/authenticate"))
+ {
+ var result = await context.AuthenticateAsync(TestExtensions.CookieAuthenticationScheme);
+ res.Describe(result.Principal);
+ }
+ else if (req.Path == new PathString("/authenticateGoogle"))
+ {
+ var result = await context.AuthenticateAsync("Google");
+ res.Describe(result?.Principal);
+ }
+ else if (req.Path == new PathString("/authenticateFacebook"))
+ {
+ var result = await context.AuthenticateAsync("Facebook");
+ res.Describe(result?.Principal);
+ }
+ else if (req.Path == new PathString("/unauthorized"))
+ {
+ // Simulate Authorization failure
+ var result = await context.AuthenticateAsync("Google");
+ await context.ChallengeAsync("Google");
+ }
+ else if (req.Path == new PathString("/unauthorizedAuto"))
+ {
+ var result = await context.AuthenticateAsync("Google");
+ await context.ChallengeAsync("Google");
+ }
+ else if (req.Path == new PathString("/401"))
+ {
+ res.StatusCode = 401;
+ }
+ else if (req.Path == new PathString("/signIn"))
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync("Google", new ClaimsPrincipal()));
+ }
+ else if (req.Path == new PathString("/signOut"))
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync("Google"));
+ }
+ else if (req.Path == new PathString("/forbid"))
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.ForbidAsync("Google"));
+ }
+ else if (testpath != null)
+ {
+ await testpath(context);
+ }
+ else
+ {
+ await next();
+ }
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
+ services.AddAuthentication(TestExtensions.CookieAuthenticationScheme)
+ .AddCookie(TestExtensions.CookieAuthenticationScheme, o => o.ForwardChallenge = GoogleDefaults.AuthenticationScheme)
+ .AddGoogle(configureOptions)
+ .AddFacebook(o =>
+ {
+ o.ClientId = "Test ClientId";
+ o.ClientSecret = "Test AppSecrent";
+ });
+ });
+ return new TestServer(builder);
+ }
+
+ private class TestStateDataFormat : ISecureDataFormat<AuthenticationProperties>
+ {
+ private AuthenticationProperties Data { get; set; }
+
+ public string Protect(AuthenticationProperties data)
+ {
+ return "protected_state";
+ }
+
+ public string Protect(AuthenticationProperties data, string purpose)
+ {
+ throw new NotImplementedException();
+ }
+
+ public AuthenticationProperties Unprotect(string protectedText)
+ {
+ Assert.Equal("protected_state", protectedText);
+ var properties = new AuthenticationProperties(new Dictionary<string, string>()
+ {
+ { ".xsrf", "corrilationId" },
+ { "testkey", "testvalue" }
+ });
+ properties.RedirectUri = "http://testhost/redirect";
+ return properties;
+ }
+
+ public AuthenticationProperties Unprotect(string protectedText, string purpose)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs
new file mode 100644
index 0000000000..d7fcdb4cad
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs
@@ -0,0 +1,1237 @@
+// 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.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Reflection;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Authentication.Tests;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.AspNetCore.Testing.xunit;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.IdentityModel.Tokens;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer
+{
+ public class JwtBearerTests
+ {
+ private void ConfigureDefaults(JwtBearerOptions o)
+ {
+ }
+
+ [Fact]
+ public async Task CanForwardDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ })
+ .AddJwtBearer(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ });
+
+ var forwardDefault = new TestHandler();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignInThrows()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddJwtBearer(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignOutThrows()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddJwtBearer(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ }
+
+ [Fact]
+ public async Task ForwardForbidWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
+ o.DefaultSignInScheme = "auth1";
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddJwtBearer(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ForbidAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(1, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardAuthenticateWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
+ o.DefaultSignInScheme = "auth1";
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddJwtBearer(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardAuthenticate = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(1, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardChallengeWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
+ o.DefaultSignInScheme = "auth1";
+ o.AddScheme<TestHandler>("specific", "specific");
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ })
+ .AddJwtBearer(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardChallenge = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ChallengeAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(1, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardSelectorWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddJwtBearer(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, selector.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, selector.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, selector.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task NullForwardSelectorUsesDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddJwtBearer(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => null;
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task SpecificForwardWinsOverSelectorAndDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddJwtBearer(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ o.ForwardAuthenticate = "specific";
+ o.ForwardChallenge = "specific";
+ o.ForwardSignIn = "specific";
+ o.ForwardSignOut = "specific";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, specific.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, specific.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, specific.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ }
+
+ [Fact]
+ public async Task VerifySchemeDefaults()
+ {
+ var services = new ServiceCollection();
+ services.AddAuthentication().AddJwtBearer();
+ var sp = services.BuildServiceProvider();
+ var schemeProvider = sp.GetRequiredService<IAuthenticationSchemeProvider>();
+ var scheme = await schemeProvider.GetSchemeAsync(JwtBearerDefaults.AuthenticationScheme);
+ Assert.NotNull(scheme);
+ Assert.Equal("JwtBearerHandler", scheme.HandlerType.Name);
+ Assert.Null(scheme.DisplayName);
+ }
+
+ [Fact]
+ public async Task BearerTokenValidation()
+ {
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128)));
+ var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
+
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.NameIdentifier, "Bob")
+ };
+
+ var token = new JwtSecurityToken(
+ issuer: "issuer.contoso.com",
+ audience: "audience.contoso.com",
+ claims: claims,
+ expires: DateTime.Now.AddMinutes(30),
+ signingCredentials: creds);
+
+ var tokenText = new JwtSecurityTokenHandler().WriteToken(token);
+
+ var server = CreateServer(o =>
+ {
+ o.TokenValidationParameters = new TokenValidationParameters()
+ {
+ ValidIssuer = "issuer.contoso.com",
+ ValidAudience = "audience.contoso.com",
+ IssuerSigningKey = key,
+ };
+ });
+
+ var newBearerToken = "Bearer " + tokenText;
+ var response = await SendAsync(server, "http://example.com/oauth", newBearerToken);
+ Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task SaveBearerToken()
+ {
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128)));
+ var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
+
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.NameIdentifier, "Bob")
+ };
+
+ var token = new JwtSecurityToken(
+ issuer: "issuer.contoso.com",
+ audience: "audience.contoso.com",
+ claims: claims,
+ expires: DateTime.Now.AddMinutes(30),
+ signingCredentials: creds);
+
+ var tokenText = new JwtSecurityTokenHandler().WriteToken(token);
+
+ var server = CreateServer(o =>
+ {
+ o.SaveToken = true;
+ o.TokenValidationParameters = new TokenValidationParameters()
+ {
+ ValidIssuer = "issuer.contoso.com",
+ ValidAudience = "audience.contoso.com",
+ IssuerSigningKey = key,
+ };
+ });
+
+ var newBearerToken = "Bearer " + tokenText;
+ var response = await SendAsync(server, "http://example.com/token", newBearerToken);
+ Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode);
+ Assert.Equal(tokenText, await response.Response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task SignInThrows()
+ {
+ var server = CreateServer();
+ var transaction = await server.SendAsync("https://example.com/signIn");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task SignOutThrows()
+ {
+ var server = CreateServer();
+ var transaction = await server.SendAsync("https://example.com/signOut");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ThrowAtAuthenticationFailedEvent()
+ {
+ var server = CreateServer(o =>
+ {
+ o.Events = new JwtBearerEvents
+ {
+ OnAuthenticationFailed = context =>
+ {
+ context.Response.StatusCode = 401;
+ throw new Exception();
+ },
+ OnMessageReceived = context =>
+ {
+ context.Token = "something";
+ return Task.FromResult(0);
+ }
+ };
+ o.SecurityTokenValidators.Clear();
+ o.SecurityTokenValidators.Insert(0, new InvalidTokenValidator());
+ },
+ async (context, next) =>
+ {
+ try
+ {
+ await next();
+ Assert.False(true, "Expected exception is not thrown");
+ }
+ catch (Exception)
+ {
+ context.Response.StatusCode = 401;
+ await context.Response.WriteAsync("i got this");
+ }
+ });
+
+ var transaction = await server.SendAsync("https://example.com/signIn");
+
+ Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task CustomHeaderReceived()
+ {
+ var server = CreateServer(o =>
+ {
+ o.Events = new JwtBearerEvents()
+ {
+ OnMessageReceived = context =>
+ {
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"),
+ new Claim(ClaimTypes.Email, "bob@contoso.com"),
+ new Claim(ClaimsIdentity.DefaultNameClaimType, "bob")
+ };
+
+ context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
+ context.Success();
+
+ return Task.FromResult<object>(null);
+ }
+ };
+ });
+
+ var response = await SendAsync(server, "http://example.com/oauth", "someHeader someblob");
+ Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode);
+ Assert.Equal("Bob le Magnifique", response.ResponseText);
+ }
+
+ [Fact]
+ public async Task NoHeaderReceived()
+ {
+ var server = CreateServer();
+ var response = await SendAsync(server, "http://example.com/oauth");
+ Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task HeaderWithoutBearerReceived()
+ {
+ var server = CreateServer();
+ var response = await SendAsync(server, "http://example.com/oauth", "Token");
+ Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task UnrecognizedTokenReceived()
+ {
+ var server = CreateServer();
+ var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
+ Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
+ Assert.Equal("", response.ResponseText);
+ }
+
+ [Fact]
+ public async Task InvalidTokenReceived()
+ {
+ var server = CreateServer(options =>
+ {
+ options.SecurityTokenValidators.Clear();
+ options.SecurityTokenValidators.Add(new InvalidTokenValidator());
+ });
+
+ var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
+ Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
+ Assert.Equal("Bearer error=\"invalid_token\"", response.Response.Headers.WwwAuthenticate.First().ToString());
+ Assert.Equal("", response.ResponseText);
+ }
+
+ [Theory]
+ [InlineData(typeof(SecurityTokenInvalidAudienceException), "The audience is invalid")]
+ [InlineData(typeof(SecurityTokenInvalidIssuerException), "The issuer is invalid")]
+ [InlineData(typeof(SecurityTokenNoExpirationException), "The token has no expiration")]
+ [InlineData(typeof(SecurityTokenInvalidLifetimeException), "The token lifetime is invalid")]
+ [InlineData(typeof(SecurityTokenNotYetValidException), "The token is not valid yet")]
+ [InlineData(typeof(SecurityTokenExpiredException), "The token is expired")]
+ [InlineData(typeof(SecurityTokenInvalidSignatureException), "The signature is invalid")]
+ [InlineData(typeof(SecurityTokenSignatureKeyNotFoundException), "The signature key was not found")]
+ public async Task ExceptionReportedInHeaderForAuthenticationFailures(Type errorType, string message)
+ {
+ var server = CreateServer(options =>
+ {
+ options.SecurityTokenValidators.Clear();
+ options.SecurityTokenValidators.Add(new InvalidTokenValidator(errorType));
+ });
+
+ var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
+ Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
+ Assert.Equal($"Bearer error=\"invalid_token\", error_description=\"{message}\"", response.Response.Headers.WwwAuthenticate.First().ToString());
+ Assert.Equal("", response.ResponseText);
+ }
+
+ [Theory]
+ [InlineData(typeof(ArgumentException))]
+ public async Task ExceptionNotReportedInHeaderForOtherFailures(Type errorType)
+ {
+ var server = CreateServer(options =>
+ {
+ options.SecurityTokenValidators.Clear();
+ options.SecurityTokenValidators.Add(new InvalidTokenValidator(errorType));
+ });
+
+ var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
+ Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
+ Assert.Equal("Bearer error=\"invalid_token\"", response.Response.Headers.WwwAuthenticate.First().ToString());
+ Assert.Equal("", response.ResponseText);
+ }
+
+ [Fact]
+ public async Task ExceptionsReportedInHeaderForMultipleAuthenticationFailures()
+ {
+ var server = CreateServer(options =>
+ {
+ options.SecurityTokenValidators.Clear();
+ options.SecurityTokenValidators.Add(new InvalidTokenValidator(typeof(SecurityTokenInvalidAudienceException)));
+ options.SecurityTokenValidators.Add(new InvalidTokenValidator(typeof(SecurityTokenSignatureKeyNotFoundException)));
+ });
+
+ var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
+ Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
+ Assert.Equal("Bearer error=\"invalid_token\", error_description=\"The audience is invalid; The signature key was not found\"",
+ response.Response.Headers.WwwAuthenticate.First().ToString());
+ Assert.Equal("", response.ResponseText);
+ }
+
+ [Theory]
+ [InlineData("custom_error", "custom_description", "custom_uri")]
+ [InlineData("custom_error", "custom_description", null)]
+ [InlineData("custom_error", null, null)]
+ [InlineData(null, "custom_description", "custom_uri")]
+ [InlineData(null, "custom_description", null)]
+ [InlineData(null, null, "custom_uri")]
+ public async Task ExceptionsReportedInHeaderExposesUserDefinedError(string error, string description, string uri)
+ {
+ var server = CreateServer(options =>
+ {
+ options.Events = new JwtBearerEvents
+ {
+ OnChallenge = context =>
+ {
+ context.Error = error;
+ context.ErrorDescription = description;
+ context.ErrorUri = uri;
+
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
+ Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
+ Assert.Equal("", response.ResponseText);
+
+ var builder = new StringBuilder(JwtBearerDefaults.AuthenticationScheme);
+
+ if (!string.IsNullOrEmpty(error))
+ {
+ builder.Append(" error=\"");
+ builder.Append(error);
+ builder.Append("\"");
+ }
+ if (!string.IsNullOrEmpty(description))
+ {
+ if (!string.IsNullOrEmpty(error))
+ {
+ builder.Append(",");
+ }
+
+ builder.Append(" error_description=\"");
+ builder.Append(description);
+ builder.Append('\"');
+ }
+ if (!string.IsNullOrEmpty(uri))
+ {
+ if (!string.IsNullOrEmpty(error) ||
+ !string.IsNullOrEmpty(description))
+ {
+ builder.Append(",");
+ }
+
+ builder.Append(" error_uri=\"");
+ builder.Append(uri);
+ builder.Append('\"');
+ }
+
+ Assert.Equal(builder.ToString(), response.Response.Headers.WwwAuthenticate.First().ToString());
+ }
+
+ [Fact]
+ public async Task ExceptionNotReportedInHeaderWhenIncludeErrorDetailsIsFalse()
+ {
+ var server = CreateServer(o =>
+ {
+ o.IncludeErrorDetails = false;
+ });
+
+ var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
+ Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
+ Assert.Equal("Bearer", response.Response.Headers.WwwAuthenticate.First().ToString());
+ Assert.Equal("", response.ResponseText);
+ }
+
+ [Fact]
+ public async Task ExceptionNotReportedInHeaderWhenTokenWasMissing()
+ {
+ var server = CreateServer();
+
+ var response = await SendAsync(server, "http://example.com/oauth");
+ Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
+ Assert.Equal("Bearer", response.Response.Headers.WwwAuthenticate.First().ToString());
+ Assert.Equal("", response.ResponseText);
+ }
+
+ [Fact]
+ public async Task CustomTokenValidated()
+ {
+ var server = CreateServer(options =>
+ {
+ options.Events = new JwtBearerEvents()
+ {
+ OnTokenValidated = context =>
+ {
+ // Retrieve the NameIdentifier claim from the identity
+ // returned by the custom security token validator.
+ var identity = (ClaimsIdentity)context.Principal.Identity;
+ var identifier = identity.FindFirst(ClaimTypes.NameIdentifier);
+
+ Assert.Equal("Bob le Tout Puissant", identifier.Value);
+
+ // Remove the existing NameIdentifier claim and replace it
+ // with a new one containing a different value.
+ identity.RemoveClaim(identifier);
+ // Make sure to use a different name identifier
+ // than the one defined by BlobTokenValidator.
+ identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"));
+
+ return Task.FromResult<object>(null);
+ }
+ };
+ options.SecurityTokenValidators.Clear();
+ options.SecurityTokenValidators.Add(new BlobTokenValidator(JwtBearerDefaults.AuthenticationScheme));
+ });
+
+ var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
+ Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode);
+ Assert.Equal("Bob le Magnifique", response.ResponseText);
+ }
+
+ [Fact]
+ public async Task RetrievingTokenFromAlternateLocation()
+ {
+ var server = CreateServer(options =>
+ {
+ options.Events = new JwtBearerEvents()
+ {
+ OnMessageReceived = context =>
+ {
+ context.Token = "CustomToken";
+ return Task.FromResult<object>(null);
+ }
+ };
+ options.SecurityTokenValidators.Clear();
+ options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT", token =>
+ {
+ Assert.Equal("CustomToken", token);
+ }));
+ });
+
+ var response = await SendAsync(server, "http://example.com/oauth", "Bearer Token");
+ Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode);
+ Assert.Equal("Bob le Tout Puissant", response.ResponseText);
+ }
+
+ [Fact]
+ public async Task EventOnMessageReceivedSkip_NoMoreEventsExecuted()
+ {
+ var server = CreateServer(options =>
+ {
+ options.Events = new JwtBearerEvents()
+ {
+ OnMessageReceived = context =>
+ {
+ context.NoResult();
+ return Task.FromResult(0);
+ },
+ OnTokenValidated = context =>
+ {
+ throw new NotImplementedException();
+ },
+ OnAuthenticationFailed = context =>
+ {
+ throw new NotImplementedException(context.Exception.ToString());
+ },
+ OnChallenge = context =>
+ {
+ throw new NotImplementedException();
+ },
+ };
+ });
+
+ var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token");
+ Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode);
+ Assert.Equal(string.Empty, response.ResponseText);
+ }
+
+ [Fact]
+ public async Task EventOnMessageReceivedReject_NoMoreEventsExecuted()
+ {
+ var server = CreateServer(options =>
+ {
+ options.Events = new JwtBearerEvents()
+ {
+ OnMessageReceived = context =>
+ {
+ context.Fail("Authentication was aborted from user code.");
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ },
+ OnTokenValidated = context =>
+ {
+ throw new NotImplementedException();
+ },
+ OnAuthenticationFailed = context =>
+ {
+ throw new NotImplementedException(context.Exception.ToString());
+ },
+ OnChallenge = context =>
+ {
+ throw new NotImplementedException();
+ },
+ };
+ });
+
+ var exception = await Assert.ThrowsAsync<Exception>(delegate
+ {
+ return SendAsync(server, "http://example.com/checkforerrors", "Bearer Token");
+ });
+
+ Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
+ }
+
+ [Fact]
+ public async Task EventOnTokenValidatedSkip_NoMoreEventsExecuted()
+ {
+ var server = CreateServer(options =>
+ {
+ options.Events = new JwtBearerEvents()
+ {
+ OnTokenValidated = context =>
+ {
+ context.NoResult();
+ return Task.FromResult(0);
+ },
+ OnAuthenticationFailed = context =>
+ {
+ throw new NotImplementedException(context.Exception.ToString());
+ },
+ OnChallenge = context =>
+ {
+ throw new NotImplementedException();
+ },
+ };
+ options.SecurityTokenValidators.Clear();
+ options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT"));
+ });
+
+ var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token");
+ Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode);
+ Assert.Equal(string.Empty, response.ResponseText);
+ }
+
+ [Fact]
+ public async Task EventOnTokenValidatedReject_NoMoreEventsExecuted()
+ {
+ var server = CreateServer(options =>
+ {
+ options.Events = new JwtBearerEvents()
+ {
+ OnTokenValidated = context =>
+ {
+ context.Fail("Authentication was aborted from user code.");
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ },
+ OnAuthenticationFailed = context =>
+ {
+ throw new NotImplementedException(context.Exception.ToString());
+ },
+ OnChallenge = context =>
+ {
+ throw new NotImplementedException();
+ },
+ };
+ options.SecurityTokenValidators.Clear();
+ options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT"));
+ });
+
+ var exception = await Assert.ThrowsAsync<Exception>(delegate
+ {
+ return SendAsync(server, "http://example.com/checkforerrors", "Bearer Token");
+ });
+
+ Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
+ }
+
+ [Fact]
+ public async Task EventOnAuthenticationFailedSkip_NoMoreEventsExecuted()
+ {
+ var server = CreateServer(options =>
+ {
+ options.Events = new JwtBearerEvents()
+ {
+ OnTokenValidated = context =>
+ {
+ throw new Exception("Test Exception");
+ },
+ OnAuthenticationFailed = context =>
+ {
+ context.NoResult();
+ return Task.FromResult(0);
+ },
+ OnChallenge = context =>
+ {
+ throw new NotImplementedException();
+ },
+ };
+ options.SecurityTokenValidators.Clear();
+ options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT"));
+ });
+
+ var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token");
+ Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode);
+ Assert.Equal(string.Empty, response.ResponseText);
+ }
+
+ [Fact]
+ public async Task EventOnAuthenticationFailedReject_NoMoreEventsExecuted()
+ {
+ var server = CreateServer(options =>
+ {
+ options.Events = new JwtBearerEvents()
+ {
+ OnTokenValidated = context =>
+ {
+ throw new Exception("Test Exception");
+ },
+ OnAuthenticationFailed = context =>
+ {
+ context.Fail("Authentication was aborted from user code.");
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ },
+ OnChallenge = context =>
+ {
+ throw new NotImplementedException();
+ },
+ };
+ options.SecurityTokenValidators.Clear();
+ options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT"));
+ });
+
+ var exception = await Assert.ThrowsAsync<Exception>(delegate
+ {
+ return SendAsync(server, "http://example.com/checkforerrors", "Bearer Token");
+ });
+
+ Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
+ }
+
+ [Fact]
+ public async Task EventOnChallengeSkip_ResponseNotModified()
+ {
+ var server = CreateServer(o =>
+ {
+ o.Events = new JwtBearerEvents()
+ {
+ OnChallenge = context =>
+ {
+ context.HandleResponse();
+ return Task.FromResult(0);
+ },
+ };
+ });
+
+ var response = await SendAsync(server, "http://example.com/unauthorized", "Bearer Token");
+ Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode);
+ Assert.Empty(response.Response.Headers.WwwAuthenticate);
+ Assert.Equal(string.Empty, response.ResponseText);
+ }
+
+ class InvalidTokenValidator : ISecurityTokenValidator
+ {
+ public InvalidTokenValidator()
+ {
+ ExceptionType = typeof(SecurityTokenException);
+ }
+
+ public InvalidTokenValidator(Type exceptionType)
+ {
+ ExceptionType = exceptionType;
+ }
+
+ public Type ExceptionType { get; set; }
+
+ public bool CanValidateToken => true;
+
+ public int MaximumTokenSizeInBytes
+ {
+ get { throw new NotImplementedException(); }
+ set { throw new NotImplementedException(); }
+ }
+
+ public bool CanReadToken(string securityToken) => true;
+
+ public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
+ {
+ var constructor = ExceptionType.GetTypeInfo().GetConstructor(new[] { typeof(string) });
+ var exception = (Exception)constructor.Invoke(new[] { ExceptionType.Name });
+ throw exception;
+ }
+ }
+
+ class BlobTokenValidator : ISecurityTokenValidator
+ {
+ private Action<string> _tokenValidator;
+
+ public BlobTokenValidator(string authenticationScheme)
+ {
+ AuthenticationScheme = authenticationScheme;
+
+ }
+ public BlobTokenValidator(string authenticationScheme, Action<string> tokenValidator)
+ {
+ AuthenticationScheme = authenticationScheme;
+ _tokenValidator = tokenValidator;
+ }
+
+ public string AuthenticationScheme { get; }
+
+ public bool CanValidateToken => true;
+
+ public int MaximumTokenSizeInBytes
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ set
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public bool CanReadToken(string securityToken) => true;
+
+ public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
+ {
+ validatedToken = null;
+ _tokenValidator?.Invoke(securityToken);
+
+ var claims = new[]
+ {
+ // Make sure to use a different name identifier
+ // than the one defined by CustomTokenValidated.
+ new Claim(ClaimTypes.NameIdentifier, "Bob le Tout Puissant"),
+ new Claim(ClaimTypes.Email, "bob@contoso.com"),
+ new Claim(ClaimsIdentity.DefaultNameClaimType, "bob"),
+ };
+
+ return new ClaimsPrincipal(new ClaimsIdentity(claims, AuthenticationScheme));
+ }
+ }
+
+ private static TestServer CreateServer(Action<JwtBearerOptions> options = null, Func<HttpContext, Func<Task>, Task> handlerBeforeAuth = null)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ if (handlerBeforeAuth != null)
+ {
+ app.Use(handlerBeforeAuth);
+ }
+
+ app.UseAuthentication();
+ app.Use(async (context, next) =>
+ {
+ if (context.Request.Path == new PathString("/checkforerrors"))
+ {
+ var result = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); // this used to be "Automatic"
+ if (result.Failure != null)
+ {
+ throw new Exception("Failed to authenticate", result.Failure);
+ }
+ return;
+ }
+ else if (context.Request.Path == new PathString("/oauth"))
+ {
+ if (context.User == null ||
+ context.User.Identity == null ||
+ !context.User.Identity.IsAuthenticated)
+ {
+ context.Response.StatusCode = 401;
+ // REVIEW: no more automatic challenge
+ await context.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme);
+ return;
+ }
+
+ var identifier = context.User.FindFirst(ClaimTypes.NameIdentifier);
+ if (identifier == null)
+ {
+ context.Response.StatusCode = 500;
+ return;
+ }
+
+ await context.Response.WriteAsync(identifier.Value);
+ }
+ else if (context.Request.Path == new PathString("/token"))
+ {
+ var token = await context.GetTokenAsync("access_token");
+ await context.Response.WriteAsync(token);
+ }
+ else if (context.Request.Path == new PathString("/unauthorized"))
+ {
+ // Simulate Authorization failure
+ var result = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
+ await context.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme);
+ }
+ else if (context.Request.Path == new PathString("/signIn"))
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(JwtBearerDefaults.AuthenticationScheme, new ClaimsPrincipal()));
+ }
+ else if (context.Request.Path == new PathString("/signOut"))
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync(JwtBearerDefaults.AuthenticationScheme));
+ }
+ else
+ {
+ await next();
+ }
+ });
+ })
+ .ConfigureServices(services => services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options));
+
+ return new TestServer(builder);
+ }
+
+ // TODO: see if we can share the TestExtensions SendAsync method (only diff is auth header)
+ private static async Task<Transaction> SendAsync(TestServer server, string uri, string authorizationHeader = null)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, uri);
+ if (!string.IsNullOrEmpty(authorizationHeader))
+ {
+ request.Headers.Add("Authorization", authorizationHeader);
+ }
+
+ var transaction = new Transaction
+ {
+ Request = request,
+ Response = await server.CreateClient().SendAsync(request),
+ };
+
+ transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync();
+
+ if (transaction.Response.Content != null &&
+ transaction.Response.Content.Headers.ContentType != null &&
+ transaction.Response.Content.Headers.ContentType.MediaType == "text/xml")
+ {
+ transaction.ResponseElement = XElement.Parse(transaction.ResponseText);
+ }
+
+ return transaction;
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj
new file mode 100644
index 0000000000..6c8d518ffa
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj
@@ -0,0 +1,47 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <None Remove="OpenIdConnect\wellknownconfig.json" />
+ <None Remove="OpenIdConnect\wellknownkeys.json" />
+ <None Remove="WsFederation\federationmetadata.xml" />
+ <None Remove="WsFederation\InvalidToken.xml" />
+ <None Remove="WsFederation\ValidToken.xml" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Content Include="WsFederation\federationmetadata.xml">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="WsFederation\InvalidToken.xml">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="WsFederation\ValidToken.xml">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ </ItemGroup>
+
+ <ItemGroup>
+ <EmbeddedResource Include="OpenIdConnect\wellknownconfig.json" />
+ <EmbeddedResource Include="OpenIdConnect\wellknownkeys.json" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Facebook\Microsoft.AspNetCore.Authentication.Facebook.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Google\Microsoft.AspNetCore.Authentication.Google.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.JwtBearer\Microsoft.AspNetCore.Authentication.JwtBearer.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.MicrosoftAccount\Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.OpenIdConnect\Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Twitter\Microsoft.AspNetCore.Authentication.Twitter.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.WsFederation\Microsoft.AspNetCore.Authentication.WsFederation.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs
new file mode 100644
index 0000000000..e2e13f270e
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs
@@ -0,0 +1,713 @@
+// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount
+{
+ public class MicrosoftAccountTests
+ {
+ private void ConfigureDefaults(MicrosoftAccountOptions o)
+ {
+ o.ClientId = "whatever";
+ o.ClientSecret = "whatever";
+ o.SignInScheme = "auth1";
+ }
+
+ [Fact]
+ public async Task CanForwardDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ })
+ .AddMicrosoftAccount(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ });
+
+ var forwardDefault = new TestHandler();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignInThrows()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddMicrosoftAccount(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignOutThrows()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddMicrosoftAccount(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ }
+
+ [Fact]
+ public async Task ForwardForbidWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddMicrosoftAccount(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ForbidAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(1, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardAuthenticateWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddMicrosoftAccount(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardAuthenticate = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(1, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardChallengeWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler>("specific", "specific");
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ })
+ .AddMicrosoftAccount(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardChallenge = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ChallengeAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(1, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardSelectorWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddMicrosoftAccount(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, selector.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, selector.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, selector.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task NullForwardSelectorUsesDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddMicrosoftAccount(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => null;
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task SpecificForwardWinsOverSelectorAndDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = MicrosoftAccountDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddMicrosoftAccount(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ o.ForwardAuthenticate = "specific";
+ o.ForwardChallenge = "specific";
+ o.ForwardSignIn = "specific";
+ o.ForwardSignOut = "specific";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, specific.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, specific.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, specific.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ }
+
+ [Fact]
+ public async Task VerifySignInSchemeCannotBeSetToSelf()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.SignInScheme = MicrosoftAccountDefaults.AuthenticationScheme;
+ });
+ var error = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("https://example.com/challenge"));
+ Assert.Contains("cannot be set to itself", error.Message);
+ }
+
+ [Fact]
+ public async Task VerifySchemeDefaults()
+ {
+ var services = new ServiceCollection();
+ services.AddAuthentication().AddMicrosoftAccount();
+ var sp = services.BuildServiceProvider();
+ var schemeProvider = sp.GetRequiredService<IAuthenticationSchemeProvider>();
+ var scheme = await schemeProvider.GetSchemeAsync(MicrosoftAccountDefaults.AuthenticationScheme);
+ Assert.NotNull(scheme);
+ Assert.Equal("MicrosoftAccountHandler", scheme.HandlerType.Name);
+ Assert.Equal(MicrosoftAccountDefaults.AuthenticationScheme, scheme.DisplayName);
+ }
+
+ [Fact]
+ public async Task ChallengeWillTriggerApplyRedirectEvent()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Client Id";
+ o.ClientSecret = "Test Client Secret";
+ o.Events = new OAuthEvents
+ {
+ OnRedirectToAuthorizationEndpoint = context =>
+ {
+ context.Response.Redirect(context.RedirectUri + "&custom=test");
+ return Task.FromResult(0);
+ }
+ };
+ });
+ var transaction = await server.SendAsync("http://example.com/challenge");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var query = transaction.Response.Headers.Location.Query;
+ Assert.Contains("custom=test", query);
+ }
+
+ [Fact]
+ public async Task SignInThrows()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ });
+ var transaction = await server.SendAsync("https://example.com/signIn");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task SignOutThrows()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ });
+ var transaction = await server.SendAsync("https://example.com/signOut");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ForbidThrows()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ });
+ var transaction = await server.SendAsync("https://example.com/signOut");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ChallengeWillTriggerRedirection()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ });
+ var transaction = await server.SendAsync("http://example.com/challenge");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var location = transaction.Response.Headers.Location.AbsoluteUri;
+ Assert.Contains("https://login.microsoftonline.com/common/oauth2/v2.0/authorize", location);
+ Assert.Contains("response_type=code", location);
+ Assert.Contains("client_id=", location);
+ Assert.Contains("redirect_uri=", location);
+ Assert.Contains("scope=", location);
+ Assert.Contains("state=", location);
+ }
+
+ [Fact]
+ public async Task ChallengeWillIncludeScopeAsConfigured()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.Scope.Clear();
+ o.Scope.Add("foo");
+ o.Scope.Add("bar");
+ });
+ var transaction = await server.SendAsync("http://example.com/challenge");
+ var res = transaction.Response;
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.Contains("scope=foo%20bar", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task ChallengeWillIncludeScopeAsOverwritten()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.Scope.Clear();
+ o.Scope.Add("foo");
+ o.Scope.Add("bar");
+ });
+ var transaction = await server.SendAsync("http://example.com/challengeWithOtherScope");
+ var res = transaction.Response;
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.Contains("scope=baz%20qux", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task ChallengeWillIncludeScopeAsOverwrittenWithBaseAuthenticationProperties()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.Scope.Clear();
+ o.Scope.Add("foo");
+ o.Scope.Add("bar");
+ });
+ var transaction = await server.SendAsync("http://example.com/challengeWithOtherScopeWithBaseAuthenticationProperties");
+ var res = transaction.Response;
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.Contains("scope=baz%20qux", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task AuthenticatedEventCanGetRefreshToken()
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("MsftTest"));
+ var server = CreateServer(o =>
+ {
+ o.ClientId = "Test Client Id";
+ o.ClientSecret = "Test Client Secret";
+ o.StateDataFormat = stateFormat;
+ o.BackchannelHttpHandler = new TestHttpMessageHandler
+ {
+ Sender = req =>
+ {
+ if (req.RequestUri.AbsoluteUri == "https://login.microsoftonline.com/common/oauth2/v2.0/token")
+ {
+ return ReturnJsonResponse(new
+ {
+ access_token = "Test Access Token",
+ expire_in = 3600,
+ token_type = "Bearer",
+ refresh_token = "Test Refresh Token"
+ });
+ }
+ else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://graph.microsoft.com/v1.0/me")
+ {
+ return ReturnJsonResponse(new
+ {
+ id = "Test User ID",
+ displayName = "Test Name",
+ givenName = "Test Given Name",
+ surname = "Test Family Name",
+ mail = "Test email"
+ });
+ }
+
+ return null;
+ }
+ };
+ o.Events = new OAuthEvents
+ {
+ OnCreatingTicket = context =>
+ {
+ var refreshToken = context.RefreshToken;
+ context.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "Microsoft") }, "Microsoft"));
+ return Task.FromResult<object>(null);
+ }
+ };
+ });
+ var properties = new AuthenticationProperties();
+ var correlationKey = ".xsrf";
+ var correlationValue = "TestCorrelationId";
+ properties.Items.Add(correlationKey, correlationValue);
+ properties.RedirectUri = "/me";
+ var state = stateFormat.Protect(properties);
+ var transaction = await server.SendAsync(
+ "https://example.com/signin-microsoft?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
+ $".AspNetCore.Correlation.Microsoft.{correlationValue}=N");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
+ Assert.Equal(2, transaction.SetCookie.Count);
+ Assert.Contains($".AspNetCore.Correlation.Microsoft.{correlationValue}", transaction.SetCookie[0]);
+ Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
+
+ var authCookie = transaction.AuthenticationCookieValue;
+ transaction = await server.SendAsync("https://example.com/me", authCookie);
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ Assert.Equal("Test Refresh Token", transaction.FindClaimValue("RefreshToken"));
+ }
+
+ private static TestServer CreateServer(Action<MicrosoftAccountOptions> configureOptions)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Use(async (context, next) =>
+ {
+ var req = context.Request;
+ var res = context.Response;
+ if (req.Path == new PathString("/challenge"))
+ {
+ await context.ChallengeAsync("Microsoft");
+ }
+ else if (req.Path == new PathString("/challengeWithOtherScope"))
+ {
+ var properties = new OAuthChallengeProperties();
+ properties.SetScope("baz", "qux");
+ await context.ChallengeAsync("Microsoft", properties);
+ }
+ else if (req.Path == new PathString("/challengeWithOtherScopeWithBaseAuthenticationProperties"))
+ {
+ var properties = new AuthenticationProperties();
+ properties.SetParameter(OAuthChallengeProperties.ScopeKey, new string[] { "baz", "qux" });
+ await context.ChallengeAsync("Microsoft", properties);
+ }
+ else if (req.Path == new PathString("/me"))
+ {
+ res.Describe(context.User);
+ }
+ else if (req.Path == new PathString("/signIn"))
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync("Microsoft", new ClaimsPrincipal()));
+ }
+ else if (req.Path == new PathString("/signOut"))
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync("Microsoft"));
+ }
+ else if (req.Path == new PathString("/forbid"))
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.ForbidAsync("Microsoft"));
+ }
+ else
+ {
+ await next();
+ }
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddAuthentication(TestExtensions.CookieAuthenticationScheme)
+ .AddCookie(TestExtensions.CookieAuthenticationScheme, o => { })
+ .AddMicrosoftAccount(configureOptions);
+ });
+ return new TestServer(builder);
+ }
+
+ private static HttpResponseMessage ReturnJsonResponse(object content)
+ {
+ var res = new HttpResponseMessage(HttpStatusCode.OK);
+ var text = JsonConvert.SerializeObject(content);
+ res.Content = new StringContent(text, Encoding.UTF8, "application/json");
+ return res;
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthChallengePropertiesTest.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthChallengePropertiesTest.cs
new file mode 100644
index 0000000000..c359bb0e8c
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthChallengePropertiesTest.cs
@@ -0,0 +1,149 @@
+using System;
+using Microsoft.AspNetCore.Authentication.Google;
+using Microsoft.AspNetCore.Authentication.OAuth;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.Test
+{
+ public class OAuthChallengePropertiesTest
+ {
+ [Fact]
+ public void ScopeProperty()
+ {
+ var properties = new OAuthChallengeProperties
+ {
+ Scope = new string[] { "foo", "bar" }
+ };
+ Assert.Equal(new string[] { "foo", "bar" }, properties.Scope);
+ Assert.Equal(new string[] { "foo", "bar" }, properties.Parameters["scope"]);
+ }
+
+ [Fact]
+ public void ScopeProperty_NullValue()
+ {
+ var properties = new OAuthChallengeProperties();
+ properties.Parameters["scope"] = new string[] { "foo", "bar" };
+ Assert.Equal(new string[] { "foo", "bar" }, properties.Scope);
+
+ properties.Scope = null;
+ Assert.Null(properties.Scope);
+ }
+
+ [Fact]
+ public void SetScope()
+ {
+ var properties = new OAuthChallengeProperties();
+ properties.SetScope("foo", "bar");
+ Assert.Equal(new string[] { "foo", "bar" }, properties.Scope);
+ Assert.Equal(new string[] { "foo", "bar" }, properties.Parameters["scope"]);
+ }
+
+ [Fact]
+ public void OidcMaxAge()
+ {
+ var properties = new OpenIdConnectChallengeProperties()
+ {
+ MaxAge = TimeSpan.FromSeconds(200)
+ };
+ Assert.Equal(TimeSpan.FromSeconds(200), properties.MaxAge);
+ }
+
+ [Fact]
+ public void OidcMaxAge_NullValue()
+ {
+ var properties = new OpenIdConnectChallengeProperties();
+ properties.Parameters["max_age"] = TimeSpan.FromSeconds(500);
+ Assert.Equal(TimeSpan.FromSeconds(500), properties.MaxAge);
+
+ properties.MaxAge = null;
+ Assert.Null(properties.MaxAge);
+ }
+
+ [Fact]
+ public void OidcPrompt()
+ {
+ var properties = new OpenIdConnectChallengeProperties()
+ {
+ Prompt = "login"
+ };
+ Assert.Equal("login", properties.Prompt);
+ Assert.Equal("login", properties.Parameters["prompt"]);
+ }
+
+ [Fact]
+ public void OidcPrompt_NullValue()
+ {
+ var properties = new OpenIdConnectChallengeProperties();
+ properties.Parameters["prompt"] = "consent";
+ Assert.Equal("consent", properties.Prompt);
+
+ properties.Prompt = null;
+ Assert.Null(properties.Prompt);
+ }
+
+ [Fact]
+ public void GoogleProperties()
+ {
+ var properties = new GoogleChallengeProperties()
+ {
+ AccessType = "offline",
+ ApprovalPrompt = "force",
+ LoginHint = "test@example.com",
+ Prompt = "login",
+ };
+ Assert.Equal("offline", properties.AccessType);
+ Assert.Equal("offline", properties.Parameters["access_type"]);
+ Assert.Equal("force", properties.ApprovalPrompt);
+ Assert.Equal("force", properties.Parameters["approval_prompt"]);
+ Assert.Equal("test@example.com", properties.LoginHint);
+ Assert.Equal("test@example.com", properties.Parameters["login_hint"]);
+ Assert.Equal("login", properties.Prompt);
+ Assert.Equal("login", properties.Parameters["prompt"]);
+ }
+
+ [Fact]
+ public void GoogleProperties_NullValues()
+ {
+ var properties = new GoogleChallengeProperties();
+ properties.Parameters["access_type"] = "offline";
+ properties.Parameters["approval_prompt"] = "force";
+ properties.Parameters["login_hint"] = "test@example.com";
+ properties.Parameters["prompt"] = "login";
+ Assert.Equal("offline", properties.AccessType);
+ Assert.Equal("force", properties.ApprovalPrompt);
+ Assert.Equal("test@example.com", properties.LoginHint);
+ Assert.Equal("login", properties.Prompt);
+
+ properties.AccessType = null;
+ Assert.Null(properties.AccessType);
+
+ properties.ApprovalPrompt = null;
+ Assert.Null(properties.ApprovalPrompt);
+
+ properties.LoginHint = null;
+ Assert.Null(properties.LoginHint);
+
+ properties.Prompt = null;
+ Assert.Null(properties.Prompt);
+ }
+
+ [Fact]
+ public void GoogleIncludeGrantedScopes()
+ {
+ var properties = new GoogleChallengeProperties()
+ {
+ IncludeGrantedScopes = true
+ };
+ Assert.True(properties.IncludeGrantedScopes);
+ Assert.Equal(true, properties.Parameters["include_granted_scopes"]);
+
+ properties.IncludeGrantedScopes = false;
+ Assert.False(properties.IncludeGrantedScopes);
+ Assert.Equal(false, properties.Parameters["include_granted_scopes"]);
+
+ properties.IncludeGrantedScopes = null;
+ Assert.Null(properties.IncludeGrantedScopes);
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthTests.cs
new file mode 100644
index 0000000000..4b822b611f
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OAuthTests.cs
@@ -0,0 +1,752 @@
+// 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.Net;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.Tests;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.OAuth
+{
+ public class OAuthTests
+ {
+ [Fact]
+ public async Task CanForwardDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = "default";
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ })
+ .AddOAuth("default", o =>
+ {
+ ConfigureDefaults(o);
+ o.SignInScheme = "auth1";
+ o.ForwardDefault = "auth1";
+ });
+
+ var forwardDefault = new TestHandler();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignInThrows()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = "default";
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOAuth("default", o =>
+ {
+ ConfigureDefaults(o);
+ o.SignInScheme = "auth1";
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignOutThrows()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = "default";
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOAuth("default", o =>
+ {
+ ConfigureDefaults(o);
+ o.SignInScheme = "auth1";
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ }
+
+ [Fact]
+ public async Task ForwardForbidWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = "default";
+ o.DefaultSignInScheme = "auth1";
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOAuth("default", o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ForbidAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(1, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardAuthenticateWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = "default";
+ o.DefaultSignInScheme = "auth1";
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOAuth("default", o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardAuthenticate = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(1, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardChallengeWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = "default";
+ o.DefaultSignInScheme = "auth1";
+ o.AddScheme<TestHandler>("specific", "specific");
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ })
+ .AddOAuth("default", o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardChallenge = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ChallengeAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(1, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardSelectorWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = "default";
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOAuth("default", o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, selector.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, selector.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, selector.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task NullForwardSelectorUsesDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = "default";
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOAuth("default", o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => null;
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task SpecificForwardWinsOverSelectorAndDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = "default";
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOAuth("default", o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ o.ForwardAuthenticate = "specific";
+ o.ForwardChallenge = "specific";
+ o.ForwardSignIn = "specific";
+ o.ForwardSignOut = "specific";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, specific.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, specific.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, specific.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ }
+
+
+ [Fact]
+ public async Task VerifySignInSchemeCannotBeSetToSelf()
+ {
+ var server = CreateServer(
+ services => services.AddAuthentication().AddOAuth("weeblie", o =>
+ {
+ o.SignInScheme = "weeblie";
+ o.ClientId = "whatever";
+ o.ClientSecret = "whatever";
+ o.CallbackPath = "/whatever";
+ o.AuthorizationEndpoint = "/whatever";
+ o.TokenEndpoint = "/whatever";
+ }));
+ var error = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("https://example.com/"));
+ Assert.Contains("cannot be set to itself", error.Message);
+ }
+
+ [Fact]
+ public async Task VerifySchemeDefaults()
+ {
+ var services = new ServiceCollection();
+ services.AddAuthentication().AddOAuth("oauth", o => { });
+ var sp = services.BuildServiceProvider();
+ var schemeProvider = sp.GetRequiredService<IAuthenticationSchemeProvider>();
+ var scheme = await schemeProvider.GetSchemeAsync("oauth");
+ Assert.NotNull(scheme);
+ Assert.Equal("OAuthHandler`1", scheme.HandlerType.Name);
+ Assert.Equal(OAuthDefaults.DisplayName, scheme.DisplayName);
+ }
+
+ [Fact]
+ public async Task ThrowsIfClientIdMissing()
+ {
+ var server = CreateServer(
+ services => services.AddAuthentication().AddOAuth("weeblie", o =>
+ {
+ o.SignInScheme = "whatever";
+ o.CallbackPath = "/";
+ o.ClientSecret = "whatever";
+ o.TokenEndpoint = "/";
+ o.AuthorizationEndpoint = "/";
+ }));
+ await Assert.ThrowsAsync<ArgumentException>("ClientId", () => server.SendAsync("http://example.com/"));
+ }
+
+ [Fact]
+ public async Task ThrowsIfClientSecretMissing()
+ {
+ var server = CreateServer(
+ services => services.AddAuthentication().AddOAuth("weeblie", o =>
+ {
+ o.SignInScheme = "whatever";
+ o.ClientId = "Whatever;";
+ o.CallbackPath = "/";
+ o.TokenEndpoint = "/";
+ o.AuthorizationEndpoint = "/";
+ }));
+ await Assert.ThrowsAsync<ArgumentException>("ClientSecret", () => server.SendAsync("http://example.com/"));
+ }
+
+ [Fact]
+ public async Task ThrowsIfCallbackPathMissing()
+ {
+ var server = CreateServer(
+ services => services.AddAuthentication().AddOAuth("weeblie", o =>
+ {
+ o.ClientId = "Whatever;";
+ o.ClientSecret = "Whatever;";
+ o.TokenEndpoint = "/";
+ o.AuthorizationEndpoint = "/";
+ o.SignInScheme = "eh";
+ }));
+ await Assert.ThrowsAsync<ArgumentException>("CallbackPath", () => server.SendAsync("http://example.com/"));
+ }
+
+ [Fact]
+ public async Task ThrowsIfTokenEndpointMissing()
+ {
+ var server = CreateServer(
+ services => services.AddAuthentication().AddOAuth("weeblie", o =>
+ {
+ o.ClientId = "Whatever;";
+ o.ClientSecret = "Whatever;";
+ o.CallbackPath = "/";
+ o.AuthorizationEndpoint = "/";
+ o.SignInScheme = "eh";
+ }));
+ await Assert.ThrowsAsync<ArgumentException>("TokenEndpoint", () => server.SendAsync("http://example.com/"));
+ }
+
+ [Fact]
+ public async Task ThrowsIfAuthorizationEndpointMissing()
+ {
+ var server = CreateServer(
+ services => services.AddAuthentication().AddOAuth("weeblie", o =>
+ {
+ o.ClientId = "Whatever;";
+ o.ClientSecret = "Whatever;";
+ o.CallbackPath = "/";
+ o.TokenEndpoint = "/";
+ o.SignInScheme = "eh";
+ }));
+ await Assert.ThrowsAsync<ArgumentException>("AuthorizationEndpoint", () => server.SendAsync("http://example.com/"));
+ }
+
+ [Fact]
+ public async Task RedirectToIdentityProvider_SetsCorrelationIdCookiePath_ToCallBackPath()
+ {
+ var server = CreateServer(
+ s => s.AddAuthentication().AddOAuth(
+ "Weblie",
+ opt =>
+ {
+ ConfigureDefaults(opt);
+ }),
+ async ctx =>
+ {
+ await ctx.ChallengeAsync("Weblie");
+ return true;
+ });
+
+ var transaction = await server.SendAsync("https://www.example.com/challenge");
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.NotNull(res.Headers.Location);
+ var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie");
+ var correlation = Assert.Single(setCookie.Value, v => v.StartsWith(".AspNetCore.Correlation."));
+ Assert.Contains("path=/oauth-callback", correlation);
+ }
+
+ [Fact]
+ public async Task RedirectToAuthorizeEndpoint_CorrelationIdCookieOptions_CanBeOverriden()
+ {
+ var server = CreateServer(
+ s => s.AddAuthentication().AddOAuth(
+ "Weblie",
+ opt =>
+ {
+ ConfigureDefaults(opt);
+ opt.CorrelationCookie.Path = "/";
+ }),
+ async ctx =>
+ {
+ await ctx.ChallengeAsync("Weblie");
+ return true;
+ });
+
+ var transaction = await server.SendAsync("https://www.example.com/challenge");
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.NotNull(res.Headers.Location);
+ var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie");
+ var correlation = Assert.Single(setCookie.Value, v => v.StartsWith(".AspNetCore.Correlation."));
+ Assert.Contains("path=/", correlation);
+ }
+
+ [Fact]
+ public async Task RedirectToAuthorizeEndpoint_HasScopeAsConfigured()
+ {
+ var server = CreateServer(
+ s => s.AddAuthentication().AddOAuth(
+ "Weblie",
+ opt =>
+ {
+ ConfigureDefaults(opt);
+ opt.Scope.Clear();
+ opt.Scope.Add("foo");
+ opt.Scope.Add("bar");
+ }),
+ async ctx =>
+ {
+ await ctx.ChallengeAsync("Weblie");
+ return true;
+ });
+
+ var transaction = await server.SendAsync("https://www.example.com/challenge");
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.Contains("scope=foo%20bar", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task RedirectToAuthorizeEndpoint_HasScopeAsOverwritten()
+ {
+ var server = CreateServer(
+ s => s.AddAuthentication().AddOAuth(
+ "Weblie",
+ opt =>
+ {
+ ConfigureDefaults(opt);
+ opt.Scope.Clear();
+ opt.Scope.Add("foo");
+ opt.Scope.Add("bar");
+ }),
+ async ctx =>
+ {
+ var properties = new OAuthChallengeProperties();
+ properties.SetScope("baz", "qux");
+ await ctx.ChallengeAsync("Weblie", properties);
+ return true;
+ });
+
+ var transaction = await server.SendAsync("https://www.example.com/challenge");
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.Contains("scope=baz%20qux", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task RedirectToAuthorizeEndpoint_HasScopeAsOverwrittenWithBaseAuthenticationProperties()
+ {
+ var server = CreateServer(
+ s => s.AddAuthentication().AddOAuth(
+ "Weblie",
+ opt =>
+ {
+ ConfigureDefaults(opt);
+ opt.Scope.Clear();
+ opt.Scope.Add("foo");
+ opt.Scope.Add("bar");
+ }),
+ async ctx =>
+ {
+ var properties = new AuthenticationProperties();
+ properties.SetParameter(OAuthChallengeProperties.ScopeKey, new string[] { "baz", "qux" });
+ await ctx.ChallengeAsync("Weblie", properties);
+ return true;
+ });
+
+ var transaction = await server.SendAsync("https://www.example.com/challenge");
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.Contains("scope=baz%20qux", res.Headers.Location.Query);
+ }
+
+ private void ConfigureDefaults(OAuthOptions o)
+ {
+ o.ClientId = "Test Id";
+ o.ClientSecret = "secret";
+ o.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ o.AuthorizationEndpoint = "https://example.com/provider/login";
+ o.TokenEndpoint = "https://example.com/provider/token";
+ o.CallbackPath = "/oauth-callback";
+ }
+
+ [Fact]
+ public async Task RemoteAuthenticationFailed_OAuthError_IncludesProperties()
+ {
+ var server = CreateServer(
+ s => s.AddAuthentication().AddOAuth(
+ "Weblie",
+ opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.ClientSecret = "secret";
+ opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ opt.AuthorizationEndpoint = "https://example.com/provider/login";
+ opt.TokenEndpoint = "https://example.com/provider/token";
+ opt.CallbackPath = "/oauth-callback";
+ opt.StateDataFormat = new TestStateDataFormat();
+ opt.Events = new OAuthEvents()
+ {
+ OnRemoteFailure = context =>
+ {
+ Assert.Contains("declined", context.Failure.Message);
+ Assert.Equal("testvalue", context.Properties.Items["testkey"]);
+ context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
+ context.HandleResponse();
+ return Task.CompletedTask;
+ }
+ };
+ }));
+
+ var transaction = await server.SendAsync("https://www.example.com/oauth-callback?error=declined&state=protected_state",
+ ".AspNetCore.Correlation.Weblie.corrilationId=N");
+
+ Assert.Equal(HttpStatusCode.NotAcceptable, transaction.Response.StatusCode);
+ Assert.Null(transaction.Response.Headers.Location);
+ }
+
+ private static TestServer CreateServer(Action<IServiceCollection> configureServices, Func<HttpContext, Task<bool>> handler = null)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Use(async (context, next) =>
+ {
+ if (handler == null || ! await handler(context))
+ {
+ await next();
+ }
+ });
+ })
+ .ConfigureServices(configureServices);
+ return new TestServer(builder);
+ }
+
+ private class TestStateDataFormat : ISecureDataFormat<AuthenticationProperties>
+ {
+ private AuthenticationProperties Data { get; set; }
+
+ public string Protect(AuthenticationProperties data)
+ {
+ return "protected_state";
+ }
+
+ public string Protect(AuthenticationProperties data, string purpose)
+ {
+ throw new NotImplementedException();
+ }
+
+ public AuthenticationProperties Unprotect(string protectedText)
+ {
+ Assert.Equal("protected_state", protectedText);
+ var properties = new AuthenticationProperties(new Dictionary<string, string>()
+ {
+ { ".xsrf", "corrilationId" },
+ { "testkey", "testvalue" }
+ });
+ properties.RedirectUri = "http://testhost/redirect";
+ return properties;
+ }
+
+ public AuthenticationProperties Unprotect(string protectedText, string purpose)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/MockOpenIdConnectMessage.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/MockOpenIdConnectMessage.cs
new file mode 100644
index 0000000000..5614fe8fea
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/MockOpenIdConnectMessage.cs
@@ -0,0 +1,21 @@
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
+{
+ internal class MockOpenIdConnectMessage : OpenIdConnectMessage
+ {
+ public string TestAuthorizeEndpoint { get; set; }
+
+ public string TestLogoutRequest { get; set; }
+
+ public override string CreateAuthenticationRequestUrl()
+ {
+ return TestAuthorizeEndpoint ?? base.CreateAuthenticationRequestUrl();
+ }
+
+ public override string CreateLogoutRequestUrl()
+ {
+ return TestLogoutRequest ?? base.CreateLogoutRequestUrl();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs
new file mode 100644
index 0000000000..cbafc46223
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs
@@ -0,0 +1,616 @@
+// 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.Net;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Primitives;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
+{
+ public class OpenIdConnectChallengeTests
+ {
+ private static readonly string ChallengeEndpoint = TestServerBuilder.TestHost + TestServerBuilder.Challenge;
+
+ [Fact]
+ public async Task ChallengeRedirectIsIssuedCorrectly()
+ {
+ var settings = new TestSettings(
+ opt =>
+ {
+ opt.Authority = TestServerBuilder.DefaultAuthority;
+ opt.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
+ opt.ClientId = "Test Id";
+ });
+
+ var server = settings.CreateTestServer();
+ var transaction = await server.SendAsync(ChallengeEndpoint);
+
+ var res = transaction.Response;
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.NotNull(res.Headers.Location);
+
+ settings.ValidateChallengeRedirect(
+ res.Headers.Location,
+ OpenIdConnectParameterNames.ClientId,
+ OpenIdConnectParameterNames.ResponseType,
+ OpenIdConnectParameterNames.ResponseMode,
+ OpenIdConnectParameterNames.Scope,
+ OpenIdConnectParameterNames.RedirectUri,
+ OpenIdConnectParameterNames.SkuTelemetry,
+ OpenIdConnectParameterNames.VersionTelemetry);
+ }
+
+ [Fact]
+ public async Task AuthorizationRequestDoesNotIncludeTelemetryParametersWhenDisabled()
+ {
+ var settings = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.Authority = TestServerBuilder.DefaultAuthority;
+ opt.DisableTelemetry = true;
+ });
+
+ var server = settings.CreateTestServer();
+ var transaction = await server.SendAsync(ChallengeEndpoint);
+
+ var res = transaction.Response;
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.DoesNotContain(OpenIdConnectParameterNames.SkuTelemetry, res.Headers.Location.Query);
+ Assert.DoesNotContain(OpenIdConnectParameterNames.VersionTelemetry, res.Headers.Location.Query);
+ }
+
+ /*
+ Example of a form post
+ <body>
+ <form name=\ "form\" method=\ "post\" action=\ "https://login.microsoftonline.com/common/oauth2/authorize\">
+ <input type=\ "hidden\" name=\ "client_id\" value=\ "51e38103-238f-410f-a5d5-61991b203e50\" />
+ <input type=\ "hidden\" name=\ "redirect_uri\" value=\ "https://example.com/signin-oidc\" />
+ <input type=\ "hidden\" name=\ "response_type\" value=\ "id_token\" />
+ <input type=\ "hidden\" name=\ "scope\" value=\ "openid profile\" />
+ <input type=\ "hidden\" name=\ "response_mode\" value=\ "form_post\" />
+ <input type=\ "hidden\" name=\ "nonce\" value=\ "636072461997914230.NTAwOGE1MjQtM2VhYS00ZDU0LWFkYzYtNmZiYWE2MDRkODg3OTlkMDFmOWUtOTMzNC00ZmI2LTg1Y2YtOWM4OTlhNjY0Yjli\" />
+ <input type=\ "hidden\" name=\ "state\" value=\
+ "CfDJ8Jh1NKaF0T5AnK4qsqzzIs89srKe4iEaBWd29MNph4Ki887QKgkD24wjhZ0ciH-ar6A_jUmRI2O5haXN2-YXbC0ZRuRAvNsx5LqbPTdh4MJBIwXWkG_rM0T0tI3h5Y2pDttWSaku6a_nzFLUYBrKfsE7sDLVoTDrzzOcHrRQhdztqOOeNUuu2wQXaKwlOtNI21ShtN9EVxvSGFOxUUOwVih4nFdF40fBcbsuPpcpCPkLARQaFRJSYsNKiP7pcFMnRwzZhnISHlyGKkzwJ1DIx7nsmdiQFBGljimw5GnYAs-5ru9L3w8NnPjkl96OyQ8MJOcayMDmOY26avs2sYP_Zw0\" />
+ <noscript>Click here to finish the process: <input type=\"submit\" /></noscript>
+ </form>
+ <script>
+ document.form.submit();
+ </script>
+ </body>
+ */
+ [Fact]
+ public async Task ChallengeFormPostIssuedCorrectly()
+ {
+ var settings = new TestSettings(
+ opt =>
+ {
+ opt.Authority = TestServerBuilder.DefaultAuthority;
+ opt.AuthenticationMethod = OpenIdConnectRedirectBehavior.FormPost;
+ opt.ClientId = "Test Id";
+ });
+
+ var server = settings.CreateTestServer();
+ var transaction = await server.SendAsync(ChallengeEndpoint);
+
+ var res = transaction.Response;
+ Assert.Equal(HttpStatusCode.OK, res.StatusCode);
+ Assert.Equal("text/html", transaction.Response.Content.Headers.ContentType.MediaType);
+
+ var body = await res.Content.ReadAsStringAsync();
+ settings.ValidateChallengeFormPost(
+ body,
+ OpenIdConnectParameterNames.ClientId,
+ OpenIdConnectParameterNames.ResponseType,
+ OpenIdConnectParameterNames.ResponseMode,
+ OpenIdConnectParameterNames.Scope,
+ OpenIdConnectParameterNames.RedirectUri);
+ }
+
+ [Theory]
+ [InlineData("sample_user_state")]
+ [InlineData(null)]
+ public async Task ChallengeCanSetUserStateThroughProperties(string userState)
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("OIDCTest"));
+ var settings = new TestSettings(o =>
+ {
+ o.ClientId = "Test Id";
+ o.Authority = TestServerBuilder.DefaultAuthority;
+ o.StateDataFormat = stateFormat;
+ });
+
+ var properties = new AuthenticationProperties();
+ properties.Items.Add(OpenIdConnectDefaults.UserstatePropertiesKey, userState);
+
+ var server = settings.CreateTestServer(properties);
+ var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties);
+
+ var res = transaction.Response;
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.NotNull(res.Headers.Location);
+
+ var values = settings.ValidateChallengeRedirect(res.Headers.Location);
+ var actualState = values[OpenIdConnectParameterNames.State];
+ var actualProperties = stateFormat.Unprotect(actualState);
+
+ Assert.Equal(userState ?? string.Empty, actualProperties.Items[OpenIdConnectDefaults.UserstatePropertiesKey]);
+ }
+
+ [Theory]
+ [InlineData("sample_user_state")]
+ [InlineData(null)]
+ public async Task OnRedirectToIdentityProviderEventCanSetState(string userState)
+ {
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("OIDCTest"));
+ var settings = new TestSettings(opt =>
+ {
+ opt.StateDataFormat = stateFormat;
+ opt.ClientId = "Test Id";
+ opt.Authority = TestServerBuilder.DefaultAuthority;
+ opt.Events = new OpenIdConnectEvents()
+ {
+ OnRedirectToIdentityProvider = context =>
+ {
+ context.ProtocolMessage.State = userState;
+ return Task.FromResult(0);
+ }
+ };
+ });
+
+ var server = settings.CreateTestServer();
+ var transaction = await server.SendAsync(ChallengeEndpoint);
+
+ var res = transaction.Response;
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.NotNull(res.Headers.Location);
+
+ var values = settings.ValidateChallengeRedirect(res.Headers.Location);
+ var actualState = values[OpenIdConnectParameterNames.State];
+ var actualProperties = stateFormat.Unprotect(actualState);
+
+ if (userState != null)
+ {
+ Assert.Equal(userState, actualProperties.Items[OpenIdConnectDefaults.UserstatePropertiesKey]);
+ }
+ else
+ {
+ Assert.False(actualProperties.Items.ContainsKey(OpenIdConnectDefaults.UserstatePropertiesKey));
+ }
+ }
+
+ [Fact]
+ public async Task OnRedirectToIdentityProviderEventIsHit()
+ {
+ var eventIsHit = false;
+ var settings = new TestSettings(
+ opts =>
+ {
+ opts.ClientId = "Test Id";
+ opts.Authority = TestServerBuilder.DefaultAuthority;
+ opts.Events = new OpenIdConnectEvents()
+ {
+ OnRedirectToIdentityProvider = context =>
+ {
+ eventIsHit = true;
+ return Task.FromResult(0);
+ }
+ };
+ }
+ );
+
+ var server = settings.CreateTestServer();
+ var transaction = await server.SendAsync(ChallengeEndpoint);
+
+ Assert.True(eventIsHit);
+
+ var res = transaction.Response;
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.NotNull(res.Headers.Location);
+
+ settings.ValidateChallengeRedirect(
+ res.Headers.Location,
+ OpenIdConnectParameterNames.ClientId,
+ OpenIdConnectParameterNames.ResponseType,
+ OpenIdConnectParameterNames.ResponseMode,
+ OpenIdConnectParameterNames.Scope,
+ OpenIdConnectParameterNames.RedirectUri);
+ }
+
+
+ [Fact]
+ public async Task OnRedirectToIdentityProviderEventCanReplaceValues()
+ {
+ var newClientId = Guid.NewGuid().ToString();
+
+ var settings = new TestSettings(
+ opts =>
+ {
+ opts.ClientId = "Test Id";
+ opts.Authority = TestServerBuilder.DefaultAuthority;
+ opts.Events = new OpenIdConnectEvents()
+ {
+ OnRedirectToIdentityProvider = context =>
+ {
+ context.ProtocolMessage.ClientId = newClientId;
+ return Task.FromResult(0);
+ }
+ };
+ }
+ );
+
+ var server = settings.CreateTestServer();
+ var transaction = await server.SendAsync(ChallengeEndpoint);
+
+ var res = transaction.Response;
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.NotNull(res.Headers.Location);
+
+ settings.ValidateChallengeRedirect(
+ res.Headers.Location,
+ OpenIdConnectParameterNames.ResponseType,
+ OpenIdConnectParameterNames.ResponseMode,
+ OpenIdConnectParameterNames.Scope,
+ OpenIdConnectParameterNames.RedirectUri);
+
+ var actual = res.Headers.Location.Query.Trim('?').Split('&').Single(seg => seg.StartsWith($"{OpenIdConnectParameterNames.ClientId}="));
+ Assert.Equal($"{OpenIdConnectParameterNames.ClientId}={newClientId}", actual);
+ }
+
+ [Fact]
+ public async Task OnRedirectToIdentityProviderEventCanReplaceMessage()
+ {
+ var newMessage = new MockOpenIdConnectMessage
+ {
+ IssuerAddress = "http://example.com/",
+ TestAuthorizeEndpoint = $"http://example.com/{Guid.NewGuid()}/oauth2/signin"
+ };
+
+ var settings = new TestSettings(
+ opts =>
+ {
+ opts.ClientId = "Test Id";
+ opts.Authority = TestServerBuilder.DefaultAuthority;
+ opts.Events = new OpenIdConnectEvents()
+ {
+ OnRedirectToIdentityProvider = context =>
+ {
+ context.ProtocolMessage = newMessage;
+
+ return Task.FromResult(0);
+ }
+ };
+ }
+ );
+
+ var server = settings.CreateTestServer();
+ var transaction = await server.SendAsync(ChallengeEndpoint);
+
+ var res = transaction.Response;
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.NotNull(res.Headers.Location);
+
+ // The CreateAuthenticationRequestUrl method is overridden MockOpenIdConnectMessage where
+ // query string is not generated and the authorization endpoint is replaced.
+ Assert.Equal(newMessage.TestAuthorizeEndpoint, res.Headers.Location.AbsoluteUri);
+ }
+
+ [Fact]
+ public async Task OnRedirectToIdentityProviderEventHandlesResponse()
+ {
+ var settings = new TestSettings(
+ opts =>
+ {
+ opts.ClientId = "Test Id";
+ opts.Authority = TestServerBuilder.DefaultAuthority;
+ opts.Events = new OpenIdConnectEvents()
+ {
+ OnRedirectToIdentityProvider = context =>
+ {
+ context.Response.StatusCode = 410;
+ context.Response.Headers.Add("tea", "Oolong");
+ context.HandleResponse();
+
+ return Task.FromResult(0);
+ }
+ };
+ }
+ );
+
+ var server = settings.CreateTestServer();
+ var transaction = await server.SendAsync(ChallengeEndpoint);
+
+ var res = transaction.Response;
+ Assert.Equal(HttpStatusCode.Gone, res.StatusCode);
+ Assert.Equal("Oolong", res.Headers.GetValues("tea").Single());
+ Assert.Null(res.Headers.Location);
+ }
+
+ // This test can be further refined. When one auth handler skips, the authentication responsibility
+ // will be flowed to the next one. A dummy auth handler can be added to ensure the correct logic.
+ [Fact]
+ public async Task OnRedirectToIdentityProviderEventHandleResponse()
+ {
+ var settings = new TestSettings(
+ opts =>
+ {
+ opts.ClientId = "Test Id";
+ opts.Authority = TestServerBuilder.DefaultAuthority;
+ opts.Events = new OpenIdConnectEvents()
+ {
+ OnRedirectToIdentityProvider = context =>
+ {
+ context.HandleResponse();
+ return Task.FromResult(0);
+ }
+ };
+ }
+ );
+
+ var server = settings.CreateTestServer();
+ var transaction = await server.SendAsync(ChallengeEndpoint);
+
+ var res = transaction.Response;
+ Assert.Equal(HttpStatusCode.OK, res.StatusCode);
+ Assert.Null(res.Headers.Location);
+ }
+
+ [Theory]
+ [InlineData(OpenIdConnectRedirectBehavior.RedirectGet)]
+ [InlineData(OpenIdConnectRedirectBehavior.FormPost)]
+ public async Task ChallengeSetsNonceAndStateCookies(OpenIdConnectRedirectBehavior method)
+ {
+ var settings = new TestSettings(o =>
+ {
+ o.AuthenticationMethod = method;
+ o.ClientId = "Test Id";
+ o.Authority = TestServerBuilder.DefaultAuthority;
+ });
+ var server = settings.CreateTestServer();
+ var transaction = await server.SendAsync(ChallengeEndpoint);
+
+ var challengeCookies = SetCookieHeaderValue.ParseList(transaction.SetCookie);
+ var nonceCookie = challengeCookies.Where(cookie => cookie.Name.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix, StringComparison.Ordinal)).Single();
+ Assert.True(nonceCookie.Expires.HasValue);
+ Assert.True(nonceCookie.Expires > DateTime.UtcNow);
+ Assert.True(nonceCookie.HttpOnly);
+ Assert.Equal("/signin-oidc", nonceCookie.Path);
+ Assert.Equal("N", nonceCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.None, nonceCookie.SameSite);
+
+ var correlationCookie = challengeCookies.Where(cookie => cookie.Name.StartsWith(".AspNetCore.Correlation.", StringComparison.Ordinal)).Single();
+ Assert.True(correlationCookie.Expires.HasValue);
+ Assert.True(nonceCookie.Expires > DateTime.UtcNow);
+ Assert.True(correlationCookie.HttpOnly);
+ Assert.Equal("/signin-oidc", correlationCookie.Path);
+ Assert.False(StringSegment.IsNullOrEmpty(correlationCookie.Value));
+
+ Assert.Equal(2, challengeCookies.Count);
+ }
+
+ [Fact]
+ public async Task Challenge_WithEmptyConfig_Fails()
+ {
+ var settings = new TestSettings(
+ opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.Configuration = new OpenIdConnectConfiguration();
+ });
+
+ var server = settings.CreateTestServer();
+ var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync(ChallengeEndpoint));
+ Assert.Equal("Cannot redirect to the authorization endpoint, the configuration may be missing or invalid.", exception.Message);
+ }
+
+ [Fact]
+ public async Task Challenge_WithDefaultMaxAge_HasExpectedMaxAgeParam()
+ {
+ var settings = new TestSettings(
+ opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.Authority = TestServerBuilder.DefaultAuthority;
+ });
+
+ var server = settings.CreateTestServer();
+ var transaction = await server.SendAsync(ChallengeEndpoint);
+
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ settings.ValidateChallengeRedirect(
+ res.Headers.Location,
+ OpenIdConnectParameterNames.MaxAge);
+ }
+
+ [Fact]
+ public async Task Challenge_WithSpecificMaxAge_HasExpectedMaxAgeParam()
+ {
+ var settings = new TestSettings(
+ opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.Authority = TestServerBuilder.DefaultAuthority;
+ opt.MaxAge = TimeSpan.FromMinutes(20);
+ });
+
+ var server = settings.CreateTestServer();
+ var transaction = await server.SendAsync(ChallengeEndpoint);
+
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ settings.ValidateChallengeRedirect(
+ res.Headers.Location,
+ OpenIdConnectParameterNames.MaxAge);
+ }
+
+ [Fact]
+ public async Task Challenge_HasExpectedPromptParam()
+ {
+ var settings = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.Authority = TestServerBuilder.DefaultAuthority;
+ opt.Prompt = "consent";
+ });
+
+ var server = settings.CreateTestServer();
+ var transaction = await server.SendAsync(ChallengeEndpoint);
+
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ settings.ValidateChallengeRedirect(res.Headers.Location, OpenIdConnectParameterNames.Prompt);
+ Assert.Contains("prompt=consent", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task Challenge_HasOverwrittenPromptParam()
+ {
+ var settings = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.Authority = TestServerBuilder.DefaultAuthority;
+ opt.Prompt = "consent";
+ });
+ var properties = new OpenIdConnectChallengeProperties()
+ {
+ Prompt = "login",
+ };
+
+ var server = settings.CreateTestServer(properties);
+ var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties);
+
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ settings.ValidateChallengeRedirect(res.Headers.Location);
+ Assert.Contains("prompt=login", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task Challenge_HasOverwrittenPromptParamFromBaseAuthenticationProperties()
+ {
+ var settings = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.Authority = TestServerBuilder.DefaultAuthority;
+ opt.Prompt = "consent";
+ });
+ var properties = new AuthenticationProperties();
+ properties.SetParameter(OpenIdConnectChallengeProperties.PromptKey, "login");
+
+ var server = settings.CreateTestServer(properties);
+ var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties);
+
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ settings.ValidateChallengeRedirect(res.Headers.Location);
+ Assert.Contains("prompt=login", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task Challenge_HasOverwrittenScopeParam()
+ {
+ var settings = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.Authority = TestServerBuilder.DefaultAuthority;
+ opt.Scope.Clear();
+ opt.Scope.Add("foo");
+ opt.Scope.Add("bar");
+ });
+ var properties = new OpenIdConnectChallengeProperties();
+ properties.SetScope("baz", "qux");
+
+ var server = settings.CreateTestServer(properties);
+ var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties);
+
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ settings.ValidateChallengeRedirect(res.Headers.Location);
+ Assert.Contains("scope=baz%20qux", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task Challenge_HasOverwrittenScopeParamFromBaseAuthenticationProperties()
+ {
+ var settings = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.Authority = TestServerBuilder.DefaultAuthority;
+ opt.Scope.Clear();
+ opt.Scope.Add("foo");
+ opt.Scope.Add("bar");
+ });
+ var properties = new AuthenticationProperties();
+ properties.SetParameter(OpenIdConnectChallengeProperties.ScopeKey, new string[] { "baz", "qux" });
+
+ var server = settings.CreateTestServer(properties);
+ var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties);
+
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ settings.ValidateChallengeRedirect(res.Headers.Location);
+ Assert.Contains("scope=baz%20qux", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task Challenge_HasOverwrittenMaxAgeParam()
+ {
+ var settings = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.Authority = TestServerBuilder.DefaultAuthority;
+ opt.MaxAge = TimeSpan.FromSeconds(500);
+ });
+ var properties = new OpenIdConnectChallengeProperties()
+ {
+ MaxAge = TimeSpan.FromSeconds(1234),
+ };
+
+ var server = settings.CreateTestServer(properties);
+ var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties);
+
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ settings.ValidateChallengeRedirect(res.Headers.Location);
+ Assert.Contains("max_age=1234", res.Headers.Location.Query);
+ }
+
+ [Fact]
+ public async Task Challenge_HasOverwrittenMaxAgeParaFromBaseAuthenticationPropertiesm()
+ {
+ var settings = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.Authority = TestServerBuilder.DefaultAuthority;
+ opt.MaxAge = TimeSpan.FromSeconds(500);
+ });
+ var properties = new AuthenticationProperties();
+ properties.SetParameter(OpenIdConnectChallengeProperties.MaxAgeKey, TimeSpan.FromSeconds(1234));
+
+ var server = settings.CreateTestServer(properties);
+ var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties);
+
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ settings.ValidateChallengeRedirect(res.Headers.Location);
+ Assert.Contains("max_age=1234", res.Headers.Location.Query);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs
new file mode 100644
index 0000000000..ed368c1ef7
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs
@@ -0,0 +1,574 @@
+// 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.Net;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.AspNetCore.Authentication.Tests;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
+{
+ public class OpenIdConnectConfigurationTests
+ {
+ private void ConfigureDefaults(OpenIdConnectOptions o)
+ {
+ o.Authority = TestServerBuilder.DefaultAuthority;
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ o.SignInScheme = "auth1";
+ }
+
+ [Fact]
+ public async Task CanForwardDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ })
+ .AddOpenIdConnect(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ });
+
+ var forwardDefault = new TestHandler();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await context.SignOutAsync();
+ Assert.Equal(1, forwardDefault.SignOutCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignInThrows()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOpenIdConnect(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignOutWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOpenIdConnect(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.SignOutAsync();
+ Assert.Equal(1, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardForbidWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOpenIdConnect(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ForbidAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(1, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardAuthenticateWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOpenIdConnect(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardAuthenticate = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(1, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardChallengeWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler>("specific", "specific");
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ })
+ .AddOpenIdConnect(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardChallenge = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ChallengeAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(1, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardSelectorWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOpenIdConnect(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, selector.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, selector.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, selector.ChallengeCount);
+
+ await context.SignOutAsync();
+ Assert.Equal(1, selector.SignOutCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task NullForwardSelectorUsesDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOpenIdConnect(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => null;
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await context.SignOutAsync();
+ Assert.Equal(1, forwardDefault.SignOutCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task SpecificForwardWinsOverSelectorAndDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddOpenIdConnect(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ o.ForwardAuthenticate = "specific";
+ o.ForwardChallenge = "specific";
+ o.ForwardSignIn = "specific";
+ o.ForwardSignOut = "specific";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, specific.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, specific.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, specific.ChallengeCount);
+
+ await context.SignOutAsync();
+ Assert.Equal(1, specific.SignOutCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ }
+
+ [Fact]
+ public async Task MetadataAddressIsGeneratedFromAuthorityWhenMissing()
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddAuthentication()
+ .AddCookie()
+ .AddOpenIdConnect(o =>
+ {
+ o.Authority = TestServerBuilder.DefaultAuthority;
+ o.ClientId = Guid.NewGuid().ToString();
+ o.SignInScheme = Guid.NewGuid().ToString();
+ });
+ })
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Run(async context =>
+ {
+ var resolver = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
+ var handler = await resolver.GetHandlerAsync(context, OpenIdConnectDefaults.AuthenticationScheme) as OpenIdConnectHandler;
+ Assert.Equal($"{TestServerBuilder.DefaultAuthority}/.well-known/openid-configuration", handler.Options.MetadataAddress);
+ });
+ });
+ var server = new TestServer(builder);
+ var transaction = await server.SendAsync(@"https://example.com");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public Task ThrowsWhenSignInSchemeIsSetToSelf()
+ {
+ return TestConfigurationException<InvalidOperationException>(
+ o =>
+ {
+ o.SignInScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ o.Authority = TestServerBuilder.DefaultAuthority;
+ o.ClientId = "Test Id";
+ o.ClientSecret = "Test Secret";
+ },
+ ex => Assert.Contains("cannot be set to itself", ex.Message));
+ }
+
+ [Fact]
+ public Task ThrowsWhenClientIdIsMissing()
+ {
+ return TestConfigurationException<ArgumentException>(
+ o =>
+ {
+ o.SignInScheme = "TestScheme";
+ o.Authority = TestServerBuilder.DefaultAuthority;
+ },
+ ex => Assert.Equal("ClientId", ex.ParamName));
+ }
+
+ [Fact]
+ public Task ThrowsWhenAuthorityIsMissing()
+ {
+ return TestConfigurationException<InvalidOperationException>(
+ o =>
+ {
+ o.SignInScheme = "TestScheme";
+ o.ClientId = "Test Id";
+ o.CallbackPath = "/";
+ },
+ ex => Assert.Equal("Provide Authority, MetadataAddress, Configuration, or ConfigurationManager to OpenIdConnectOptions", ex.Message)
+ );
+ }
+
+ [Fact]
+ public Task ThrowsWhenAuthorityIsNotHttps()
+ {
+ return TestConfigurationException<InvalidOperationException>(
+ o =>
+ {
+ o.SignInScheme = "TestScheme";
+ o.ClientId = "Test Id";
+ o.MetadataAddress = "http://example.com";
+ o.CallbackPath = "/";
+ },
+ ex => Assert.Equal("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false.", ex.Message)
+ );
+ }
+
+ [Fact]
+ public Task ThrowsWhenMetadataAddressIsNotHttps()
+ {
+ return TestConfigurationException<InvalidOperationException>(
+ o =>
+ {
+ o.SignInScheme = "TestScheme";
+ o.ClientId = "Test Id";
+ o.MetadataAddress = "http://example.com";
+ o.CallbackPath = "/";
+ },
+ ex => Assert.Equal("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false.", ex.Message)
+ );
+ }
+
+ [Fact]
+ public Task ThrowsWhenMaxAgeIsNegative()
+ {
+ return TestConfigurationException<ArgumentOutOfRangeException>(
+ o =>
+ {
+ o.SignInScheme = "TestScheme";
+ o.ClientId = "Test Id";
+ o.Authority = TestServerBuilder.DefaultAuthority;
+ o.MaxAge = TimeSpan.FromSeconds(-1);
+ },
+ ex => Assert.StartsWith("The value must not be a negative TimeSpan.", ex.Message)
+ );
+ }
+
+ private TestServer BuildTestServer(Action<OpenIdConnectOptions> options)
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddAuthentication()
+ .AddCookie()
+ .AddOpenIdConnect(options);
+ })
+ .Configure(app => app.UseAuthentication());
+
+ return new TestServer(builder);
+ }
+
+ private async Task TestConfigurationException<T>(
+ Action<OpenIdConnectOptions> options,
+ Action<T> verifyException)
+ where T : Exception
+ {
+ var exception = await Assert.ThrowsAsync<T>(() => BuildTestServer(options).SendAsync(@"https://example.com"));
+ verifyException(exception);
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs
new file mode 100644
index 0000000000..7530b00c31
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs
@@ -0,0 +1,1347 @@
+// 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.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Primitives;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+using Microsoft.IdentityModel.Tokens;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
+{
+ public class OpenIdConnectEventTests
+ {
+ private readonly RequestDelegate AppWritePath = context => context.Response.WriteAsync(context.Request.Path);
+ private readonly RequestDelegate AppNotImpl = context => { throw new NotImplementedException("App"); };
+
+ [Fact]
+ public async Task OnMessageReceived_Skip_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ };
+ events.OnMessageReceived = context =>
+ {
+ context.SkipHandler();
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var response = await PostAsync(server, "signin-oidc", "");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnMessageReceived_Fail_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectRemoteFailure = true,
+ };
+ events.OnMessageReceived = context =>
+ {
+ context.Fail("Authentication was aborted from user code.");
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var exception = await Assert.ThrowsAsync<Exception>(delegate
+ {
+ return PostAsync(server, "signin-oidc", "");
+ });
+
+ Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnMessageReceived_Handled_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ };
+ events.OnMessageReceived = context =>
+ {
+ context.HandleResponse();
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTokenValidated_Skip_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ };
+ events.OnTokenValidated = context =>
+ {
+ context.SkipHandler();
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTokenValidated_Fail_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectRemoteFailure = true,
+ };
+ events.OnTokenValidated = context =>
+ {
+ context.Fail("Authentication was aborted from user code.");
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var exception = await Assert.ThrowsAsync<Exception>(delegate
+ {
+ return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state");
+ });
+
+ Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTokenValidated_HandledWithoutTicket_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ };
+ events.OnTokenValidated = context =>
+ {
+ context.HandleResponse();
+ context.Principal = null;
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTokenValidated_HandledWithTicket_SkipToTicketReceived()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectTicketReceived = true,
+ };
+ events.OnTokenValidated = context =>
+ {
+ context.HandleResponse();
+ context.Principal = null;
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ events.OnTokenValidated = context =>
+ {
+ context.Success();
+ return Task.FromResult(0);
+ };
+ events.OnTicketReceived = context =>
+ {
+ context.HandleResponse();
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnAuthorizationCodeReceived_Skip_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ };
+ events.OnAuthorizationCodeReceived = context =>
+ {
+ context.SkipHandler();
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnAuthorizationCodeReceived_Fail_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectRemoteFailure = true,
+ };
+ events.OnAuthorizationCodeReceived = context =>
+ {
+ context.Fail("Authentication was aborted from user code.");
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var exception = await Assert.ThrowsAsync<Exception>(delegate
+ {
+ return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+ });
+
+ Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnAuthorizationCodeReceived_HandledWithoutTicket_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ };
+ events.OnAuthorizationCodeReceived = context =>
+ {
+ context.HandleResponse();
+ context.Principal = null;
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnAuthorizationCodeReceived_HandledWithTicket_SkipToTicketReceived()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTicketReceived = true,
+ };
+ events.OnAuthorizationCodeReceived = context =>
+ {
+ context.Success();
+ return Task.FromResult(0);
+ };
+ events.OnTicketReceived = context =>
+ {
+ context.HandleResponse();
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTokenResponseReceived_Skip_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ };
+ events.OnTokenResponseReceived = context =>
+ {
+ context.SkipHandler();
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTokenResponseReceived_Fail_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectRemoteFailure = true,
+ };
+ events.OnTokenResponseReceived = context =>
+ {
+ context.Fail("Authentication was aborted from user code.");
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var exception = await Assert.ThrowsAsync<Exception>(delegate
+ {
+ return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+ });
+
+ Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTokenResponseReceived_HandledWithoutTicket_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ };
+ events.OnTokenResponseReceived = context =>
+ {
+ context.Principal = null;
+ context.HandleResponse();
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTokenResponseReceived_HandledWithTicket_SkipToTicketReceived()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectTicketReceived = true,
+ };
+ events.OnTokenResponseReceived = context =>
+ {
+ context.Success();
+ return Task.FromResult(0);
+ };
+ events.OnTicketReceived = context =>
+ {
+ context.HandleResponse();
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTokenValidatedBackchannel_Skip_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ };
+ events.OnTokenValidated = context =>
+ {
+ context.SkipHandler();
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTokenValidatedBackchannel_Fail_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectRemoteFailure = true,
+ };
+ events.OnTokenValidated = context =>
+ {
+ context.Fail("Authentication was aborted from user code.");
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var exception = await Assert.ThrowsAsync<Exception>(delegate
+ {
+ return PostAsync(server, "signin-oidc", "state=protected_state&code=my_code");
+ });
+
+ Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTokenValidatedBackchannel_HandledWithoutTicket_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ };
+ events.OnTokenValidated = context =>
+ {
+ context.Principal = null;
+ context.HandleResponse();
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTokenValidatedBackchannel_HandledWithTicket_SkipToTicketReceived()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectTicketReceived = true,
+ };
+ events.OnTokenValidated = context =>
+ {
+ context.Success();
+ return Task.FromResult(0);
+ };
+ events.OnTicketReceived = context =>
+ {
+ context.HandleResponse();
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnUserInformationReceived_Skip_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectUserInfoReceived = true,
+ };
+ events.OnUserInformationReceived = context =>
+ {
+ context.SkipHandler();
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnUserInformationReceived_Fail_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectUserInfoReceived = true,
+ ExpectRemoteFailure = true,
+ };
+ events.OnUserInformationReceived = context =>
+ {
+ context.Fail("Authentication was aborted from user code.");
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var exception = await Assert.ThrowsAsync<Exception>(delegate
+ {
+ return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+ });
+
+ Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnUserInformationReceived_HandledWithoutTicket_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectUserInfoReceived = true,
+ };
+ events.OnUserInformationReceived = context =>
+ {
+ context.Principal = null;
+ context.HandleResponse();
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnUserInformationReceived_HandledWithTicket_SkipToTicketReceived()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectUserInfoReceived = true,
+ ExpectTicketReceived = true,
+ };
+ events.OnUserInformationReceived = context =>
+ {
+ context.Success();
+ return Task.FromResult(0);
+ };
+ events.OnTicketReceived = context =>
+ {
+ context.HandleResponse();
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnAuthenticationFailed_Skip_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectUserInfoReceived = true,
+ ExpectAuthenticationFailed = true,
+ };
+ events.OnUserInformationReceived = context =>
+ {
+ throw new NotImplementedException("TestException");
+ };
+ events.OnAuthenticationFailed = context =>
+ {
+ Assert.Equal("TestException", context.Exception.Message);
+ context.SkipHandler();
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnAuthenticationFailed_Fail_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectUserInfoReceived = true,
+ ExpectAuthenticationFailed = true,
+ ExpectRemoteFailure = true,
+ };
+ events.OnUserInformationReceived = context =>
+ {
+ throw new NotImplementedException("TestException");
+ };
+ events.OnAuthenticationFailed = context =>
+ {
+ Assert.Equal("TestException", context.Exception.Message);
+ context.Fail("Authentication was aborted from user code.");
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var exception = await Assert.ThrowsAsync<Exception>(delegate
+ {
+ return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+ });
+
+ Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message);
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnAuthenticationFailed_HandledWithoutTicket_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectUserInfoReceived = true,
+ ExpectAuthenticationFailed = true,
+ };
+ events.OnUserInformationReceived = context =>
+ {
+ throw new NotImplementedException("TestException");
+ };
+ events.OnAuthenticationFailed = context =>
+ {
+ Assert.Equal("TestException", context.Exception.Message);
+ Assert.Null(context.Principal);
+ context.HandleResponse();
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnAuthenticationFailed_HandledWithTicket_SkipToTicketReceived()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectUserInfoReceived = true,
+ ExpectAuthenticationFailed = true,
+ ExpectTicketReceived = true,
+ };
+ events.OnUserInformationReceived = context =>
+ {
+ throw new NotImplementedException("TestException");
+ };
+ events.OnAuthenticationFailed = context =>
+ {
+ Assert.Equal("TestException", context.Exception.Message);
+ Assert.Null(context.Principal);
+
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"),
+ new Claim(ClaimTypes.Email, "bob@contoso.com"),
+ new Claim(ClaimsIdentity.DefaultNameClaimType, "bob")
+ };
+
+ context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
+ context.Success();
+ return Task.FromResult(0);
+ };
+ events.OnTicketReceived = context =>
+ {
+ context.HandleResponse();
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnRemoteFailure_Skip_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectUserInfoReceived = true,
+ ExpectAuthenticationFailed = true,
+ ExpectRemoteFailure = true,
+ };
+ events.OnUserInformationReceived = context =>
+ {
+ throw new NotImplementedException("TestException");
+ };
+ events.OnAuthenticationFailed = context =>
+ {
+ Assert.Equal("TestException", context.Exception.Message);
+ return Task.FromResult(0);
+ };
+ events.OnRemoteFailure = context =>
+ {
+ Assert.Equal("TestException", context.Failure.Message);
+ context.SkipHandler();
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnRemoteFailure_Handled_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectUserInfoReceived = true,
+ ExpectAuthenticationFailed = true,
+ ExpectRemoteFailure = true,
+ };
+ events.OnUserInformationReceived = context =>
+ {
+ throw new NotImplementedException("TestException");
+ };
+ events.OnRemoteFailure = context =>
+ {
+ Assert.Equal("TestException", context.Failure.Message);
+ Assert.Equal("testvalue", context.Properties.Items["testkey"]);
+ context.HandleResponse();
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTicketReceived_Skip_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectUserInfoReceived = true,
+ ExpectTicketReceived = true,
+ };
+ events.OnTicketReceived = context =>
+ {
+ context.SkipHandler();
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppWritePath);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnTicketReceived_Handled_NoMoreEventsRun()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectMessageReceived = true,
+ ExpectTokenValidated = true,
+ ExpectAuthorizationCodeReceived = true,
+ ExpectTokenResponseReceived = true,
+ ExpectUserInfoReceived = true,
+ ExpectTicketReceived = true,
+ };
+ events.OnTicketReceived = context =>
+ {
+ context.HandleResponse();
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.FromResult(0);
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnRedirectToIdentityProviderForSignOut_Invoked()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectRedirectForSignOut = true,
+ };
+ var server = CreateServer(events,
+ context =>
+ {
+ return context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
+ });
+
+ var client = server.CreateClient();
+ var response = await client.GetAsync("/");
+
+ Assert.Equal(HttpStatusCode.Found, response.StatusCode);
+ Assert.Equal("http://testhost/end", response.Headers.Location.GetLeftPart(UriPartial.Path));
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnRedirectToIdentityProviderForSignOut_Handled_RedirectNotInvoked()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectRedirectForSignOut = true,
+ };
+ events.OnRedirectToIdentityProviderForSignOut = context =>
+ {
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ context.HandleResponse();
+ return Task.CompletedTask;
+ };
+ var server = CreateServer(events,
+ context =>
+ {
+ return context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
+ });
+
+ var client = server.CreateClient();
+ var response = await client.GetAsync("/");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Null(response.Headers.Location);
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnRemoteSignOut_Invoked()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectRemoteSignOut = true,
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var client = server.CreateClient();
+ var response = await client.GetAsync("/signout-oidc");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ events.ValidateExpectations();
+ Assert.True(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values));
+ Assert.True(SetCookieHeaderValue.TryParseStrictList(values.ToList(), out var parsedValues));
+ Assert.Equal(1, parsedValues.Count);
+ Assert.True(StringSegment.IsNullOrEmpty(parsedValues.Single().Value));
+ }
+
+ [Fact]
+ public async Task OnRemoteSignOut_Handled_NoSignout()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectRemoteSignOut = true,
+ };
+ events.OnRemoteSignOut = context =>
+ {
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ context.HandleResponse();
+ return Task.CompletedTask;
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var client = server.CreateClient();
+ var response = await client.GetAsync("/signout-oidc");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ events.ValidateExpectations();
+ Assert.False(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values));
+ }
+
+ [Fact]
+ public async Task OnRemoteSignOut_Skip_NoSignout()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectRemoteSignOut = true,
+ };
+ events.OnRemoteSignOut = context =>
+ {
+ context.SkipHandler();
+ return Task.CompletedTask;
+ };
+ var server = CreateServer(events, context =>
+ {
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.CompletedTask;
+ });
+
+ var client = server.CreateClient();
+ var response = await client.GetAsync("/signout-oidc");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ events.ValidateExpectations();
+ Assert.False(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values));
+ }
+
+ [Fact]
+ public async Task OnRedirectToSignedOutRedirectUri_Invoked()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectRedirectToSignedOut = true,
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var client = server.CreateClient();
+ var response = await client.GetAsync("/signout-callback-oidc?state=protected_state");
+
+ Assert.Equal(HttpStatusCode.Found, response.StatusCode);
+ Assert.Equal("http://testhost/redirect", response.Headers.Location.AbsoluteUri);
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnRedirectToSignedOutRedirectUri_Handled_NoRedirect()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectRedirectToSignedOut = true,
+ };
+ events.OnSignedOutCallbackRedirect = context =>
+ {
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ context.HandleResponse();
+ return Task.CompletedTask;
+ };
+ var server = CreateServer(events, AppNotImpl);
+
+ var client = server.CreateClient();
+ var response = await client.GetAsync("/signout-callback-oidc?state=protected_state");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Null(response.Headers.Location);
+ events.ValidateExpectations();
+ }
+
+ [Fact]
+ public async Task OnRedirectToSignedOutRedirectUri_Skipped_NoRedirect()
+ {
+ var events = new ExpectedOidcEvents()
+ {
+ ExpectRedirectToSignedOut = true,
+ };
+ events.OnSignedOutCallbackRedirect = context =>
+ {
+ context.SkipHandler();
+ return Task.CompletedTask;
+ };
+ var server = CreateServer(events,
+ context =>
+ {
+ context.Response.StatusCode = StatusCodes.Status202Accepted;
+ return Task.CompletedTask;
+ });
+
+ var client = server.CreateClient();
+ var response = await client.GetAsync("/signout-callback-oidc?state=protected_state");
+
+ Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
+ Assert.Null(response.Headers.Location);
+ events.ValidateExpectations();
+ }
+
+ private class ExpectedOidcEvents : OpenIdConnectEvents
+ {
+ public bool ExpectMessageReceived { get; set; }
+ public bool InvokedMessageReceived { get; set; }
+
+ public bool ExpectTokenValidated { get; set; }
+ public bool InvokedTokenValidated { get; set; }
+
+ public bool ExpectRemoteFailure { get; set; }
+ public bool InvokedRemoteFailure { get; set; }
+
+ public bool ExpectTicketReceived { get; set; }
+ public bool InvokedTicketReceived { get; set; }
+
+ public bool ExpectAuthorizationCodeReceived { get; set; }
+ public bool InvokedAuthorizationCodeReceived { get; set; }
+
+ public bool ExpectTokenResponseReceived { get; set; }
+ public bool InvokedTokenResponseReceived { get; set; }
+
+ public bool ExpectUserInfoReceived { get; set; }
+ public bool InvokedUserInfoReceived { get; set; }
+
+ public bool ExpectAuthenticationFailed { get; set; }
+ public bool InvokeAuthenticationFailed { get; set; }
+
+ public bool ExpectRedirectForSignOut { get; set; }
+ public bool InvokedRedirectForSignOut { get; set; }
+
+ public bool ExpectRemoteSignOut { get; set; }
+ public bool InvokedRemoteSignOut { get; set; }
+
+ public bool ExpectRedirectToSignedOut { get; set; }
+ public bool InvokedRedirectToSignedOut { get; set; }
+
+ public override Task MessageReceived(MessageReceivedContext context)
+ {
+ InvokedMessageReceived = true;
+ return base.MessageReceived(context);
+ }
+
+ public override Task TokenValidated(TokenValidatedContext context)
+ {
+ InvokedTokenValidated = true;
+ return base.TokenValidated(context);
+ }
+
+ public override Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
+ {
+ InvokedAuthorizationCodeReceived = true;
+ return base.AuthorizationCodeReceived(context);
+ }
+
+ public override Task TokenResponseReceived(TokenResponseReceivedContext context)
+ {
+ InvokedTokenResponseReceived = true;
+ return base.TokenResponseReceived(context);
+ }
+
+ public override Task UserInformationReceived(UserInformationReceivedContext context)
+ {
+ InvokedUserInfoReceived = true;
+ return base.UserInformationReceived(context);
+ }
+
+ public override Task AuthenticationFailed(AuthenticationFailedContext context)
+ {
+ InvokeAuthenticationFailed = true;
+ return base.AuthenticationFailed(context);
+ }
+
+ public override Task TicketReceived(TicketReceivedContext context)
+ {
+ InvokedTicketReceived = true;
+ return base.TicketReceived(context);
+ }
+
+ public override Task RemoteFailure(RemoteFailureContext context)
+ {
+ InvokedRemoteFailure = true;
+ return base.RemoteFailure(context);
+ }
+
+ public override Task RedirectToIdentityProviderForSignOut(RedirectContext context)
+ {
+ InvokedRedirectForSignOut = true;
+ return base.RedirectToIdentityProviderForSignOut(context);
+ }
+
+ public override Task RemoteSignOut(RemoteSignOutContext context)
+ {
+ InvokedRemoteSignOut = true;
+ return base.RemoteSignOut(context);
+ }
+
+ public override Task SignedOutCallbackRedirect(RemoteSignOutContext context)
+ {
+ InvokedRedirectToSignedOut = true;
+ return base.SignedOutCallbackRedirect(context);
+ }
+
+ public void ValidateExpectations()
+ {
+ Assert.Equal(ExpectMessageReceived, InvokedMessageReceived);
+ Assert.Equal(ExpectTokenValidated, InvokedTokenValidated);
+ Assert.Equal(ExpectAuthorizationCodeReceived, InvokedAuthorizationCodeReceived);
+ Assert.Equal(ExpectTokenResponseReceived, InvokedTokenResponseReceived);
+ Assert.Equal(ExpectUserInfoReceived, InvokedUserInfoReceived);
+ Assert.Equal(ExpectAuthenticationFailed, InvokeAuthenticationFailed);
+ Assert.Equal(ExpectTicketReceived, InvokedTicketReceived);
+ Assert.Equal(ExpectRemoteFailure, InvokedRemoteFailure);
+ Assert.Equal(ExpectRedirectForSignOut, InvokedRedirectForSignOut);
+ Assert.Equal(ExpectRemoteSignOut, InvokedRemoteSignOut);
+ Assert.Equal(ExpectRedirectToSignedOut, InvokedRedirectToSignedOut);
+ }
+ }
+
+ private TestServer CreateServer(OpenIdConnectEvents events, RequestDelegate appCode)
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddAuthentication(auth =>
+ {
+ auth.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
+ })
+ .AddCookie()
+ .AddOpenIdConnect(o =>
+ {
+ o.Events = events;
+ o.ClientId = "ClientId";
+ o.GetClaimsFromUserInfoEndpoint = true;
+ o.Configuration = new OpenIdConnectConfiguration()
+ {
+ TokenEndpoint = "http://testhost/tokens",
+ UserInfoEndpoint = "http://testhost/user",
+ EndSessionEndpoint = "http://testhost/end"
+ };
+ o.StateDataFormat = new TestStateDataFormat();
+ o.SecurityTokenValidator = new TestTokenValidator();
+ o.ProtocolValidator = new TestProtocolValidator();
+ o.BackchannelHttpHandler = new TestBackchannel();
+ });
+ })
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Run(appCode);
+ });
+
+ return new TestServer(builder);
+ }
+
+ private Task<HttpResponseMessage> PostAsync(TestServer server, string path, string form)
+ {
+ var client = server.CreateClient();
+ var cookie = ".AspNetCore.Correlation." + OpenIdConnectDefaults.AuthenticationScheme + ".corrilationId=N";
+ client.DefaultRequestHeaders.Add("Cookie", cookie);
+ return client.PostAsync("signin-oidc",
+ new StringContent(form, Encoding.ASCII, "application/x-www-form-urlencoded"));
+ }
+
+ private class TestStateDataFormat : ISecureDataFormat<AuthenticationProperties>
+ {
+ private AuthenticationProperties Data { get; set; }
+
+ public string Protect(AuthenticationProperties data)
+ {
+ return "protected_state";
+ }
+
+ public string Protect(AuthenticationProperties data, string purpose)
+ {
+ throw new NotImplementedException();
+ }
+
+ public AuthenticationProperties Unprotect(string protectedText)
+ {
+ Assert.Equal("protected_state", protectedText);
+ var properties = new AuthenticationProperties(new Dictionary<string, string>()
+ {
+ { ".xsrf", "corrilationId" },
+ { OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, "redirect_uri" },
+ { "testkey", "testvalue" }
+ });
+ properties.RedirectUri = "http://testhost/redirect";
+ return properties;
+ }
+
+ public AuthenticationProperties Unprotect(string protectedText, string purpose)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class TestTokenValidator : ISecurityTokenValidator
+ {
+ public bool CanValidateToken => true;
+
+ public int MaximumTokenSizeInBytes
+ {
+ get { return 1024; }
+ set { throw new NotImplementedException(); }
+ }
+
+ public bool CanReadToken(string securityToken)
+ {
+ Assert.Equal("my_id_token", securityToken);
+ return true;
+ }
+
+ public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
+ {
+ Assert.Equal("my_id_token", securityToken);
+ validatedToken = new JwtSecurityToken();
+ return new ClaimsPrincipal(new ClaimsIdentity("customAuthType"));
+ }
+ }
+
+ private class TestProtocolValidator : OpenIdConnectProtocolValidator
+ {
+ public override void ValidateAuthenticationResponse(OpenIdConnectProtocolValidationContext validationContext)
+ {
+ }
+
+ public override void ValidateTokenResponse(OpenIdConnectProtocolValidationContext validationContext)
+ {
+ }
+
+ public override void ValidateUserInfoResponse(OpenIdConnectProtocolValidationContext validationContext)
+ {
+ }
+ }
+
+ private class TestBackchannel : HttpMessageHandler
+ {
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ if (string.Equals("/tokens", request.RequestUri.AbsolutePath, StringComparison.Ordinal))
+ {
+ return Task.FromResult(new HttpResponseMessage() { Content =
+ new StringContent("{ \"id_token\": \"my_id_token\", \"access_token\": \"my_access_token\" }", Encoding.ASCII, "application/json") });
+ }
+ if (string.Equals("/user", request.RequestUri.AbsolutePath, StringComparison.Ordinal))
+ {
+ return Task.FromResult(new HttpResponseMessage() { Content = new StringContent("{ }", Encoding.ASCII, "application/json") });
+ }
+
+ throw new NotImplementedException(request.RequestUri.ToString());
+ }
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectTests.cs
new file mode 100644
index 0000000000..da52e0e4cb
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectTests.cs
@@ -0,0 +1,351 @@
+// 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.Globalization;
+using System.Linq;
+using System.Net;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
+{
+ public class OpenIdConnectTests
+ {
+ static string noncePrefix = "OpenIdConnect." + "Nonce.";
+ static string nonceDelimiter = ".";
+ const string DefaultHost = @"https://example.com";
+ const string Logout = "/logout";
+ const string Signin = "/signin";
+ const string Signout = "/signout";
+
+ /// <summary>
+ /// Tests RedirectForSignOutContext replaces the OpenIdConnectMesssage correctly.
+ /// </summary>
+ /// <returns>Task</returns>
+ [Fact]
+ public async Task SignOutSettingMessage()
+ {
+ var setting = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ opt.Configuration = new OpenIdConnectConfiguration
+ {
+ EndSessionEndpoint = "https://example.com/signout_test/signout_request"
+ };
+ });
+
+ var server = setting.CreateTestServer();
+
+ var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout);
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.NotNull(res.Headers.Location);
+
+ setting.ValidateSignoutRedirect(
+ transaction.Response.Headers.Location,
+ OpenIdConnectParameterNames.SkuTelemetry,
+ OpenIdConnectParameterNames.VersionTelemetry);
+ }
+
+ [Fact]
+ public async Task RedirectToIdentityProvider_SetsNonceCookiePath_ToCallBackPath()
+ {
+ var setting = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ opt.Configuration = new OpenIdConnectConfiguration
+ {
+ AuthorizationEndpoint = "https://example.com/provider/login"
+ };
+ });
+
+ var server = setting.CreateTestServer();
+
+ var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Challenge);
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.NotNull(res.Headers.Location);
+ var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie");
+ var nonce = Assert.Single(setCookie.Value, v => v.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix));
+ Assert.Contains("path=/signin-oidc", nonce);
+ }
+
+ [Fact]
+ public async Task RedirectToIdentityProvider_NonceCookieOptions_CanBeOverriden()
+ {
+ var setting = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ opt.Configuration = new OpenIdConnectConfiguration
+ {
+ AuthorizationEndpoint = "https://example.com/provider/login"
+ };
+ opt.NonceCookie.Path = "/";
+ });
+
+ var server = setting.CreateTestServer();
+
+ var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Challenge);
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.NotNull(res.Headers.Location);
+ var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie");
+ var nonce = Assert.Single(setCookie.Value, v => v.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix));
+ Assert.Contains("path=/", nonce);
+ }
+
+ [Fact]
+ public async Task RedirectToIdentityProvider_SetsCorrelationIdCookiePath_ToCallBackPath()
+ {
+ var setting = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ opt.Configuration = new OpenIdConnectConfiguration
+ {
+ AuthorizationEndpoint = "https://example.com/provider/login"
+ };
+ });
+
+ var server = setting.CreateTestServer();
+
+ var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Challenge);
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.NotNull(res.Headers.Location);
+ var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie");
+ var correlation = Assert.Single(setCookie.Value, v => v.StartsWith(".AspNetCore.Correlation."));
+ Assert.Contains("path=/signin-oidc", correlation);
+ }
+
+ [Fact]
+ public async Task RedirectToIdentityProvider_CorrelationIdCookieOptions_CanBeOverriden()
+ {
+ var setting = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ opt.Configuration = new OpenIdConnectConfiguration
+ {
+ AuthorizationEndpoint = "https://example.com/provider/login"
+ };
+ opt.CorrelationCookie.Path = "/";
+ });
+
+ var server = setting.CreateTestServer();
+
+ var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Challenge);
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.NotNull(res.Headers.Location);
+ var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie");
+ var correlation = Assert.Single(setCookie.Value, v => v.StartsWith(".AspNetCore.Correlation."));
+ Assert.Contains("path=/", correlation);
+ }
+
+ [Fact]
+ public async Task EndSessionRequestDoesNotIncludeTelemetryParametersWhenDisabled()
+ {
+ var configuration = TestServerBuilder.CreateDefaultOpenIdConnectConfiguration();
+ var setting = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.Configuration = configuration;
+ opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ opt.DisableTelemetry = true;
+ });
+
+ var server = setting.CreateTestServer();
+
+ var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout);
+ var res = transaction.Response;
+
+ Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
+ Assert.DoesNotContain(OpenIdConnectParameterNames.SkuTelemetry, res.Headers.Location.Query);
+ Assert.DoesNotContain(OpenIdConnectParameterNames.VersionTelemetry, res.Headers.Location.Query);
+ setting.ValidateSignoutRedirect(transaction.Response.Headers.Location);
+ }
+
+ [Fact]
+ public async Task SignOutFormPostWithDefaultRedirectUri()
+ {
+ var settings = new TestSettings(o =>
+ {
+ o.AuthenticationMethod = OpenIdConnectRedirectBehavior.FormPost;
+ o.Authority = TestServerBuilder.DefaultAuthority;
+ o.ClientId = "Test Id";
+ });
+ var server = settings.CreateTestServer();
+
+ var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout);
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+
+ settings.ValidateSignoutFormPost(transaction,
+ OpenIdConnectParameterNames.PostLogoutRedirectUri);
+ }
+
+ [Fact]
+ public async Task SignOutRedirectWithDefaultRedirectUri()
+ {
+ var settings = new TestSettings(o =>
+ {
+ o.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
+ o.Authority = TestServerBuilder.DefaultAuthority;
+ o.ClientId = "Test Id";
+ });
+ var server = settings.CreateTestServer();
+
+ var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout);
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+
+ settings.ValidateSignoutRedirect(transaction.Response.Headers.Location,
+ OpenIdConnectParameterNames.PostLogoutRedirectUri);
+ }
+
+ [Fact]
+ public async Task SignOutWithCustomRedirectUri()
+ {
+ var configuration = TestServerBuilder.CreateDefaultOpenIdConnectConfiguration();
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("OIDCTest"));
+ var server = TestServerBuilder.CreateServer(o =>
+ {
+ o.Authority = TestServerBuilder.DefaultAuthority;
+ o.ClientId = "Test Id";
+ o.Configuration = configuration;
+ o.StateDataFormat = stateFormat;
+ o.SignedOutCallbackPath = "/thelogout";
+ o.SignedOutRedirectUri = "https://example.com/postlogout";
+ });
+
+ var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout);
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+
+ var query = transaction.Response.Headers.Location.Query.Substring(1).Split('&')
+ .Select(each => each.Split('='))
+ .ToDictionary(pair => pair[0], pair => pair[1]);
+
+ string redirectUri;
+ Assert.True(query.TryGetValue("post_logout_redirect_uri", out redirectUri));
+ Assert.Equal(UrlEncoder.Default.Encode("https://example.com/thelogout"), redirectUri, true);
+
+ string state;
+ Assert.True(query.TryGetValue("state", out state));
+ var properties = stateFormat.Unprotect(state);
+ Assert.Equal("https://example.com/postlogout", properties.RedirectUri, true);
+ }
+
+ [Fact]
+ public async Task SignOutWith_Specific_RedirectUri_From_Authentication_Properites()
+ {
+ var configuration = TestServerBuilder.CreateDefaultOpenIdConnectConfiguration();
+ var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("OIDCTest"));
+ var server = TestServerBuilder.CreateServer(o =>
+ {
+ o.Authority = TestServerBuilder.DefaultAuthority;
+ o.StateDataFormat = stateFormat;
+ o.ClientId = "Test Id";
+ o.Configuration = configuration;
+ o.SignedOutRedirectUri = "https://example.com/postlogout";
+ });
+
+ var transaction = await server.SendAsync("https://example.com/signout_with_specific_redirect_uri");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+
+ var query = transaction.Response.Headers.Location.Query.Substring(1).Split('&')
+ .Select(each => each.Split('='))
+ .ToDictionary(pair => pair[0], pair => pair[1]);
+
+ string redirectUri;
+ Assert.True(query.TryGetValue("post_logout_redirect_uri", out redirectUri));
+ Assert.Equal(UrlEncoder.Default.Encode("https://example.com/signout-callback-oidc"), redirectUri, true);
+
+ string state;
+ Assert.True(query.TryGetValue("state", out state));
+ var properties = stateFormat.Unprotect(state);
+ Assert.Equal("http://www.example.com/specific_redirect_uri", properties.RedirectUri, true);
+ }
+
+ [Fact]
+ public async Task SignOut_WithMissingConfig_Throws()
+ {
+ var setting = new TestSettings(opt =>
+ {
+ opt.ClientId = "Test Id";
+ opt.Configuration = new OpenIdConnectConfiguration();
+ });
+ var server = setting.CreateTestServer();
+
+ var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync(DefaultHost + TestServerBuilder.Signout));
+ Assert.Equal("Cannot redirect to the end session endpoint, the configuration may be missing or invalid.", exception.Message);
+ }
+
+ // Test Cases for calculating the expiration time of cookie from cookie name
+ [Fact]
+ public void NonceCookieExpirationTime()
+ {
+ DateTime utcNow = DateTime.UtcNow;
+
+ Assert.Equal(DateTime.MaxValue, GetNonceExpirationTime(noncePrefix + DateTime.MaxValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)));
+
+ Assert.Equal(DateTime.MinValue + TimeSpan.FromHours(1), GetNonceExpirationTime(noncePrefix + DateTime.MinValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)));
+
+ Assert.Equal(utcNow + TimeSpan.FromHours(1), GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)));
+
+ Assert.Equal(DateTime.MinValue, GetNonceExpirationTime(noncePrefix, TimeSpan.FromHours(1)));
+
+ Assert.Equal(DateTime.MinValue, GetNonceExpirationTime("", TimeSpan.FromHours(1)));
+
+ Assert.Equal(DateTime.MinValue, GetNonceExpirationTime(noncePrefix + noncePrefix, TimeSpan.FromHours(1)));
+
+ Assert.Equal(utcNow + TimeSpan.FromHours(1), GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)));
+
+ Assert.Equal(DateTime.MinValue, GetNonceExpirationTime(utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)));
+ }
+
+ private static DateTime GetNonceExpirationTime(string keyname, TimeSpan nonceLifetime)
+ {
+ DateTime nonceTime = DateTime.MinValue;
+ string timestamp = null;
+ int endOfTimestamp;
+ if (keyname.StartsWith(noncePrefix, StringComparison.Ordinal))
+ {
+ timestamp = keyname.Substring(noncePrefix.Length);
+ endOfTimestamp = timestamp.IndexOf('.');
+
+ if (endOfTimestamp != -1)
+ {
+ timestamp = timestamp.Substring(0, endOfTimestamp);
+ try
+ {
+ nonceTime = DateTime.FromBinary(Convert.ToInt64(timestamp, CultureInfo.InvariantCulture));
+ if ((nonceTime >= DateTime.UtcNow) && ((DateTime.MaxValue - nonceTime) < nonceLifetime))
+ nonceTime = DateTime.MaxValue;
+ else
+ nonceTime += nonceLifetime;
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ return nonceTime;
+ }
+
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerBuilder.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerBuilder.cs
new file mode 100644
index 0000000000..c37da8c043
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerBuilder.cs
@@ -0,0 +1,120 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.IdentityModel.Protocols;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+
+namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
+{
+ internal class TestServerBuilder
+ {
+ public static readonly string DefaultAuthority = @"https://login.microsoftonline.com/common";
+ public static readonly string TestHost = @"https://example.com";
+ public static readonly string Challenge = "/challenge";
+ public static readonly string ChallengeWithOutContext = "/challengeWithOutContext";
+ public static readonly string ChallengeWithProperties = "/challengeWithProperties";
+ public static readonly string Signin = "/signin";
+ public static readonly string Signout = "/signout";
+
+ public static OpenIdConnectOptions CreateOpenIdConnectOptions() =>
+ new OpenIdConnectOptions
+ {
+ Authority = DefaultAuthority,
+ ClientId = Guid.NewGuid().ToString(),
+ Configuration = CreateDefaultOpenIdConnectConfiguration()
+ };
+
+ public static OpenIdConnectOptions CreateOpenIdConnectOptions(Action<OpenIdConnectOptions> update)
+ {
+ var options = CreateOpenIdConnectOptions();
+ update?.Invoke(options);
+ return options;
+ }
+
+ public static OpenIdConnectConfiguration CreateDefaultOpenIdConnectConfiguration() =>
+ new OpenIdConnectConfiguration()
+ {
+ AuthorizationEndpoint = DefaultAuthority + "/oauth2/authorize",
+ EndSessionEndpoint = DefaultAuthority + "/oauth2/endsessionendpoint",
+ TokenEndpoint = DefaultAuthority + "/oauth2/token"
+ };
+
+ public static IConfigurationManager<OpenIdConnectConfiguration> CreateDefaultOpenIdConnectConfigurationManager() =>
+ new StaticConfigurationManager<OpenIdConnectConfiguration>(CreateDefaultOpenIdConnectConfiguration());
+
+ public static TestServer CreateServer(Action<OpenIdConnectOptions> options)
+ {
+ return CreateServer(options, handler: null, properties: null);
+ }
+
+ public static TestServer CreateServer(
+ Action<OpenIdConnectOptions> options,
+ Func<HttpContext, Task> handler,
+ AuthenticationProperties properties)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Use(async (context, next) =>
+ {
+ var req = context.Request;
+ var res = context.Response;
+
+ if (req.Path == new PathString(Challenge))
+ {
+ await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme);
+ }
+ else if (req.Path == new PathString(ChallengeWithProperties))
+ {
+ await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, properties);
+ }
+ else if (req.Path == new PathString(ChallengeWithOutContext))
+ {
+ res.StatusCode = 401;
+ }
+ else if (req.Path == new PathString(Signin))
+ {
+ await context.SignInAsync(OpenIdConnectDefaults.AuthenticationScheme, new ClaimsPrincipal());
+ }
+ else if (req.Path == new PathString(Signout))
+ {
+ await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
+ }
+ else if (req.Path == new PathString("/signout_with_specific_redirect_uri"))
+ {
+ await context.SignOutAsync(
+ OpenIdConnectDefaults.AuthenticationScheme,
+ new AuthenticationProperties() { RedirectUri = "http://www.example.com/specific_redirect_uri" });
+ }
+ else if (handler != null)
+ {
+ await handler(context);
+ }
+ else
+ {
+ await next();
+ }
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
+ .AddCookie()
+ .AddOpenIdConnect(options);
+ });
+
+ return new TestServer(builder);
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerExtensions.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerExtensions.cs
new file mode 100644
index 0000000000..609aed6f6a
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerExtensions.cs
@@ -0,0 +1,49 @@
+// 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.Net.Http;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.TestHost;
+
+namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
+{
+ internal static class TestServerExtensions
+ {
+ public static Task<TestTransaction> SendAsync(this TestServer server, string url)
+ {
+ return SendAsync(server, url, cookieHeader: null);
+ }
+
+ public static async Task<TestTransaction> SendAsync(this TestServer server, string uri, string cookieHeader)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, uri);
+ if (!string.IsNullOrEmpty(cookieHeader))
+ {
+ request.Headers.Add("Cookie", cookieHeader);
+ }
+
+ var transaction = new TestTransaction
+ {
+ Request = request,
+ Response = await server.CreateClient().SendAsync(request),
+ };
+
+ if (transaction.Response.Headers.Contains("Set-Cookie"))
+ {
+ transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList();
+ }
+
+ transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync();
+ if (transaction.Response.Content != null &&
+ transaction.Response.Content.Headers.ContentType != null &&
+ transaction.Response.Content.Headers.ContentType.MediaType == "text/xml")
+ {
+ transaction.ResponseElement = XElement.Parse(transaction.ResponseText);
+ }
+
+ return transaction;
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs
new file mode 100644
index 0000000000..a1e0233f3a
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs
@@ -0,0 +1,351 @@
+// 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.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Reflection;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
+{
+ /// <summary>
+ /// This helper class is used to check that query string parameters are as expected.
+ /// </summary>
+ internal class TestSettings
+ {
+ private readonly Action<OpenIdConnectOptions> _configureOptions;
+ private OpenIdConnectOptions _options;
+
+ public TestSettings() : this(configure: null)
+ {
+ }
+
+ public TestSettings(Action<OpenIdConnectOptions> configure)
+ {
+ _configureOptions = o =>
+ {
+ configure?.Invoke(o);
+ _options = o;
+ _options.BackchannelHttpHandler = new MockBackchannel();
+ };
+ }
+
+ public UrlEncoder Encoder => UrlEncoder.Default;
+
+ public string ExpectedState { get; set; }
+
+ public TestServer CreateTestServer(AuthenticationProperties properties = null) => TestServerBuilder.CreateServer(_configureOptions, handler: null, properties: properties);
+
+ public IDictionary<string, string> ValidateChallengeFormPost(string responseBody, params string[] parametersToValidate)
+ {
+ IDictionary<string, string> formInputs = null;
+ var errors = new List<string>();
+ var xdoc = XDocument.Parse(responseBody.Replace("doctype", "DOCTYPE"));
+ var forms = xdoc.Descendants("form");
+ if (forms.Count() != 1)
+ {
+ errors.Add("Only one form element is expected in response body.");
+ }
+ else
+ {
+ formInputs = forms.Single()
+ .Elements("input")
+ .ToDictionary(elem => elem.Attribute("name").Value,
+ elem => elem.Attribute("value").Value);
+
+ ValidateParameters(formInputs, parametersToValidate, errors, htmlEncoded: false);
+ }
+
+ if (errors.Any())
+ {
+ var buf = new StringBuilder();
+ buf.AppendLine($"The challenge form post is not valid.");
+ // buf.AppendLine();
+
+ foreach (var error in errors)
+ {
+ buf.AppendLine(error);
+ }
+
+ Debug.WriteLine(buf.ToString());
+ Assert.True(false, buf.ToString());
+ }
+
+ return formInputs;
+ }
+
+ public IDictionary<string, string> ValidateSignoutFormPost(TestTransaction transaction, params string[] parametersToValidate)
+ {
+ IDictionary<string, string> formInputs = null;
+ var errors = new List<string>();
+ var xdoc = XDocument.Parse(transaction.ResponseText.Replace("doctype", "DOCTYPE"));
+ var forms = xdoc.Descendants("form");
+ if (forms.Count() != 1)
+ {
+ errors.Add("Only one form element is expected in response body.");
+ }
+ else
+ {
+ formInputs = forms.Single()
+ .Elements("input")
+ .ToDictionary(elem => elem.Attribute("name").Value,
+ elem => elem.Attribute("value").Value);
+
+ ValidateParameters(formInputs, parametersToValidate, errors, htmlEncoded: false);
+ }
+
+ if (errors.Any())
+ {
+ var buf = new StringBuilder();
+ buf.AppendLine($"The signout form post is not valid.");
+ // buf.AppendLine();
+
+ foreach (var error in errors)
+ {
+ buf.AppendLine(error);
+ }
+
+ Debug.WriteLine(buf.ToString());
+ Assert.True(false, buf.ToString());
+ }
+
+ return formInputs;
+ }
+
+ public IDictionary<string, string> ValidateChallengeRedirect(Uri redirectUri, params string[] parametersToValidate) =>
+ ValidateRedirectCore(redirectUri, OpenIdConnectRequestType.Authentication, parametersToValidate);
+
+ public IDictionary<string, string> ValidateSignoutRedirect(Uri redirectUri, params string[] parametersToValidate) =>
+ ValidateRedirectCore(redirectUri, OpenIdConnectRequestType.Logout, parametersToValidate);
+
+ private IDictionary<string, string> ValidateRedirectCore(Uri redirectUri, OpenIdConnectRequestType requestType, string[] parametersToValidate)
+ {
+ var errors = new List<string>();
+
+ // Validate the authority
+ ValidateExpectedAuthority(redirectUri.AbsoluteUri, errors, requestType);
+
+ // Convert query to dictionary
+ var queryDict = string.IsNullOrEmpty(redirectUri.Query) ?
+ new Dictionary<string, string>() :
+ redirectUri.Query.TrimStart('?').Split('&').Select(part => part.Split('=')).ToDictionary(parts => parts[0], parts => parts[1]);
+
+ // Validate the query string parameters
+ ValidateParameters(queryDict, parametersToValidate, errors, htmlEncoded: true);
+
+ if (errors.Any())
+ {
+ var buf = new StringBuilder();
+ buf.AppendLine($"The redirect uri is not valid.");
+ buf.AppendLine(redirectUri.AbsoluteUri);
+
+ foreach (var error in errors)
+ {
+ buf.AppendLine(error);
+ }
+
+ Debug.WriteLine(buf.ToString());
+ Assert.True(false, buf.ToString());
+ }
+
+ return queryDict;
+ }
+
+ private void ValidateParameters(
+ IDictionary<string, string> actualValues,
+ IEnumerable<string> parametersToValidate,
+ ICollection<string> errors,
+ bool htmlEncoded)
+ {
+ foreach (var paramToValidate in parametersToValidate)
+ {
+ switch (paramToValidate)
+ {
+ case OpenIdConnectParameterNames.ClientId:
+ ValidateClientId(actualValues, errors, htmlEncoded);
+ break;
+ case OpenIdConnectParameterNames.ResponseType:
+ ValidateResponseType(actualValues, errors, htmlEncoded);
+ break;
+ case OpenIdConnectParameterNames.ResponseMode:
+ ValidateResponseMode(actualValues, errors, htmlEncoded);
+ break;
+ case OpenIdConnectParameterNames.Scope:
+ ValidateScope(actualValues, errors, htmlEncoded);
+ break;
+ case OpenIdConnectParameterNames.RedirectUri:
+ ValidateRedirectUri(actualValues, errors, htmlEncoded);
+ break;
+ case OpenIdConnectParameterNames.Resource:
+ ValidateResource(actualValues, errors, htmlEncoded);
+ break;
+ case OpenIdConnectParameterNames.State:
+ ValidateState(actualValues, errors, htmlEncoded);
+ break;
+ case OpenIdConnectParameterNames.SkuTelemetry:
+ ValidateSkuTelemetry(actualValues, errors, htmlEncoded);
+ break;
+ case OpenIdConnectParameterNames.VersionTelemetry:
+ ValidateVersionTelemetry(actualValues, errors, htmlEncoded);
+ break;
+ case OpenIdConnectParameterNames.PostLogoutRedirectUri:
+ ValidatePostLogoutRedirectUri(actualValues, errors, htmlEncoded);
+ break;
+ case OpenIdConnectParameterNames.MaxAge:
+ ValidateMaxAge(actualValues, errors, htmlEncoded);
+ break;
+ case OpenIdConnectParameterNames.Prompt:
+ ValidatePrompt(actualValues, errors, htmlEncoded);
+ break;
+ default:
+ throw new InvalidOperationException($"Unknown parameter \"{paramToValidate}\".");
+ }
+ }
+ }
+
+ private void ValidateExpectedAuthority(string absoluteUri, ICollection<string> errors, OpenIdConnectRequestType requestType)
+ {
+ string expectedAuthority;
+ switch (requestType)
+ {
+ case OpenIdConnectRequestType.Token:
+ expectedAuthority = _options.Configuration?.TokenEndpoint ?? _options.Authority + @"/oauth2/token";
+ break;
+ case OpenIdConnectRequestType.Logout:
+ expectedAuthority = _options.Configuration?.EndSessionEndpoint ?? _options.Authority + @"/oauth2/logout";
+ break;
+ default:
+ expectedAuthority = _options.Configuration?.AuthorizationEndpoint ?? _options.Authority + @"/oauth2/authorize";
+ break;
+ }
+
+ if (!absoluteUri.StartsWith(expectedAuthority))
+ {
+ errors.Add($"ExpectedAuthority: {expectedAuthority}");
+ }
+ }
+
+ private void ValidateClientId(IDictionary<string, string> actualParams, ICollection<string> errors, bool htmlEncoded) =>
+ ValidateParameter(OpenIdConnectParameterNames.ClientId, _options.ClientId, actualParams, errors, htmlEncoded);
+
+ private void ValidateResponseType(IDictionary<string, string> actualParams, ICollection<string> errors, bool htmlEncoded) =>
+ ValidateParameter(OpenIdConnectParameterNames.ResponseType, _options.ResponseType, actualParams, errors, htmlEncoded);
+
+ private void ValidateResponseMode(IDictionary<string, string> actualParams, ICollection<string> errors, bool htmlEncoded) =>
+ ValidateParameter(OpenIdConnectParameterNames.ResponseMode, _options.ResponseMode, actualParams, errors, htmlEncoded);
+
+ private void ValidateScope(IDictionary<string, string> actualParams, ICollection<string> errors, bool htmlEncoded) =>
+ ValidateParameter(OpenIdConnectParameterNames.Scope, string.Join(" ", _options.Scope), actualParams, errors, htmlEncoded);
+
+ private void ValidateRedirectUri(IDictionary<string, string> actualParams, ICollection<string> errors, bool htmlEncoded) =>
+ ValidateParameter(OpenIdConnectParameterNames.RedirectUri, TestServerBuilder.TestHost + _options.CallbackPath, actualParams, errors, htmlEncoded);
+
+ private void ValidateResource(IDictionary<string, string> actualParams, ICollection<string> errors, bool htmlEncoded) =>
+ ValidateParameter(OpenIdConnectParameterNames.RedirectUri, _options.Resource, actualParams, errors, htmlEncoded);
+
+ private void ValidateState(IDictionary<string, string> actualParams, ICollection<string> errors, bool htmlEncoded) =>
+ ValidateParameter(OpenIdConnectParameterNames.State, ExpectedState, actualParams, errors, htmlEncoded);
+
+ private void ValidateSkuTelemetry(IDictionary<string, string> actualParams, ICollection<string> errors, bool htmlEncoded) =>
+#if NETCOREAPP2_0 || NETCOREAPP2_1
+ ValidateParameter(OpenIdConnectParameterNames.SkuTelemetry, "ID_NETSTANDARD1_4", actualParams, errors, htmlEncoded);
+#elif NET461
+ ValidateParameter(OpenIdConnectParameterNames.SkuTelemetry, "ID_NET451", actualParams, errors, htmlEncoded);
+#else
+#error Invalid target framework.
+#endif
+
+ private void ValidateVersionTelemetry(IDictionary<string, string> actualParams, ICollection<string> errors, bool htmlEncoded) =>
+ ValidateParameter(OpenIdConnectParameterNames.VersionTelemetry, typeof(OpenIdConnectMessage).GetTypeInfo().Assembly.GetName().Version.ToString(), actualParams, errors, htmlEncoded);
+
+ private void ValidatePostLogoutRedirectUri(IDictionary<string, string> actualParams, ICollection<string> errors, bool htmlEncoded) =>
+ ValidateParameter(OpenIdConnectParameterNames.PostLogoutRedirectUri, "https://example.com/signout-callback-oidc", actualParams, errors, htmlEncoded);
+
+ private void ValidateMaxAge(IDictionary<string, string> actualQuery, ICollection<string> errors, bool htmlEncoded)
+ {
+ if(_options.MaxAge.HasValue)
+ {
+ Assert.Equal(TimeSpan.FromMinutes(20), _options.MaxAge.Value);
+ string expectedMaxAge = "1200";
+ ValidateParameter(OpenIdConnectParameterNames.MaxAge, expectedMaxAge, actualQuery, errors, htmlEncoded);
+ }
+ else if(actualQuery.ContainsKey(OpenIdConnectParameterNames.MaxAge))
+ {
+ errors.Add($"Parameter {OpenIdConnectParameterNames.MaxAge} is present but it should be absent");
+ }
+ }
+
+ private void ValidatePrompt(IDictionary<string, string> actualParams, ICollection<string> errors, bool htmlEncoded) =>
+ ValidateParameter(OpenIdConnectParameterNames.Prompt, _options.Prompt, actualParams, errors, htmlEncoded);
+
+ private void ValidateParameter(
+ string parameterName,
+ string expectedValue,
+ IDictionary<string, string> actualParams,
+ ICollection<string> errors,
+ bool htmlEncoded)
+ {
+ string actualValue;
+ if (actualParams.TryGetValue(parameterName, out actualValue))
+ {
+ if (htmlEncoded)
+ {
+ expectedValue = Encoder.Encode(expectedValue);
+ }
+
+ if (actualValue != expectedValue)
+ {
+ errors.Add($"Parameter {parameterName}'s expected value is '{expectedValue}' but its actual value is '{actualValue}'");
+ }
+ }
+ else
+ {
+ errors.Add($"Parameter {parameterName} is missing");
+ }
+ }
+
+ private class MockBackchannel : HttpMessageHandler
+ {
+ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ if (request.RequestUri.AbsoluteUri.Equals("https://login.microsoftonline.com/common/.well-known/openid-configuration"))
+ {
+ return await ReturnResource("wellknownconfig.json");
+ }
+ if (request.RequestUri.AbsoluteUri.Equals("https://login.microsoftonline.com/common/discovery/keys"))
+ {
+ return await ReturnResource("wellknownkeys.json");
+ }
+
+ throw new NotImplementedException();
+ }
+
+ private async Task<HttpResponseMessage> ReturnResource(string resource)
+ {
+ var resourceName = "Microsoft.AspNetCore.Authentication.Test.OpenIdConnect." + resource;
+ using (var stream = typeof(MockBackchannel).Assembly.GetManifestResourceStream(resourceName))
+ using (var reader = new StreamReader(stream))
+ {
+ var body = await reader.ReadToEndAsync();
+ var content = new StringContent(body, Encoding.UTF8, "application/json");
+ return new HttpResponseMessage()
+ {
+ Content = content,
+ };
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestTransaction.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestTransaction.cs
new file mode 100644
index 0000000000..4f924172c6
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestTransaction.cs
@@ -0,0 +1,40 @@
+// 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.Linq;
+using System.Net.Http;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
+{
+ internal class TestTransaction
+ {
+ public HttpRequestMessage Request { get; set; }
+
+ public HttpResponseMessage Response { get; set; }
+
+ public IList<string> SetCookie { get; set; }
+
+ public string ResponseText { get; set; }
+
+ public XElement ResponseElement { get; set; }
+
+ public string AuthenticationCookieValue
+ {
+ get
+ {
+ if (SetCookie != null && SetCookie.Count > 0)
+ {
+ var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNetCore.Cookie="));
+ if (authCookie != null)
+ {
+ return authCookie.Substring(0, authCookie.IndexOf(';'));
+ }
+ }
+
+ return null;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownconfig.json b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownconfig.json
new file mode 100644
index 0000000000..4d46a8cf0a
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownconfig.json
@@ -0,0 +1,23 @@
+{
+ "authorization_endpoint": "https://login.microsoftonline.com/common/oauth2/authorize",
+ "token_endpoint": "https://login.microsoftonline.com/common/oauth2/token",
+ "token_endpoint_auth_methods_supported": [ "client_secret_post", "private_key_jwt", "client_secret_basic" ],
+ "jwks_uri": "https://login.microsoftonline.com/common/discovery/keys",
+ "response_modes_supported": [ "query", "fragment", "form_post" ],
+ "subject_types_supported": [ "pairwise" ],
+ "id_token_signing_alg_values_supported": [ "RS256" ],
+ "http_logout_supported": true,
+ "frontchannel_logout_supported": true,
+ "end_session_endpoint": "https://login.microsoftonline.com/common/oauth2/logout",
+ "response_types_supported": [ "code", "id_token", "code id_token", "token id_token", "token" ],
+ "scopes_supported": [ "openid" ],
+ "issuer": "https://sts.windows.net/{tenantid}/",
+ "claims_supported": [ "sub", "iss", "cloud_instance_name", "cloud_instance_host_name", "cloud_graph_host_name", "msgraph_host", "aud", "exp", "iat", "auth_time", "acr", "amr", "nonce", "email", "given_name", "family_name", "nickname" ],
+ "microsoft_multi_refresh_token": true,
+ "check_session_iframe": "https://login.microsoftonline.com/common/oauth2/checksession",
+ "userinfo_endpoint": "https://login.microsoftonline.com/common/openid/userinfo",
+ "tenant_region_scope": null,
+ "cloud_instance_name": "microsoftonline.com",
+ "cloud_graph_host_name": "graph.windows.net",
+ "msgraph_host": "graph.microsoft.com"
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownkeys.json b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownkeys.json
new file mode 100644
index 0000000000..77cc5562af
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownkeys.json
@@ -0,0 +1,31 @@
+{
+ "keys": [
+ {
+ "kty": "RSA",
+ "use": "sig",
+ "kid": "SSQdhI1cKvhQEDSJxE2gGYs40Q0",
+ "x5t": "SSQdhI1cKvhQEDSJxE2gGYs40Q0",
+ "n": "pJUB90EMxiNjgkVz5CLLUuG5bYwirL2LXfVsq_nnY686WzbinkvFnNs6LvrJ6DWD5NV1-0Tq2eZj7WU8H9ytmDPsRnJ0b49gRCJYOg6-SdOe9Tl0lB0IBJE1aWh3OdCVrZLE4LH4-LGIDrkwnCV8dKFkO3EIUYPaEysL4g4wLx-TCfpMWE37XC09P-nBRVkRNcihrzY38_MC42NkRdDwByZemXkQKddnn5Y5o4rVzPGqQy3vjmTjKolYEIBYa7n3yF0848MG0k338bjnyceJgmZzjxttkWTVDikQXSldbu3QCrCAlipbWPUAXaZK8buY8LP80G4U_wx4LuZ_Krq5OQ",
+ "e": "AQAB",
+ "x5c": [ "MIIDBTCCAe2gAwIBAgIQHJ7yHxNEM7tBeqcRTMBhhTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDEwODAwMDAwMFoXDTIwMDEwOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKSVAfdBDMYjY4JFc+Qiy1LhuW2MIqy9i131bKv552OvOls24p5LxZzbOi76yeg1g+TVdftE6tnmY+1lPB/crZgz7EZydG+PYEQiWDoOvknTnvU5dJQdCASRNWlodznQla2SxOCx+PixiA65MJwlfHShZDtxCFGD2hMrC+IOMC8fkwn6TFhN+1wtPT/pwUVZETXIoa82N/PzAuNjZEXQ8AcmXpl5ECnXZ5+WOaOK1czxqkMt745k4yqJWBCAWGu598hdPOPDBtJN9/G458nHiYJmc48bbZFk1Q4pEF0pXW7t0AqwgJYqW1j1AF2mSvG7mPCz/NBuFP8MeC7mfyq6uTkCAwEAAaMhMB8wHQYDVR0OBBYEFFVWt/4iTcBiWk+EX1dCKZGoAlBQMA0GCSqGSIb3DQEBCwUAA4IBAQCJrLUiCfldfDFhZLG08DpLcwpn8Mvhm61ZpPANyCawRgaoBOsxFJew1qpC2wfXh3yUwJEIkkTGXAsiWENkeqmjd2XXQwZuIHIo5/KyZLfo2m1CmEH/QXL6uRx1f8liPr4efC8H+7/sduf6nPfk0UpNsAHA6RxJFy2jg2wHU+Ux4jK4Gc5d/rJPhJhvyS9Zax1hbTlf+N32ZvS780VMDb/nf2LdtACL0ya/+KSDGVXS3GhS9FLEXrBNjq921ghVIFJhPzm54yfeupIwz+zfISoTHIYw37i7iNUbkvDrm14a27pwLS/tfSuJHKcGPt1sjMu6SS/pf1BlvdoFkKdLLaUb" ]
+ },
+ {
+ "kty": "RSA",
+ "use": "sig",
+ "kid": "FSimuFrFNoC0sJXGmv13nNZceDc",
+ "x5t": "FSimuFrFNoC0sJXGmv13nNZceDc",
+ "n": "yCYaJF8uHoV2L31cjZUDdcodK1Y1EsTLkDD-DEXFyGeHaQ92T9t6MU6zazBzHvJRarG6OMI1GwsFxZ9opSVOeuRjuL3H2ehmUyuKOAnL8uT4cfkdfbg9AIN_63COccfFn0br_xUszZ7lkF5mb63sze-G66YQcbdTCWgsXpxR6491b57Gc4HVTV8cEgU4byezhJIiirrPDmt23QJIjr6XtvUMSNW88u0kX7PKOUnVCns2AG8DB2I-JExTiXwhFVu5JUqgpgmjIngvd5eyNzOgFJMnpWNXabKDP3oMLvQxjdq9xwWuTu0IQLpmUxEF9jVc8vKV1Pu2xHcS7ON5xJrUzw",
+ "e": "AQAB",
+ "x5c": [ "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk" ]
+ },
+ {
+ "kty": "RSA",
+ "use": "sig",
+ "kid": "2S4SCVGs8Sg9LS6AqLIq6DpW-g8",
+ "x5t": "2S4SCVGs8Sg9LS6AqLIq6DpW-g8",
+ "n": "oZ-QQrNuB4ei9ATYrT61ebPtvwwYWnsrTpp4ISSp6niZYb92XM0oUTNgqd_C1vGN8J-y9wCbaJWkpBf46CjdZehrqczPhzhHau8WcRXocSB1u_tuZhv1ooAZ4bAcy79UkeLiG60HkuTNJJC8CfaTp1R97szBhuk0Vz5yt4r5SpfewIlBCnZUYwkDS172H9WapQu-3P2Qjh0l-JLyCkdrhvizZUk0atq5_AIDKRU-A0pRGc-EZhUL0LqUMz6c6M2s_4GnQaScv44A5iZUDD15B6e8Apb2yARohkWmOnmRcTVfes8EkfxjzZEzm3cNkvP0ogILyISHKlkzy2OmlU6iXw",
+ "e": "AQAB",
+ "x5c": [ "MIIDKDCCAhCgAwIBAgIQBHJvVNxP1oZO4HYKh+rypDANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMTYxMTE2MDgwMDAwWhcNMTgxMTE2MDgwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChn5BCs24Hh6L0BNitPrV5s+2/DBhaeytOmnghJKnqeJlhv3ZczShRM2Cp38LW8Y3wn7L3AJtolaSkF/joKN1l6GupzM+HOEdq7xZxFehxIHW7+25mG/WigBnhsBzLv1SR4uIbrQeS5M0kkLwJ9pOnVH3uzMGG6TRXPnK3ivlKl97AiUEKdlRjCQNLXvYf1ZqlC77c/ZCOHSX4kvIKR2uG+LNlSTRq2rn8AgMpFT4DSlEZz4RmFQvQupQzPpzozaz/gadBpJy/jgDmJlQMPXkHp7wClvbIBGiGRaY6eZFxNV96zwSR/GPNkTObdw2S8/SiAgvIhIcqWTPLY6aVTqJfAgMBAAGjWDBWMFQGA1UdAQRNMEuAEDUj0BrjP0RTbmoRPTRMY3WhJTAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXOCEARyb1TcT9aGTuB2Cofq8qQwDQYJKoZIhvcNAQELBQADggEBAGnLhDHVz2gLDiu9L34V3ro/6xZDiSWhGyHcGqky7UlzQH3pT5so8iF5P0WzYqVtogPsyC2LPJYSTt2vmQugD4xlu/wbvMFLcV0hmNoTKCF1QTVtEQiAiy0Aq+eoF7Al5fV1S3Sune0uQHimuUFHCmUuF190MLcHcdWnPAmzIc8fv7quRUUsExXmxSX2ktUYQXzqFyIOSnDCuWFm6tpfK5JXS8fW5bpqTlrysXXz/OW/8NFGq/alfjrya4ojrOYLpunGriEtNPwK7hxj1AlCYEWaRHRXaUIW1ByoSff/6Y6+ZhXPUe0cDlNRt/qIz5aflwO7+W8baTS4O8m/icu7ItE=" ]
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/PolicyTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/PolicyTests.cs
new file mode 100644
index 0000000000..368026beb8
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/PolicyTests.cs
@@ -0,0 +1,487 @@
+// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class PolicyTests
+ {
+ [Fact]
+ public async Task CanDispatch()
+ {
+ var server = CreateServer(services =>
+ {
+ services.AddLogging().AddAuthentication(o =>
+ {
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ o.AddScheme<TestHandler>("auth2", "auth2");
+ o.AddScheme<TestHandler>("auth3", "auth3");
+ })
+ .AddPolicyScheme("policy1", "policy1", p =>
+ {
+ p.ForwardDefault = "auth1";
+ })
+ .AddPolicyScheme("policy2", "policy2", p =>
+ {
+ p.ForwardAuthenticate = "auth2";
+ });
+ });
+
+ var transaction = await server.SendAsync("http://example.com/auth/policy1");
+ Assert.Equal("auth1", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth1"));
+
+ transaction = await server.SendAsync("http://example.com/auth/auth1");
+ Assert.Equal("auth1", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth1"));
+
+ transaction = await server.SendAsync("http://example.com/auth/auth2");
+ Assert.Equal("auth2", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth2"));
+
+ transaction = await server.SendAsync("http://example.com/auth/auth3");
+ Assert.Equal("auth3", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth3"));
+
+ transaction = await server.SendAsync("http://example.com/auth/policy2");
+ Assert.Equal("auth2", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth2"));
+ }
+
+ [Fact]
+ public async Task DefaultTargetSelectorWinsOverDefaultTarget()
+ {
+ var services = new ServiceCollection().AddOptions().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ o.AddScheme<TestHandler2>("auth2", "auth2");
+ })
+ .AddPolicyScheme("forward", "forward", p => {
+ p.ForwardDefault= "auth2";
+ p.ForwardDefaultSelector = ctx => "auth1";
+ });
+
+ var handler1 = new TestHandler();
+ services.AddSingleton(handler1);
+ var handler2 = new TestHandler2();
+ services.AddSingleton(handler2);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ Assert.Equal(0, handler1.AuthenticateCount);
+ Assert.Equal(0, handler1.ForbidCount);
+ Assert.Equal(0, handler1.ChallengeCount);
+ Assert.Equal(0, handler1.SignInCount);
+ Assert.Equal(0, handler1.SignOutCount);
+ Assert.Equal(0, handler2.AuthenticateCount);
+ Assert.Equal(0, handler2.ForbidCount);
+ Assert.Equal(0, handler2.ChallengeCount);
+ Assert.Equal(0, handler2.SignInCount);
+ Assert.Equal(0, handler2.SignOutCount);
+
+ await context.AuthenticateAsync("forward");
+ Assert.Equal(1, handler1.AuthenticateCount);
+ Assert.Equal(0, handler2.AuthenticateCount);
+
+ await context.ForbidAsync("forward");
+ Assert.Equal(1, handler1.ForbidCount);
+ Assert.Equal(0, handler2.ForbidCount);
+
+ await context.ChallengeAsync("forward");
+ Assert.Equal(1, handler1.ChallengeCount);
+ Assert.Equal(0, handler2.ChallengeCount);
+
+ await context.SignOutAsync("forward");
+ Assert.Equal(1, handler1.SignOutCount);
+ Assert.Equal(0, handler2.SignOutCount);
+
+ await context.SignInAsync("forward", new ClaimsPrincipal());
+ Assert.Equal(1, handler1.SignInCount);
+ Assert.Equal(0, handler2.SignInCount);
+ }
+
+ [Fact]
+ public async Task NullDefaultTargetSelectorFallsBacktoDefaultTarget()
+ {
+ var services = new ServiceCollection().AddOptions().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ o.AddScheme<TestHandler2>("auth2", "auth2");
+ })
+ .AddPolicyScheme("forward", "forward", p => {
+ p.ForwardDefault= "auth1";
+ p.ForwardDefaultSelector = ctx => null;
+ });
+
+ var handler1 = new TestHandler();
+ services.AddSingleton(handler1);
+ var handler2 = new TestHandler2();
+ services.AddSingleton(handler2);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ Assert.Equal(0, handler1.AuthenticateCount);
+ Assert.Equal(0, handler1.ForbidCount);
+ Assert.Equal(0, handler1.ChallengeCount);
+ Assert.Equal(0, handler1.SignInCount);
+ Assert.Equal(0, handler1.SignOutCount);
+ Assert.Equal(0, handler2.AuthenticateCount);
+ Assert.Equal(0, handler2.ForbidCount);
+ Assert.Equal(0, handler2.ChallengeCount);
+ Assert.Equal(0, handler2.SignInCount);
+ Assert.Equal(0, handler2.SignOutCount);
+
+ await context.AuthenticateAsync("forward");
+ Assert.Equal(1, handler1.AuthenticateCount);
+ Assert.Equal(0, handler2.AuthenticateCount);
+
+ await context.ForbidAsync("forward");
+ Assert.Equal(1, handler1.ForbidCount);
+ Assert.Equal(0, handler2.ForbidCount);
+
+ await context.ChallengeAsync("forward");
+ Assert.Equal(1, handler1.ChallengeCount);
+ Assert.Equal(0, handler2.ChallengeCount);
+
+ await context.SignOutAsync("forward");
+ Assert.Equal(1, handler1.SignOutCount);
+ Assert.Equal(0, handler2.SignOutCount);
+
+ await context.SignInAsync("forward", new ClaimsPrincipal());
+ Assert.Equal(1, handler1.SignInCount);
+ Assert.Equal(0, handler2.SignInCount);
+ }
+
+ [Fact]
+ public async Task SpecificTargetAlwaysWinsOverDefaultTarget()
+ {
+ var services = new ServiceCollection().AddOptions().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ o.AddScheme<TestHandler2>("auth2", "auth2");
+ })
+ .AddPolicyScheme("forward", "forward", p => {
+ p.ForwardDefault= "auth2";
+ p.ForwardDefaultSelector = ctx => "auth2";
+ p.ForwardAuthenticate = "auth1";
+ p.ForwardSignIn = "auth1";
+ p.ForwardSignOut = "auth1";
+ p.ForwardForbid = "auth1";
+ p.ForwardChallenge = "auth1";
+ });
+
+ var handler1 = new TestHandler();
+ services.AddSingleton(handler1);
+ var handler2 = new TestHandler2();
+ services.AddSingleton(handler2);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ Assert.Equal(0, handler1.AuthenticateCount);
+ Assert.Equal(0, handler1.ForbidCount);
+ Assert.Equal(0, handler1.ChallengeCount);
+ Assert.Equal(0, handler1.SignInCount);
+ Assert.Equal(0, handler1.SignOutCount);
+ Assert.Equal(0, handler2.AuthenticateCount);
+ Assert.Equal(0, handler2.ForbidCount);
+ Assert.Equal(0, handler2.ChallengeCount);
+ Assert.Equal(0, handler2.SignInCount);
+ Assert.Equal(0, handler2.SignOutCount);
+
+ await context.AuthenticateAsync("forward");
+ Assert.Equal(1, handler1.AuthenticateCount);
+ Assert.Equal(0, handler2.AuthenticateCount);
+
+ await context.ForbidAsync("forward");
+ Assert.Equal(1, handler1.ForbidCount);
+ Assert.Equal(0, handler2.ForbidCount);
+
+ await context.ChallengeAsync("forward");
+ Assert.Equal(1, handler1.ChallengeCount);
+ Assert.Equal(0, handler2.ChallengeCount);
+
+ await context.SignOutAsync("forward");
+ Assert.Equal(1, handler1.SignOutCount);
+ Assert.Equal(0, handler2.SignOutCount);
+
+ await context.SignInAsync("forward", new ClaimsPrincipal());
+ Assert.Equal(1, handler1.SignInCount);
+ Assert.Equal(0, handler2.SignInCount);
+ }
+
+ [Fact]
+ public async Task VirtualSchemeTargetsForwardWithDefaultTarget()
+ {
+ var services = new ServiceCollection().AddOptions().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ o.AddScheme<TestHandler2>("auth2", "auth2");
+ })
+ .AddPolicyScheme("forward", "forward", p => p.ForwardDefault= "auth1");
+
+ var handler1 = new TestHandler();
+ services.AddSingleton(handler1);
+ var handler2 = new TestHandler2();
+ services.AddSingleton(handler2);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ Assert.Equal(0, handler1.AuthenticateCount);
+ Assert.Equal(0, handler1.ForbidCount);
+ Assert.Equal(0, handler1.ChallengeCount);
+ Assert.Equal(0, handler1.SignInCount);
+ Assert.Equal(0, handler1.SignOutCount);
+ Assert.Equal(0, handler2.AuthenticateCount);
+ Assert.Equal(0, handler2.ForbidCount);
+ Assert.Equal(0, handler2.ChallengeCount);
+ Assert.Equal(0, handler2.SignInCount);
+ Assert.Equal(0, handler2.SignOutCount);
+
+ await context.AuthenticateAsync("forward");
+ Assert.Equal(1, handler1.AuthenticateCount);
+ Assert.Equal(0, handler2.AuthenticateCount);
+
+ await context.ForbidAsync("forward");
+ Assert.Equal(1, handler1.ForbidCount);
+ Assert.Equal(0, handler2.ForbidCount);
+
+ await context.ChallengeAsync("forward");
+ Assert.Equal(1, handler1.ChallengeCount);
+ Assert.Equal(0, handler2.ChallengeCount);
+
+ await context.SignOutAsync("forward");
+ Assert.Equal(1, handler1.SignOutCount);
+ Assert.Equal(0, handler2.SignOutCount);
+
+ await context.SignInAsync("forward", new ClaimsPrincipal());
+ Assert.Equal(1, handler1.SignInCount);
+ Assert.Equal(0, handler2.SignInCount);
+ }
+
+ [Fact]
+ public async Task VirtualSchemeTargetsOverrideDefaultTarget()
+ {
+ var services = new ServiceCollection().AddOptions().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ o.AddScheme<TestHandler2>("auth2", "auth2");
+ })
+ .AddPolicyScheme("forward", "forward", p =>
+ {
+ p.ForwardDefault= "auth1";
+ p.ForwardChallenge = "auth2";
+ p.ForwardSignIn = "auth2";
+ });
+
+ var handler1 = new TestHandler();
+ services.AddSingleton(handler1);
+ var handler2 = new TestHandler2();
+ services.AddSingleton(handler2);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ Assert.Equal(0, handler1.AuthenticateCount);
+ Assert.Equal(0, handler1.ForbidCount);
+ Assert.Equal(0, handler1.ChallengeCount);
+ Assert.Equal(0, handler1.SignInCount);
+ Assert.Equal(0, handler1.SignOutCount);
+ Assert.Equal(0, handler2.AuthenticateCount);
+ Assert.Equal(0, handler2.ForbidCount);
+ Assert.Equal(0, handler2.ChallengeCount);
+ Assert.Equal(0, handler2.SignInCount);
+ Assert.Equal(0, handler2.SignOutCount);
+
+ await context.AuthenticateAsync("forward");
+ Assert.Equal(1, handler1.AuthenticateCount);
+ Assert.Equal(0, handler2.AuthenticateCount);
+
+ await context.ForbidAsync("forward");
+ Assert.Equal(1, handler1.ForbidCount);
+ Assert.Equal(0, handler2.ForbidCount);
+
+ await context.ChallengeAsync("forward");
+ Assert.Equal(0, handler1.ChallengeCount);
+ Assert.Equal(1, handler2.ChallengeCount);
+
+ await context.SignOutAsync("forward");
+ Assert.Equal(1, handler1.SignOutCount);
+ Assert.Equal(0, handler2.SignOutCount);
+
+ await context.SignInAsync("forward", new ClaimsPrincipal());
+ Assert.Equal(0, handler1.SignInCount);
+ Assert.Equal(1, handler2.SignInCount);
+ }
+
+ [Fact]
+ public async Task CanDynamicTargetBasedOnQueryString()
+ {
+ var server = CreateServer(services =>
+ {
+ services.AddAuthentication(o =>
+ {
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ o.AddScheme<TestHandler>("auth2", "auth2");
+ o.AddScheme<TestHandler>("auth3", "auth3");
+ })
+ .AddPolicyScheme("dynamic", "dynamic", p =>
+ {
+ p.ForwardDefaultSelector = c => c.Request.QueryString.Value.Substring(1);
+ });
+ });
+
+ var transaction = await server.SendAsync("http://example.com/auth/dynamic?auth1");
+ Assert.Equal("auth1", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth1"));
+ transaction = await server.SendAsync("http://example.com/auth/dynamic?auth2");
+ Assert.Equal("auth2", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth2"));
+ transaction = await server.SendAsync("http://example.com/auth/dynamic?auth3");
+ Assert.Equal("auth3", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth3"));
+ }
+
+ private class TestHandler : IAuthenticationSignInHandler
+ {
+ public AuthenticationScheme Scheme { get; set; }
+ public int SignInCount { get; set; }
+ public int SignOutCount { get; set; }
+ public int ForbidCount { get; set; }
+ public int ChallengeCount { get; set; }
+ public int AuthenticateCount { get; set; }
+
+ public Task<AuthenticateResult> AuthenticateAsync()
+ {
+ AuthenticateCount++;
+ var principal = new ClaimsPrincipal();
+ var id = new ClaimsIdentity();
+ id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name));
+ principal.AddIdentity(id);
+ return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name)));
+ }
+
+ public Task ChallengeAsync(AuthenticationProperties properties)
+ {
+ ChallengeCount++;
+ return Task.CompletedTask;
+ }
+
+ public Task ForbidAsync(AuthenticationProperties properties)
+ {
+ ForbidCount++;
+ return Task.CompletedTask;
+ }
+
+ public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
+ {
+ Scheme = scheme;
+ return Task.CompletedTask;
+ }
+
+ public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
+ {
+ SignInCount++;
+ return Task.CompletedTask;
+ }
+
+ public Task SignOutAsync(AuthenticationProperties properties)
+ {
+ SignOutCount++;
+ return Task.CompletedTask;
+ }
+ }
+
+ private class TestHandler2 : IAuthenticationSignInHandler
+ {
+ public AuthenticationScheme Scheme { get; set; }
+ public int SignInCount { get; set; }
+ public int SignOutCount { get; set; }
+ public int ForbidCount { get; set; }
+ public int ChallengeCount { get; set; }
+ public int AuthenticateCount { get; set; }
+
+ public Task<AuthenticateResult> AuthenticateAsync()
+ {
+ AuthenticateCount++;
+ var principal = new ClaimsPrincipal();
+ var id = new ClaimsIdentity();
+ id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name));
+ principal.AddIdentity(id);
+ return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name)));
+ }
+
+ public Task ChallengeAsync(AuthenticationProperties properties)
+ {
+ ChallengeCount++;
+ return Task.CompletedTask;
+ }
+
+ public Task ForbidAsync(AuthenticationProperties properties)
+ {
+ ForbidCount++;
+ return Task.CompletedTask;
+ }
+
+ public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
+ {
+ Scheme = scheme;
+ return Task.CompletedTask;
+ }
+
+ public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
+ {
+ SignInCount++;
+ return Task.CompletedTask;
+ }
+
+ public Task SignOutAsync(AuthenticationProperties properties)
+ {
+ SignOutCount++;
+ return Task.CompletedTask;
+ }
+ }
+
+ private static TestServer CreateServer(Action<IServiceCollection> configure = null, string defaultScheme = null)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Use(async (context, next) =>
+ {
+ var req = context.Request;
+ var res = context.Response;
+ if (req.Path.StartsWithSegments(new PathString("/auth"), out var remainder))
+ {
+ var name = (remainder.Value.Length > 0) ? remainder.Value.Substring(1) : null;
+ var result = await context.AuthenticateAsync(name);
+ res.Describe(result?.Ticket?.Principal);
+ }
+ else
+ {
+ await next();
+ }
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ configure?.Invoke(services);
+ });
+ return new TestServer(builder);
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/SecureDataFormatTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/SecureDataFormatTests.cs
new file mode 100644
index 0000000000..bda4b09fa7
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/SecureDataFormatTests.cs
@@ -0,0 +1,80 @@
+// 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.Text;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.DataHandler
+{
+ public class SecureDataFormatTests
+ {
+ public SecureDataFormatTests()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddDataProtection();
+ ServiceProvider = serviceCollection.BuildServiceProvider();
+ }
+
+ public IServiceProvider ServiceProvider { get; }
+
+ [Fact]
+ public void ProtectDataRoundTrips()
+ {
+ var provider = ServiceProvider.GetRequiredService<IDataProtectionProvider>();
+ var prototector = provider.CreateProtector("test");
+ var secureDataFormat = new SecureDataFormat<string>(new StringSerializer(), prototector);
+
+ string input = "abcdefghijklmnopqrstuvwxyz0123456789";
+ var protectedData = secureDataFormat.Protect(input);
+ var result = secureDataFormat.Unprotect(protectedData);
+ Assert.Equal(input, result);
+ }
+
+ [Fact]
+ public void ProtectWithPurposeRoundTrips()
+ {
+ var provider = ServiceProvider.GetRequiredService<IDataProtectionProvider>();
+ var prototector = provider.CreateProtector("test");
+ var secureDataFormat = new SecureDataFormat<string>(new StringSerializer(), prototector);
+
+ string input = "abcdefghijklmnopqrstuvwxyz0123456789";
+ string purpose = "purpose1";
+ var protectedData = secureDataFormat.Protect(input, purpose);
+ var result = secureDataFormat.Unprotect(protectedData, purpose);
+ Assert.Equal(input, result);
+ }
+
+ [Fact]
+ public void UnprotectWithDifferentPurposeFails()
+ {
+ var provider = ServiceProvider.GetRequiredService<IDataProtectionProvider>();
+ var prototector = provider.CreateProtector("test");
+ var secureDataFormat = new SecureDataFormat<string>(new StringSerializer(), prototector);
+
+ string input = "abcdefghijklmnopqrstuvwxyz0123456789";
+ string purpose = "purpose1";
+ var protectedData = secureDataFormat.Protect(input, purpose);
+ var result = secureDataFormat.Unprotect(protectedData); // Null other purpose
+ Assert.Null(result);
+
+ result = secureDataFormat.Unprotect(protectedData, "purpose2");
+ Assert.Null(result);
+ }
+
+ private class StringSerializer : IDataSerializer<string>
+ {
+ public byte[] Serialize(string model)
+ {
+ return Encoding.UTF8.GetBytes(model);
+ }
+
+ public string Deserialize(byte[] data)
+ {
+ return Encoding.UTF8.GetString(data);
+ }
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestClock.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestClock.cs
new file mode 100644
index 0000000000..c34e4fd2da
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestClock.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 System;
+using Microsoft.AspNetCore.Authentication;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class TestClock : ISystemClock
+ {
+ public TestClock()
+ {
+ UtcNow = new DateTimeOffset(2013, 6, 11, 12, 34, 56, 789, TimeSpan.Zero);
+ }
+
+ public DateTimeOffset UtcNow { get; set; }
+
+ public void Add(TimeSpan timeSpan)
+ {
+ UtcNow = UtcNow + timeSpan;
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestExtensions.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestExtensions.cs
new file mode 100644
index 0000000000..87d6d95a2c
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestExtensions.cs
@@ -0,0 +1,86 @@
+// 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.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public static class TestExtensions
+ {
+ public const string CookieAuthenticationScheme = "External";
+
+ public static async Task<Transaction> SendAsync(this TestServer server, string uri, string cookieHeader = null)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, uri);
+ if (!string.IsNullOrEmpty(cookieHeader))
+ {
+ request.Headers.Add("Cookie", cookieHeader);
+ }
+ var transaction = new Transaction
+ {
+ Request = request,
+ Response = await server.CreateClient().SendAsync(request),
+ };
+ if (transaction.Response.Headers.Contains("Set-Cookie"))
+ {
+ transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList();
+ }
+ transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync();
+
+ if (transaction.Response.Content != null &&
+ transaction.Response.Content.Headers.ContentType != null &&
+ transaction.Response.Content.Headers.ContentType.MediaType == "text/xml")
+ {
+ transaction.ResponseElement = XElement.Parse(transaction.ResponseText);
+ }
+ return transaction;
+ }
+
+ public static void Describe(this HttpResponse res, ClaimsPrincipal principal)
+ {
+ res.StatusCode = 200;
+ res.ContentType = "text/xml";
+ var xml = new XElement("xml");
+ if (principal != null)
+ {
+ foreach (var identity in principal.Identities)
+ {
+ xml.Add(identity.Claims.Select(claim =>
+ new XElement("claim", new XAttribute("type", claim.Type),
+ new XAttribute("value", claim.Value),
+ new XAttribute("issuer", claim.Issuer))));
+ }
+ }
+ var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString());
+ res.Body.Write(xmlBytes, 0, xmlBytes.Length);
+ }
+
+ public static void Describe(this HttpResponse res, IEnumerable<AuthenticationToken> tokens)
+ {
+ res.StatusCode = 200;
+ res.ContentType = "text/xml";
+ var xml = new XElement("xml");
+ if (tokens != null)
+ {
+ foreach (var token in tokens)
+ {
+ xml.Add(new XElement("token", new XAttribute("name", token.Name),
+ new XAttribute("value", token.Value)));
+ }
+ }
+ var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString());
+ res.Body.Write(xmlBytes, 0, xmlBytes.Length);
+ }
+
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHandlers.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHandlers.cs
new file mode 100644
index 0000000000..cd9fe9fb1a
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHandlers.cs
@@ -0,0 +1,115 @@
+// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information.
+
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Authentication.Tests
+{
+ public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>, IAuthenticationSignInHandler
+ {
+ public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
+ { }
+
+ public int SignInCount { get; set; }
+ public int SignOutCount { get; set; }
+ public int ForbidCount { get; set; }
+ public int ChallengeCount { get; set; }
+ public int AuthenticateCount { get; set; }
+
+ protected override Task HandleChallengeAsync(AuthenticationProperties properties)
+ {
+ ChallengeCount++;
+ return Task.CompletedTask;
+ }
+
+ protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
+ {
+ ForbidCount++;
+ return Task.CompletedTask;
+ }
+
+ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
+ {
+ AuthenticateCount++;
+ var principal = new ClaimsPrincipal();
+ var id = new ClaimsIdentity();
+ id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name));
+ principal.AddIdentity(id);
+ return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name)));
+ }
+
+ public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
+ {
+ SignInCount++;
+ return Task.CompletedTask;
+ }
+
+ public Task SignOutAsync(AuthenticationProperties properties)
+ {
+ SignOutCount++;
+ return Task.CompletedTask;
+ }
+ }
+
+ public class TestHandler : IAuthenticationSignInHandler
+ {
+ public AuthenticationScheme Scheme { get; set; }
+ public int SignInCount { get; set; }
+ public int SignOutCount { get; set; }
+ public int ForbidCount { get; set; }
+ public int ChallengeCount { get; set; }
+ public int AuthenticateCount { get; set; }
+
+ public Task<AuthenticateResult> AuthenticateAsync()
+ {
+ AuthenticateCount++;
+ var principal = new ClaimsPrincipal();
+ var id = new ClaimsIdentity();
+ id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name));
+ principal.AddIdentity(id);
+ return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name)));
+ }
+
+ public Task ChallengeAsync(AuthenticationProperties properties)
+ {
+ ChallengeCount++;
+ return Task.CompletedTask;
+ }
+
+ public Task ForbidAsync(AuthenticationProperties properties)
+ {
+ ForbidCount++;
+ return Task.CompletedTask;
+ }
+
+ public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
+ {
+ Scheme = scheme;
+ return Task.CompletedTask;
+ }
+
+ public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
+ {
+ SignInCount++;
+ return Task.CompletedTask;
+ }
+
+ public Task SignOutAsync(AuthenticationProperties properties)
+ {
+ SignOutCount++;
+ return Task.CompletedTask;
+ }
+ }
+
+ public class TestHandler2 : TestHandler
+ {
+ }
+
+ public class TestHandler3 : TestHandler
+ {
+ }
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHttpMessageHandler.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHttpMessageHandler.cs
new file mode 100644
index 0000000000..5289e38809
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TestHttpMessageHandler.cs
@@ -0,0 +1,24 @@
+// 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.Net.Http;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class TestHttpMessageHandler : HttpMessageHandler
+ {
+ public Func<HttpRequestMessage, HttpResponseMessage> Sender { get; set; }
+
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
+ {
+ if (Sender != null)
+ {
+ return Task.FromResult(Sender(request));
+ }
+
+ return Task.FromResult<HttpResponseMessage>(null);
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TicketSerializerTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TicketSerializerTests.cs
new file mode 100644
index 0000000000..a1e58743b6
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TicketSerializerTests.cs
@@ -0,0 +1,130 @@
+// 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 System.Linq;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Testing.xunit;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class TicketSerializerTests
+ {
+ [Fact]
+ public void CanRoundTripEmptyPrincipal()
+ {
+ var serializer = new TicketSerializer();
+ var properties = new AuthenticationProperties();
+ properties.RedirectUri = "bye";
+ var ticket = new AuthenticationTicket(new ClaimsPrincipal(), properties, "Hello");
+
+ using (var stream = new MemoryStream())
+ using (var writer = new BinaryWriter(stream))
+ using (var reader = new BinaryReader(stream))
+ {
+ serializer.Write(writer, ticket);
+ stream.Position = 0;
+ var readTicket = serializer.Read(reader);
+ Assert.Empty(readTicket.Principal.Identities);
+ Assert.Equal("bye", readTicket.Properties.RedirectUri);
+ Assert.Equal("Hello", readTicket.AuthenticationScheme);
+ }
+ }
+
+ [Fact]
+ public void CanRoundTripBootstrapContext()
+ {
+ var serializer = new TicketSerializer();
+ var properties = new AuthenticationProperties();
+
+ var ticket = new AuthenticationTicket(new ClaimsPrincipal(), properties, "Hello");
+ ticket.Principal.AddIdentity(new ClaimsIdentity("misc") { BootstrapContext = "bootstrap" });
+
+ using (var stream = new MemoryStream())
+ using (var writer = new BinaryWriter(stream))
+ using (var reader = new BinaryReader(stream))
+ {
+ serializer.Write(writer, ticket);
+ stream.Position = 0;
+ var readTicket = serializer.Read(reader);
+ Assert.Single(readTicket.Principal.Identities);
+ Assert.Equal("misc", readTicket.Principal.Identity.AuthenticationType);
+ Assert.Equal("bootstrap", readTicket.Principal.Identities.First().BootstrapContext);
+ }
+ }
+
+ [Fact]
+ public void CanRoundTripActorIdentity()
+ {
+ var serializer = new TicketSerializer();
+ var properties = new AuthenticationProperties();
+
+ var actor = new ClaimsIdentity("actor");
+ var ticket = new AuthenticationTicket(new ClaimsPrincipal(), properties, "Hello");
+ ticket.Principal.AddIdentity(new ClaimsIdentity("misc") { Actor = actor });
+
+ using (var stream = new MemoryStream())
+ using (var writer = new BinaryWriter(stream))
+ using (var reader = new BinaryReader(stream))
+ {
+ serializer.Write(writer, ticket);
+ stream.Position = 0;
+ var readTicket = serializer.Read(reader);
+ Assert.Single(readTicket.Principal.Identities);
+ Assert.Equal("misc", readTicket.Principal.Identity.AuthenticationType);
+
+ var identity = (ClaimsIdentity) readTicket.Principal.Identity;
+ Assert.NotNull(identity.Actor);
+ Assert.Equal("actor", identity.Actor.AuthenticationType);
+ }
+ }
+
+ [ConditionalFact]
+ [FrameworkSkipCondition(
+ RuntimeFrameworks.Mono,
+ SkipReason = "Test fails with Mono 4.0.4. Build rarely reaches testing with Mono 4.2.1")]
+ public void CanRoundTripClaimProperties()
+ {
+ var serializer = new TicketSerializer();
+ var properties = new AuthenticationProperties();
+
+ var claim = new Claim("type", "value", "valueType", "issuer", "original-issuer");
+ claim.Properties.Add("property-1", "property-value");
+
+ // Note: a null value MUST NOT result in a crash
+ // and MUST instead be treated like an empty string.
+ claim.Properties.Add("property-2", null);
+
+ var ticket = new AuthenticationTicket(new ClaimsPrincipal(), properties, "Hello");
+ ticket.Principal.AddIdentity(new ClaimsIdentity(new[] { claim }, "misc"));
+
+ using (var stream = new MemoryStream())
+ using (var writer = new BinaryWriter(stream))
+ using (var reader = new BinaryReader(stream))
+ {
+ serializer.Write(writer, ticket);
+ stream.Position = 0;
+ var readTicket = serializer.Read(reader);
+ Assert.Single(readTicket.Principal.Identities);
+ Assert.Equal("misc", readTicket.Principal.Identity.AuthenticationType);
+
+ var readClaim = readTicket.Principal.FindFirst("type");
+ Assert.NotNull(claim);
+ Assert.Equal("type", claim.Type);
+ Assert.Equal("value", claim.Value);
+ Assert.Equal("valueType", claim.ValueType);
+ Assert.Equal("issuer", claim.Issuer);
+ Assert.Equal("original-issuer", claim.OriginalIssuer);
+
+ var property1 = readClaim.Properties["property-1"];
+ Assert.Equal("property-value", property1);
+
+ var property2 = readClaim.Properties["property-2"];
+ Assert.Equal(string.Empty, property2);
+ }
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs
new file mode 100644
index 0000000000..4d4023bee5
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs
@@ -0,0 +1,181 @@
+// 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.Linq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class TokenExtensionTests
+ {
+ [Fact]
+ public void CanStoreMultipleTokens()
+ {
+ var props = new AuthenticationProperties();
+ var tokens = new List<AuthenticationToken>();
+ var tok1 = new AuthenticationToken { Name = "One", Value = "1" };
+ var tok2 = new AuthenticationToken { Name = "Two", Value = "2" };
+ var tok3 = new AuthenticationToken { Name = "Three", Value = "3" };
+ tokens.Add(tok1);
+ tokens.Add(tok2);
+ tokens.Add(tok3);
+ props.StoreTokens(tokens);
+
+ Assert.Equal("1", props.GetTokenValue("One"));
+ Assert.Equal("2", props.GetTokenValue("Two"));
+ Assert.Equal("3", props.GetTokenValue("Three"));
+ Assert.Equal(3, props.GetTokens().Count());
+ }
+
+ [Fact]
+ public void SubsequentStoreTokenDeletesPreviousTokens()
+ {
+ var props = new AuthenticationProperties();
+ var tokens = new List<AuthenticationToken>();
+ var tok1 = new AuthenticationToken { Name = "One", Value = "1" };
+ var tok2 = new AuthenticationToken { Name = "Two", Value = "2" };
+ var tok3 = new AuthenticationToken { Name = "Three", Value = "3" };
+ tokens.Add(tok1);
+ tokens.Add(tok2);
+ tokens.Add(tok3);
+
+ props.StoreTokens(tokens);
+
+ props.StoreTokens(new[] { new AuthenticationToken { Name = "Zero", Value = "0" } });
+
+ Assert.Equal("0", props.GetTokenValue("Zero"));
+ Assert.Null(props.GetTokenValue("One"));
+ Assert.Null(props.GetTokenValue("Two"));
+ Assert.Null(props.GetTokenValue("Three"));
+ Assert.Single(props.GetTokens());
+ }
+
+ [Fact]
+ public void CanUpdateTokens()
+ {
+ var props = new AuthenticationProperties();
+ var tokens = new List<AuthenticationToken>();
+ var tok1 = new AuthenticationToken { Name = "One", Value = "1" };
+ var tok2 = new AuthenticationToken { Name = "Two", Value = "2" };
+ var tok3 = new AuthenticationToken { Name = "Three", Value = "3" };
+ tokens.Add(tok1);
+ tokens.Add(tok2);
+ tokens.Add(tok3);
+ props.StoreTokens(tokens);
+
+ tok1.Value = ".1";
+ tok2.Value = ".2";
+ tok3.Value = ".3";
+ props.StoreTokens(tokens);
+
+ Assert.Equal(".1", props.GetTokenValue("One"));
+ Assert.Equal(".2", props.GetTokenValue("Two"));
+ Assert.Equal(".3", props.GetTokenValue("Three"));
+ Assert.Equal(3, props.GetTokens().Count());
+ }
+
+ [Fact]
+ public void CanUpdateTokenValues()
+ {
+ var props = new AuthenticationProperties();
+ var tokens = new List<AuthenticationToken>();
+ var tok1 = new AuthenticationToken { Name = "One", Value = "1" };
+ var tok2 = new AuthenticationToken { Name = "Two", Value = "2" };
+ var tok3 = new AuthenticationToken { Name = "Three", Value = "3" };
+ tokens.Add(tok1);
+ tokens.Add(tok2);
+ tokens.Add(tok3);
+ props.StoreTokens(tokens);
+
+ Assert.True(props.UpdateTokenValue("One", ".11"));
+ Assert.True(props.UpdateTokenValue("Two", ".22"));
+ Assert.True(props.UpdateTokenValue("Three", ".33"));
+
+ Assert.Equal(".11", props.GetTokenValue("One"));
+ Assert.Equal(".22", props.GetTokenValue("Two"));
+ Assert.Equal(".33", props.GetTokenValue("Three"));
+ Assert.Equal(3, props.GetTokens().Count());
+ }
+
+ [Fact]
+ public void UpdateTokenValueReturnsFalseForUnknownToken()
+ {
+ var props = new AuthenticationProperties();
+ var tokens = new List<AuthenticationToken>();
+ var tok1 = new AuthenticationToken { Name = "One", Value = "1" };
+ var tok2 = new AuthenticationToken { Name = "Two", Value = "2" };
+ var tok3 = new AuthenticationToken { Name = "Three", Value = "3" };
+ tokens.Add(tok1);
+ tokens.Add(tok2);
+ tokens.Add(tok3);
+ props.StoreTokens(tokens);
+
+ Assert.False(props.UpdateTokenValue("ONE", ".11"));
+ Assert.False(props.UpdateTokenValue("Jigglypuff", ".11"));
+
+ Assert.Null(props.GetTokenValue("ONE"));
+ Assert.Null(props.GetTokenValue("Jigglypuff"));
+ Assert.Equal(3, props.GetTokens().Count());
+
+ }
+
+ //public class TestAuthHandler : IAuthenticationHandler
+ //{
+ // private readonly AuthenticationProperties _props;
+ // public TestAuthHandler(AuthenticationProperties props)
+ // {
+ // _props = props;
+ // }
+
+ // public Task AuthenticateAsync(AuthenticateContext context)
+ // {
+ // context.Authenticated(new ClaimsPrincipal(), _props.Items, new Dictionary<string, object>());
+ // return Task.FromResult(0);
+ // }
+
+ // public Task ChallengeAsync(AuthenticationProperties properties)
+ // {
+ // throw new NotImplementedException();
+ // }
+
+ // public void GetDescriptions(DescribeSchemesContext context)
+ // {
+ // throw new NotImplementedException();
+ // }
+
+ // public Task SignInAsync(ClaimsPrincipal principal, AuthenticationProperties properties)
+ // {
+ // throw new NotImplementedException();
+ // }
+
+ // public Task SignOutAsync(AuthenticationProperties properties)
+ // {
+ // throw new NotImplementedException();
+ // }
+ //}
+
+ //[Fact]
+ //public async Task CanGetTokenFromContext()
+ //{
+ // var props = new AuthenticationProperties();
+ // var tokens = new List<AuthenticationToken>();
+ // var tok1 = new AuthenticationToken { Name = "One", Value = "1" };
+ // var tok2 = new AuthenticationToken { Name = "Two", Value = "2" };
+ // var tok3 = new AuthenticationToken { Name = "Three", Value = "3" };
+ // tokens.Add(tok1);
+ // tokens.Add(tok2);
+ // tokens.Add(tok3);
+ // props.StoreTokens(tokens);
+
+ // var context = new DefaultHttpContext();
+ // var handler = new TestAuthHandler(props);
+ // context.Features.Set<IHttpAuthenticationFeature>(new HttpAuthenticationFeature() { Handler = handler });
+
+ // Assert.Equal("1", await context.GetTokenAsync("One"));
+ // Assert.Equal("2", await context.GetTokenAsync("Two"));
+ // Assert.Equal("3", await context.GetTokenAsync("Three"));
+ //}
+
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Transaction.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Transaction.cs
new file mode 100644
index 0000000000..f7128a6f11
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/Transaction.cs
@@ -0,0 +1,62 @@
+// 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.Linq;
+using System.Net.Http;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNetCore.Authentication
+{
+ public class Transaction
+ {
+ public HttpRequestMessage Request { get; set; }
+ public HttpResponseMessage Response { get; set; }
+
+ public IList<string> SetCookie { get; set; }
+
+ public string ResponseText { get; set; }
+ public XElement ResponseElement { get; set; }
+
+ public string AuthenticationCookieValue
+ {
+ get
+ {
+ if (SetCookie != null && SetCookie.Count > 0)
+ {
+ var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme + "="));
+ if (authCookie != null)
+ {
+ return authCookie.Substring(0, authCookie.IndexOf(';'));
+ }
+ }
+
+ return null;
+ }
+ }
+
+ public string FindClaimValue(string claimType, string issuer = null)
+ {
+ var claim = ResponseElement.Elements("claim")
+ .SingleOrDefault(elt => elt.Attribute("type").Value == claimType &&
+ (issuer == null || elt.Attribute("issuer").Value == issuer));
+ if (claim == null)
+ {
+ return null;
+ }
+ return claim.Attribute("value").Value;
+ }
+
+ public string FindTokenValue(string name)
+ {
+ var claim = ResponseElement.Elements("token")
+ .SingleOrDefault(elt => elt.Attribute("name").Value == name);
+ if (claim == null)
+ {
+ return null;
+ }
+ return claim.Attribute("value").Value;
+ }
+
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TwitterTests.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TwitterTests.cs
new file mode 100644
index 0000000000..c1937d136c
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/TwitterTests.cs
@@ -0,0 +1,685 @@
+// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.Tests;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.Twitter
+{
+ public class TwitterTests
+ {
+ private void ConfigureDefaults(TwitterOptions o)
+ {
+ o.ConsumerKey = "whatever";
+ o.ConsumerSecret = "whatever";
+ o.SignInScheme = "auth1";
+ }
+
+ [Fact]
+ public async Task CanForwardDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = TwitterDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler>("auth1", "auth1");
+ })
+ .AddTwitter(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ });
+
+ var forwardDefault = new TestHandler();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignInThrows()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = TwitterDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddTwitter(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+ }
+
+ [Fact]
+ public async Task ForwardSignOutThrows()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = TwitterDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddTwitter(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardSignOut = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ }
+
+ [Fact]
+ public async Task ForwardForbidWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = TwitterDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddTwitter(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ForbidAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(1, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardAuthenticateWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = TwitterDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddTwitter(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardAuthenticate = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(1, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardChallengeWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = TwitterDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler>("specific", "specific");
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ })
+ .AddTwitter(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardChallenge = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.ChallengeAsync();
+ Assert.Equal(0, specific.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(1, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ }
+
+ [Fact]
+ public async Task ForwardSelectorWinsOverDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = TwitterDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddTwitter(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, selector.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, selector.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, selector.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task NullForwardSelectorUsesDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = TwitterDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddTwitter(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => null;
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, forwardDefault.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, forwardDefault.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, forwardDefault.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ Assert.Equal(0, specific.AuthenticateCount);
+ Assert.Equal(0, specific.ForbidCount);
+ Assert.Equal(0, specific.ChallengeCount);
+ Assert.Equal(0, specific.SignInCount);
+ Assert.Equal(0, specific.SignOutCount);
+ }
+
+ [Fact]
+ public async Task SpecificForwardWinsOverSelectorAndDefault()
+ {
+ var services = new ServiceCollection().AddLogging();
+ services.AddAuthentication(o =>
+ {
+ o.DefaultScheme = TwitterDefaults.AuthenticationScheme;
+ o.AddScheme<TestHandler2>("auth1", "auth1");
+ o.AddScheme<TestHandler3>("selector", "selector");
+ o.AddScheme<TestHandler>("specific", "specific");
+ })
+ .AddTwitter(o =>
+ {
+ ConfigureDefaults(o);
+ o.ForwardDefault = "auth1";
+ o.ForwardDefaultSelector = _ => "selector";
+ o.ForwardAuthenticate = "specific";
+ o.ForwardChallenge = "specific";
+ o.ForwardSignIn = "specific";
+ o.ForwardSignOut = "specific";
+ o.ForwardForbid = "specific";
+ });
+
+ var specific = new TestHandler();
+ services.AddSingleton(specific);
+ var forwardDefault = new TestHandler2();
+ services.AddSingleton(forwardDefault);
+ var selector = new TestHandler3();
+ services.AddSingleton(selector);
+
+ var sp = services.BuildServiceProvider();
+ var context = new DefaultHttpContext();
+ context.RequestServices = sp;
+
+ await context.AuthenticateAsync();
+ Assert.Equal(1, specific.AuthenticateCount);
+
+ await context.ForbidAsync();
+ Assert.Equal(1, specific.ForbidCount);
+
+ await context.ChallengeAsync();
+ Assert.Equal(1, specific.ChallengeCount);
+
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
+
+ Assert.Equal(0, forwardDefault.AuthenticateCount);
+ Assert.Equal(0, forwardDefault.ForbidCount);
+ Assert.Equal(0, forwardDefault.ChallengeCount);
+ Assert.Equal(0, forwardDefault.SignInCount);
+ Assert.Equal(0, forwardDefault.SignOutCount);
+ Assert.Equal(0, selector.AuthenticateCount);
+ Assert.Equal(0, selector.ForbidCount);
+ Assert.Equal(0, selector.ChallengeCount);
+ Assert.Equal(0, selector.SignInCount);
+ Assert.Equal(0, selector.SignOutCount);
+ }
+
+ [Fact]
+ public async Task VerifySignInSchemeCannotBeSetToSelf()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ConsumerKey = "Test Consumer Key";
+ o.ConsumerSecret = "Test Consumer Secret";
+ o.SignInScheme = TwitterDefaults.AuthenticationScheme;
+ });
+ var error = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendAsync("https://example.com/challenge"));
+ Assert.Contains("cannot be set to itself", error.Message);
+ }
+
+ [Fact]
+ public async Task VerifySchemeDefaults()
+ {
+ var services = new ServiceCollection();
+ services.AddAuthentication().AddTwitter();
+ var sp = services.BuildServiceProvider();
+ var schemeProvider = sp.GetRequiredService<IAuthenticationSchemeProvider>();
+ var scheme = await schemeProvider.GetSchemeAsync(TwitterDefaults.AuthenticationScheme);
+ Assert.NotNull(scheme);
+ Assert.Equal("TwitterHandler", scheme.HandlerType.Name);
+ Assert.Equal(TwitterDefaults.AuthenticationScheme, scheme.DisplayName);
+ }
+
+ [Fact]
+ public async Task ChallengeWillTriggerApplyRedirectEvent()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ConsumerKey = "Test Consumer Key";
+ o.ConsumerSecret = "Test Consumer Secret";
+ o.Events = new TwitterEvents
+ {
+ OnRedirectToAuthorizationEndpoint = context =>
+ {
+ context.Response.Redirect(context.RedirectUri + "&custom=test");
+ return Task.FromResult(0);
+ }
+ };
+ o.BackchannelHttpHandler = new TestHttpMessageHandler
+ {
+ Sender = BackchannelRequestToken
+ };
+ },
+ async context =>
+ {
+ await context.ChallengeAsync("Twitter");
+ return true;
+ });
+ var transaction = await server.SendAsync("http://example.com/challenge");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var query = transaction.Response.Headers.Location.Query;
+ Assert.Contains("custom=test", query);
+ }
+
+ /// <summary>
+ /// Validates the Twitter Options to check if the Consumer Key is missing in the TwitterOptions and if so throws the ArgumentException
+ /// </summary>
+ /// <returns></returns>
+ [Fact]
+ public async Task ThrowsIfClientIdMissing()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ConsumerSecret = "Test Consumer Secret";
+ });
+
+ await Assert.ThrowsAsync<ArgumentException>("ConsumerKey", async () => await server.SendAsync("http://example.com/challenge"));
+ }
+
+ /// <summary>
+ /// Validates the Twitter Options to check if the Consumer Secret is missing in the TwitterOptions and if so throws the ArgumentException
+ /// </summary>
+ /// <returns></returns>
+ [Fact]
+ public async Task ThrowsIfClientSecretMissing()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ConsumerKey = "Test Consumer Key";
+ });
+
+ await Assert.ThrowsAsync<ArgumentException>("ConsumerSecret", async () => await server.SendAsync("http://example.com/challenge"));
+ }
+
+ [Fact]
+ public async Task BadSignInWillThrow()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ConsumerKey = "Test Consumer Key";
+ o.ConsumerSecret = "Test Consumer Secret";
+ });
+
+ // Send a bogus sign in
+ var error = await Assert.ThrowsAnyAsync<Exception>(() => server.SendAsync("https://example.com/signin-twitter"));
+ Assert.Equal("Invalid state cookie.", error.GetBaseException().Message);
+ }
+
+ [Fact]
+ public async Task SignInThrows()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ConsumerKey = "Test Consumer Key";
+ o.ConsumerSecret = "Test Consumer Secret";
+ });
+ var transaction = await server.SendAsync("https://example.com/signIn");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task SignOutThrows()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ConsumerKey = "Test Consumer Key";
+ o.ConsumerSecret = "Test Consumer Secret";
+ });
+ var transaction = await server.SendAsync("https://example.com/signOut");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ForbidThrows()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ConsumerKey = "Test Consumer Key";
+ o.ConsumerSecret = "Test Consumer Secret";
+ });
+ var transaction = await server.SendAsync("https://example.com/signOut");
+ Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ChallengeWillTriggerRedirection()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ConsumerKey = "Test Consumer Key";
+ o.ConsumerSecret = "Test Consumer Secret";
+ o.BackchannelHttpHandler = new TestHttpMessageHandler
+ {
+ Sender = BackchannelRequestToken
+ };
+ },
+ async context =>
+ {
+ await context.ChallengeAsync("Twitter");
+ return true;
+ });
+ var transaction = await server.SendAsync("http://example.com/challenge");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var location = transaction.Response.Headers.Location.AbsoluteUri;
+ Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location);
+ }
+
+ [Fact]
+ public async Task BadCallbackCallsRemoteAuthFailedWithState()
+ {
+ var server = CreateServer(o =>
+ {
+ o.ConsumerKey = "Test Consumer Key";
+ o.ConsumerSecret = "Test Consumer Secret";
+ o.BackchannelHttpHandler = new TestHttpMessageHandler
+ {
+ Sender = BackchannelRequestToken
+ };
+ o.Events = new TwitterEvents()
+ {
+ OnRemoteFailure = context =>
+ {
+ Assert.NotNull(context.Failure);
+ Assert.Equal("The user denied permissions.", context.Failure.Message);
+ Assert.NotNull(context.Properties);
+ Assert.Equal("testvalue", context.Properties.Items["testkey"]);
+ context.Response.StatusCode = StatusCodes.Status406NotAcceptable;
+ context.HandleResponse();
+ return Task.CompletedTask;
+ }
+ };
+ },
+ async context =>
+ {
+ var properties = new AuthenticationProperties();
+ properties.Items["testkey"] = "testvalue";
+ await context.ChallengeAsync("Twitter", properties);
+ return true;
+ });
+ var transaction = await server.SendAsync("http://example.com/challenge");
+ Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
+ var location = transaction.Response.Headers.Location.AbsoluteUri;
+ Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location);
+ Assert.True(transaction.Response.Headers.TryGetValues(HeaderNames.SetCookie, out var setCookie));
+ Assert.True(SetCookieHeaderValue.TryParseList(setCookie.ToList(), out var setCookieValues));
+ Assert.Single(setCookieValues);
+ var setCookieValue = setCookieValues.Single();
+ var cookie = new CookieHeaderValue(setCookieValue.Name, setCookieValue.Value);
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "/signin-twitter?denied=ABCDEFG");
+ request.Headers.Add(HeaderNames.Cookie, cookie.ToString());
+ var client = server.CreateClient();
+ var response = await client.SendAsync(request);
+
+ Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode);
+ }
+
+ private static TestServer CreateServer(Action<TwitterOptions> options, Func<HttpContext, Task<bool>> handler = null)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Use(async (context, next) =>
+ {
+ var req = context.Request;
+ var res = context.Response;
+ if (req.Path == new PathString("/signIn"))
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync("Twitter", new ClaimsPrincipal()));
+ }
+ else if (req.Path == new PathString("/signOut"))
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync("Twitter"));
+ }
+ else if (req.Path == new PathString("/forbid"))
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => context.ForbidAsync("Twitter"));
+ }
+ else if (handler == null || ! await handler(context))
+ {
+ await next();
+ }
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ Action<TwitterOptions> wrapOptions = o =>
+ {
+ o.SignInScheme = "External";
+ options(o);
+ };
+ services.AddAuthentication()
+ .AddCookie("External", _ => { })
+ .AddTwitter(wrapOptions);
+ });
+ return new TestServer(builder);
+ }
+
+ private HttpResponseMessage BackchannelRequestToken(HttpRequestMessage req)
+ {
+ if (req.RequestUri.AbsoluteUri == "https://api.twitter.com/oauth/request_token")
+ {
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content =
+ new StringContent("oauth_callback_confirmed=true&oauth_token=test_oauth_token&oauth_token_secret=test_oauth_token_secret",
+ Encoding.UTF8,
+ "application/x-www-form-urlencoded")
+ };
+ }
+ throw new NotImplementedException(req.RequestUri.AbsoluteUri);
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/CustomStateDataFormat.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/CustomStateDataFormat.cs
new file mode 100644
index 0000000000..0de867d286
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/CustomStateDataFormat.cs
@@ -0,0 +1,58 @@
+// 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.Runtime.Serialization;
+using System.Text;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ public class CustomStateDataFormat : ISecureDataFormat<AuthenticationProperties>
+ {
+ public const string ValidStateData = "ValidStateData";
+
+ private string lastSavedAuthenticationProperties;
+ private DataContractSerializer serializer = new DataContractSerializer(typeof(AuthenticationProperties));
+
+ public string Protect(AuthenticationProperties data)
+ {
+ lastSavedAuthenticationProperties = Serialize(data);
+ return ValidStateData;
+ }
+
+ public string Protect(AuthenticationProperties data, string purpose)
+ {
+ return Protect(data);
+ }
+
+ public AuthenticationProperties Unprotect(string state)
+ {
+ return state == ValidStateData ? DeSerialize(lastSavedAuthenticationProperties) : null;
+ }
+
+ public AuthenticationProperties Unprotect(string protectedText, string purpose)
+ {
+ return Unprotect(protectedText);
+ }
+
+ private string Serialize(AuthenticationProperties data)
+ {
+ using (MemoryStream memoryStream = new MemoryStream())
+ {
+ serializer.WriteObject(memoryStream, data);
+ memoryStream.Position = 0;
+ return new StreamReader(memoryStream).ReadToEnd();
+ }
+ }
+
+ private AuthenticationProperties DeSerialize(string state)
+ {
+ var stateDataAsBytes = Encoding.UTF8.GetBytes(state);
+
+ using (var ms = new MemoryStream(stateDataAsBytes, false))
+ {
+ return (AuthenticationProperties)serializer.ReadObject(ms);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/InvalidToken.xml b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/InvalidToken.xml
new file mode 100644
index 0000000000..dfdb0d68d0
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/InvalidToken.xml
@@ -0,0 +1,83 @@
+<t:RequestSecurityTokenResponse Context="WsFedOwinState=AQAAANCMnd8BFdERjHoAwE_Cl-sBAAAAzaTmu3688ESVbKJen1i8YwAAAAACAAAAAAADZgAAwAAAABAAAADoUPrFjHqMTp30emvI0XZ_AAAAAASAAACgAAAAEAAAAGTBC8oT24BI8BSJf4SbwjowAAAAA4ip7JyKg6vyK-PtWTapIASA3XLOXiIj8KFO3cuSd4t4H4o-W_wnQl2FAKMOKNNrFAAAAEoWRHnCSYvPKPo0kU09EciG6TJS" xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
+ <t:Lifetime>
+ <wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2014-04-18T20:21:17.341Z</wsu:Created>
+ <wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2014-04-19T08:21:17.341Z</wsu:Expires>
+ </t:Lifetime>
+ <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
+ <EndpointReference xmlns="http://www.w3.org/2005/08/addressing">
+ <Address>http://automation1/</Address>
+ </EndpointReference>
+ </wsp:AppliesTo>
+ <t:RequestedSecurityToken>
+ <Assertion ID="_660ec874-f70a-4997-a9c4-bd591f1c7469" IssueInstant="2014-04-18T20:21:17.450Z" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
+ <Issuer>https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/</Issuer>
+ <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+ <ds:SignedInfo>
+ <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
+ <ds:Reference URI="#_660ec874-f70a-4997-a9c4-bd591f1c7469">
+ <ds:Transforms>
+ <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
+ <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ </ds:Transforms>
+ <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
+ <ds:DigestValue>Lkq0wTyTFxLUU2cyx0XybJqhka5RzRGj6kC4aIpFg+g=</ds:DigestValue>
+ </ds:Reference>
+ </ds:SignedInfo>
+ <ds:SignatureValue>bPwNswOB/B9xcdAljIkin9A2vjq+u94JdyvK03mf8vZFGUYNu9uN/Q6ims1DvW1FnP7SgFBwhIvW5OjZyW8fdYGhC2bq36izkxH6ulkWbciOcyELkyHDACLudvh8kP/Q+IwpicefKzAeI2Qu/5MFq16vFg5YgI+dovg8u1fYPPEPmmptW893RNTHWeh9mLRpLYnHyg7aLG6emNRkEu7w9rzeoICeMFybb9BvJl/q/8MFCW/Z5WemQhCi6YXFSEwCO6zJzCFi/3T6ChU/xYgXbFykDLqulsNOCQxdgutyqxJzugt+3PH5IKHHuoqe7UZNUIyELJ4BgwE1sXCGYIi24rg==</ds:SignatureValue>
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </ds:Signature>
+ <Subject>
+ <NameID>t0ch1TsP0pi5VoW8q5CGWsCXVZoNtpsg0mbMZPOYb4I</NameID>
+ <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer" />
+ </Subject>
+ <Conditions NotBefore="2014-04-18T20:21:17.341Z" NotOnOrAfter="2014-04-19T08:21:17.341Z">
+ <AudienceRestriction>
+ <Audience>http://Automation1</Audience>
+ </AudienceRestriction>
+ </Conditions>
+ <AttributeStatement>
+ <Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname">
+ <AttributeValue>Test</AttributeValue>
+ </Attribute>
+ <Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname">
+ <AttributeValue>Test</AttributeValue>
+ </Attribute>
+ <Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name">
+ <AttributeValue>user1@praburajgmail.onmicrosoft.com</AttributeValue>
+ </Attribute>
+ <Attribute Name="http://schemas.microsoft.com/identity/claims/tenantid">
+ <AttributeValue>4afbc689-805b-48cf-a24c-d4aa3248a248</AttributeValue>
+ </Attribute>
+ <Attribute Name="http://schemas.microsoft.com/identity/claims/objectidentifier">
+ <AttributeValue>c2f0cd49-5e53-4520-8ed9-4e178dc488c5</AttributeValue>
+ </Attribute>
+ <Attribute Name="http://schemas.microsoft.com/identity/claims/identityprovider">
+ <AttributeValue>https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/</AttributeValue>
+ </Attribute>
+ </AttributeStatement>
+ <AuthnStatement AuthnInstant="2014-04-18T20:21:14.000Z">
+ <AuthnContext>
+ <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef>
+ </AuthnContext>
+ </AuthnStatement>
+ </Assertion>
+ </t:RequestedSecurityToken>
+ <t:RequestedAttachedReference>
+ <SecurityTokenReference d3p1:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0" xmlns:d3p1="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
+ <KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLID">_660ec874-f70a-4997-a9c4-bd591f1c7469</KeyIdentifier>
+ </SecurityTokenReference>
+ </t:RequestedAttachedReference>
+ <t:RequestedUnattachedReference>
+ <SecurityTokenReference d3p1:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0" xmlns:d3p1="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
+ <KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLID">_660ec874-f70a-4997-a9c4-bd591f1c7469</KeyIdentifier>
+ </SecurityTokenReference>
+ </t:RequestedUnattachedReference>
+ <t:TokenType>http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0</t:TokenType>
+ <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
+ <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>
+</t:RequestSecurityTokenResponse> \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityToken.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityToken.cs
new file mode 100644
index 0000000000..dfe8607242
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityToken.cs
@@ -0,0 +1,27 @@
+// 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.IdentityModel.Tokens;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ internal class TestSecurityToken : SecurityToken
+ {
+ public override string Id => "id";
+
+ public override string Issuer => "issuer";
+
+ public override SecurityKey SecurityKey => throw new NotImplementedException();
+
+ public override SecurityKey SigningKey
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public override DateTime ValidFrom => new DateTime(2008, 3, 22);
+
+ public override DateTime ValidTo => new DateTime(2017, 3, 22);
+ }
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityTokenValidator.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityTokenValidator.cs
new file mode 100644
index 0000000000..05882518f9
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityTokenValidator.cs
@@ -0,0 +1,31 @@
+// 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.Claims;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ internal class TestSecurityTokenValidator : ISecurityTokenValidator
+ {
+ public bool CanValidateToken => true;
+
+ public int MaximumTokenSizeInBytes { get; set; } = 1024 * 5;
+
+ public bool CanReadToken(string securityToken)
+ {
+ return true;
+ }
+
+ public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
+ {
+ if (!string.IsNullOrEmpty(securityToken) && securityToken.Contains("ThisIsAValidToken"))
+ {
+ validatedToken = new TestSecurityToken();
+ return new ClaimsPrincipal(new ClaimsIdentity("Test"));
+ }
+
+ throw new SecurityTokenException("The security token did not contain ThisIsAValidToken");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/ValidToken.xml b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/ValidToken.xml
new file mode 100644
index 0000000000..2addae96c1
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/ValidToken.xml
@@ -0,0 +1,83 @@
+<t:RequestSecurityTokenResponse Context="WsFedOwinState=AQAAANCMnd8BFdERjHoAwE_Cl-sBAAAAzaTmu3688ESVbKJen1i8YwAAAAACAAAAAAADZgAAwAAAABAAAADoUPrFjHqMTp30emvI0XZ_AAAAAASAAACgAAAAEAAAAGTBC8oT24BI8BSJf4SbwjowAAAAA4ip7JyKg6vyK-PtWTapIASA3XLOXiIj8KFO3cuSd4t4H4o-W_wnQl2FAKMOKNNrFAAAAEoWRHnCSYvPKPo0kU09EciG6TJS" xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
+ <t:Lifetime>
+ <wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2014-04-18T20:21:17.341Z</wsu:Created>
+ <wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2014-04-19T08:21:17.341Z</wsu:Expires>
+ </t:Lifetime>
+ <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
+ <EndpointReference xmlns="http://www.w3.org/2005/08/addressing">
+ <Address>http://automation1/</Address>
+ </EndpointReference>
+ </wsp:AppliesTo>
+ <t:RequestedSecurityToken>
+ <Assertion ID="_660ec874-f70a-4997-a9c4-bd591f1c7469" IssueInstant="2014-04-18T20:21:17.450Z" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
+ <Issuer>https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/</Issuer>
+ <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+ <ds:SignedInfo>
+ <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
+ <ds:Reference URI="#_660ec874-f70a-4997-a9c4-bd591f1c7469">
+ <ds:Transforms>
+ <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
+ <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ </ds:Transforms>
+ <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
+ <ds:DigestValue>Lkq0wTyTFxLUU2cyx0XybJqhka5RzRGj6kC4aIpFg+g=</ds:DigestValue>
+ </ds:Reference>
+ </ds:SignedInfo>
+ <ds:SignatureValue>bPwNswOB/B9xcdAljIkin9A2vjq+u94JdyvK03mf8vZFGUYNu9uN/Q6ims1DvW1FnP7SgFBwhIvW5OjZyW8fdYGhC2bq36izkxH6ulkWbciOcyELkyHDACLudvh8kP/Q+IwpicefKzAeI2Qu/5MFq16vFg5YgI+dovg8u1fYPPEPmmptW893RNTHWeh9mLRpLYnHyg7aLG6emNRkEu7w9rzeoICeMFybb9BvJl/q/8MFCW/Z5WemQhCi6YXFSEwCO6zJzCFi/3T6ChU/xYgXbFykDLqulsNOCQxdgutyqxJzugt+3PH5IKHHuoqe7UZNUIyELJ4BgwE1sXCGYIi24rg==</ds:SignatureValue>
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>ThisIsAValidToken</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </ds:Signature>
+ <Subject>
+ <NameID>t0ch1TsP0pi5VoW8q5CGWsCXVZoNtpsg0mbMZPOYb4I</NameID>
+ <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer" />
+ </Subject>
+ <Conditions NotBefore="2014-04-18T20:21:17.341Z" NotOnOrAfter="2014-04-19T08:21:17.341Z">
+ <AudienceRestriction>
+ <Audience>http://Automation1</Audience>
+ </AudienceRestriction>
+ </Conditions>
+ <AttributeStatement>
+ <Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname">
+ <AttributeValue>Test</AttributeValue>
+ </Attribute>
+ <Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname">
+ <AttributeValue>Test</AttributeValue>
+ </Attribute>
+ <Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name">
+ <AttributeValue>user1@praburajgmail.onmicrosoft.com</AttributeValue>
+ </Attribute>
+ <Attribute Name="http://schemas.microsoft.com/identity/claims/tenantid">
+ <AttributeValue>4afbc689-805b-48cf-a24c-d4aa3248a248</AttributeValue>
+ </Attribute>
+ <Attribute Name="http://schemas.microsoft.com/identity/claims/objectidentifier">
+ <AttributeValue>c2f0cd49-5e53-4520-8ed9-4e178dc488c5</AttributeValue>
+ </Attribute>
+ <Attribute Name="http://schemas.microsoft.com/identity/claims/identityprovider">
+ <AttributeValue>https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/</AttributeValue>
+ </Attribute>
+ </AttributeStatement>
+ <AuthnStatement AuthnInstant="2014-04-18T20:21:14.000Z">
+ <AuthnContext>
+ <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef>
+ </AuthnContext>
+ </AuthnStatement>
+ </Assertion>
+ </t:RequestedSecurityToken>
+ <t:RequestedAttachedReference>
+ <SecurityTokenReference d3p1:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0" xmlns:d3p1="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
+ <KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLID">_660ec874-f70a-4997-a9c4-bd591f1c7469</KeyIdentifier>
+ </SecurityTokenReference>
+ </t:RequestedAttachedReference>
+ <t:RequestedUnattachedReference>
+ <SecurityTokenReference d3p1:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0" xmlns:d3p1="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
+ <KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLID">_660ec874-f70a-4997-a9c4-bd591f1c7469</KeyIdentifier>
+ </SecurityTokenReference>
+ </t:RequestedUnattachedReference>
+ <t:TokenType>http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0</t:TokenType>
+ <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
+ <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>
+</t:RequestSecurityTokenResponse> \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/WsFederationTest.cs b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/WsFederationTest.cs
new file mode 100644
index 0000000000..bc1ef757f1
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/WsFederationTest.cs
@@ -0,0 +1,443 @@
+// 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.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.IdentityModel.Tokens;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authentication.WsFederation
+{
+ public class WsFederationTest
+ {
+ [Fact]
+ public async Task VerifySchemeDefaults()
+ {
+ var services = new ServiceCollection();
+ services.AddAuthentication().AddWsFederation();
+ var sp = services.BuildServiceProvider();
+ var schemeProvider = sp.GetRequiredService<IAuthenticationSchemeProvider>();
+ var scheme = await schemeProvider.GetSchemeAsync(WsFederationDefaults.AuthenticationScheme);
+ Assert.NotNull(scheme);
+ Assert.Equal("WsFederationHandler", scheme.HandlerType.Name);
+ Assert.Equal(WsFederationDefaults.AuthenticationScheme, scheme.DisplayName);
+ }
+
+ [Fact]
+ public async Task MissingConfigurationThrows()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(ConfigureApp)
+ .ConfigureServices(services =>
+ {
+ services.AddAuthentication(sharedOptions =>
+ {
+ sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
+ })
+ .AddCookie()
+ .AddWsFederation();
+ });
+ var server = new TestServer(builder);
+ var httpClient = server.CreateClient();
+
+ // Verify if the request is redirected to STS with right parameters
+ var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => httpClient.GetAsync("/"));
+ Assert.Equal("Provide MetadataAddress, Configuration, or ConfigurationManager to WsFederationOptions", exception.Message);
+ }
+
+ [Fact]
+ public async Task ChallengeRedirects()
+ {
+ var httpClient = CreateClient();
+
+ // Verify if the request is redirected to STS with right parameters
+ var response = await httpClient.GetAsync("/");
+ Assert.Equal("https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed", response.Headers.Location.GetLeftPart(System.UriPartial.Path));
+ var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query);
+
+ Assert.Equal("http://Automation1", queryItems["wtrealm"]);
+ Assert.True(queryItems["wctx"].ToString().Equals(CustomStateDataFormat.ValidStateData), "wctx does not equal ValidStateData");
+ Assert.Equal(httpClient.BaseAddress + "signin-wsfed", queryItems["wreply"]);
+ Assert.Equal("wsignin1.0", queryItems["wa"]);
+ }
+
+ [Fact]
+ public async Task MapWillNotAffectRedirect()
+ {
+ var httpClient = CreateClient();
+
+ // Verify if the request is redirected to STS with right parameters
+ var response = await httpClient.GetAsync("/mapped-challenge");
+ Assert.Equal("https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed", response.Headers.Location.GetLeftPart(System.UriPartial.Path));
+ var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query);
+
+ Assert.Equal("http://Automation1", queryItems["wtrealm"]);
+ Assert.True(queryItems["wctx"].ToString().Equals(CustomStateDataFormat.ValidStateData), "wctx does not equal ValidStateData");
+ Assert.Equal(httpClient.BaseAddress + "signin-wsfed", queryItems["wreply"]);
+ Assert.Equal("wsignin1.0", queryItems["wa"]);
+ }
+
+ [Fact]
+ public async Task PreMappedWillAffectRedirect()
+ {
+ var httpClient = CreateClient();
+
+ // Verify if the request is redirected to STS with right parameters
+ var response = await httpClient.GetAsync("/premapped-challenge");
+ Assert.Equal("https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed", response.Headers.Location.GetLeftPart(System.UriPartial.Path));
+ var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query);
+
+ Assert.Equal("http://Automation1", queryItems["wtrealm"]);
+ Assert.True(queryItems["wctx"].ToString().Equals(CustomStateDataFormat.ValidStateData), "wctx does not equal ValidStateData");
+ Assert.Equal(httpClient.BaseAddress + "premapped-challenge/signin-wsfed", queryItems["wreply"]);
+ Assert.Equal("wsignin1.0", queryItems["wa"]);
+ }
+
+ [Fact]
+ public async Task ValidTokenIsAccepted()
+ {
+ var httpClient = CreateClient();
+
+ // Verify if the request is redirected to STS with right parameters
+ var response = await httpClient.GetAsync("/");
+ var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query);
+
+ var request = new HttpRequestMessage(HttpMethod.Post, queryItems["wreply"]);
+ CopyCookies(response, request);
+ request.Content = CreateSignInContent("WsFederation/ValidToken.xml", queryItems["wctx"]);
+ response = await httpClient.SendAsync(request);
+
+ Assert.Equal(HttpStatusCode.Found, response.StatusCode);
+
+ request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location);
+ CopyCookies(response, request);
+ response = await httpClient.SendAsync(request);
+
+ // Did the request end in the actual resource requested for
+ Assert.Equal(WsFederationDefaults.AuthenticationScheme, await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task ValidUnsolicitedTokenIsRefused()
+ {
+ var httpClient = CreateClient();
+ var form = CreateSignInContent("WsFederation/ValidToken.xml", suppressWctx: true);
+ var exception = await Assert.ThrowsAsync<Exception>(() => httpClient.PostAsync(httpClient.BaseAddress + "signin-wsfed", form));
+ Assert.Contains("Unsolicited logins are not allowed.", exception.InnerException.Message);
+ }
+
+ [Fact]
+ public async Task ValidUnsolicitedTokenIsAcceptedWhenAllowed()
+ {
+ var httpClient = CreateClient(allowUnsolicited: true);
+
+ var form = CreateSignInContent("WsFederation/ValidToken.xml", suppressWctx: true);
+ var response = await httpClient.PostAsync(httpClient.BaseAddress + "signin-wsfed", form);
+
+ Assert.Equal(HttpStatusCode.Found, response.StatusCode);
+
+ var request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location);
+ CopyCookies(response, request);
+ response = await httpClient.SendAsync(request);
+
+ // Did the request end in the actual resource requested for
+ Assert.Equal(WsFederationDefaults.AuthenticationScheme, await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task InvalidTokenIsRejected()
+ {
+ var httpClient = CreateClient();
+
+ // Verify if the request is redirected to STS with right parameters
+ var response = await httpClient.GetAsync("/");
+ var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query);
+
+ var request = new HttpRequestMessage(HttpMethod.Post, queryItems["wreply"]);
+ CopyCookies(response, request);
+ request.Content = CreateSignInContent("WsFederation/InvalidToken.xml", queryItems["wctx"]);
+ response = await httpClient.SendAsync(request);
+
+ // Did the request end in the actual resource requested for
+ Assert.Equal("AuthenticationFailed", await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task RemoteSignoutRequestTriggersSignout()
+ {
+ var httpClient = CreateClient();
+
+ var response = await httpClient.GetAsync("/signin-wsfed?wa=wsignoutcleanup1.0");
+ response.EnsureSuccessStatusCode();
+
+ var cookie = response.Headers.GetValues(HeaderNames.SetCookie).Single();
+ Assert.Equal(".AspNetCore.Cookies=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; samesite=lax", cookie);
+ Assert.Equal("OnRemoteSignOut", response.Headers.GetValues("EventHeader").Single());
+ Assert.Equal("", await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task EventsResolvedFromDI()
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton<MyWsFedEvents>();
+ services.AddAuthentication(sharedOptions =>
+ {
+ sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
+ })
+ .AddCookie()
+ .AddWsFederation(options =>
+ {
+ options.Wtrealm = "http://Automation1";
+ options.MetadataAddress = "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/federationmetadata/2007-06/federationmetadata.xml";
+ options.BackchannelHttpHandler = new WaadMetadataDocumentHandler();
+ options.EventsType = typeof(MyWsFedEvents);
+ });
+ })
+ .Configure(app =>
+ {
+ app.Run(context => context.ChallengeAsync());
+ });
+ var server = new TestServer(builder);
+
+ var result = await server.CreateClient().GetAsync("");
+ Assert.Contains("CustomKey=CustomValue", result.Headers.Location.Query);
+ }
+
+ private class MyWsFedEvents : WsFederationEvents
+ {
+ public override Task RedirectToIdentityProvider(RedirectContext context)
+ {
+ context.ProtocolMessage.SetParameter("CustomKey", "CustomValue");
+ return base.RedirectToIdentityProvider(context);
+ }
+ }
+
+ private FormUrlEncodedContent CreateSignInContent(string tokenFile, string wctx = null, bool suppressWctx = false)
+ {
+ var kvps = new List<KeyValuePair<string, string>>();
+ kvps.Add(new KeyValuePair<string, string>("wa", "wsignin1.0"));
+ kvps.Add(new KeyValuePair<string, string>("wresult", File.ReadAllText(tokenFile)));
+ if (!string.IsNullOrEmpty(wctx))
+ {
+ kvps.Add(new KeyValuePair<string, string>("wctx", wctx));
+ }
+ if (suppressWctx)
+ {
+ kvps.Add(new KeyValuePair<string, string>("suppressWctx", "true"));
+ }
+ return new FormUrlEncodedContent(kvps);
+ }
+
+ private void CopyCookies(HttpResponseMessage response, HttpRequestMessage request)
+ {
+ var cookies = SetCookieHeaderValue.ParseList(response.Headers.GetValues(HeaderNames.SetCookie).ToList());
+ foreach (var cookie in cookies)
+ {
+ if (cookie.Value.HasValue)
+ {
+ request.Headers.Add(HeaderNames.Cookie, new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
+ }
+ }
+ }
+
+ private HttpClient CreateClient(bool allowUnsolicited = false)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(ConfigureApp)
+ .ConfigureServices(services =>
+ {
+ services.AddAuthentication(sharedOptions =>
+ {
+ sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
+ })
+ .AddCookie()
+ .AddWsFederation(options =>
+ {
+ options.Wtrealm = "http://Automation1";
+ options.MetadataAddress = "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/federationmetadata/2007-06/federationmetadata.xml";
+ options.BackchannelHttpHandler = new WaadMetadataDocumentHandler();
+ options.StateDataFormat = new CustomStateDataFormat();
+ options.SecurityTokenHandlers = new List<ISecurityTokenValidator>() { new TestSecurityTokenValidator() };
+ options.UseTokenLifetime = false;
+ options.AllowUnsolicitedLogins = allowUnsolicited;
+ options.Events = new WsFederationEvents()
+ {
+ OnMessageReceived = context =>
+ {
+ if (!context.ProtocolMessage.Parameters.TryGetValue("suppressWctx", out var suppress))
+ {
+ Assert.True(context.ProtocolMessage.Wctx.Equals("customValue"), "wctx is not my custom value");
+ }
+ context.HttpContext.Items["MessageReceived"] = true;
+ return Task.FromResult(0);
+ },
+ OnRedirectToIdentityProvider = context =>
+ {
+ if (context.ProtocolMessage.IsSignInMessage)
+ {
+ // Sign in message
+ context.ProtocolMessage.Wctx = "customValue";
+ }
+
+ return Task.FromResult(0);
+ },
+ OnSecurityTokenReceived = context =>
+ {
+ context.HttpContext.Items["SecurityTokenReceived"] = true;
+ return Task.FromResult(0);
+ },
+ OnSecurityTokenValidated = context =>
+ {
+ Assert.True((bool)context.HttpContext.Items["MessageReceived"], "MessageReceived notification not invoked");
+ Assert.True((bool)context.HttpContext.Items["SecurityTokenReceived"], "SecurityTokenReceived notification not invoked");
+
+ if (context.Principal != null)
+ {
+ var identity = context.Principal.Identities.Single();
+ identity.AddClaim(new Claim("ReturnEndpoint", "true"));
+ identity.AddClaim(new Claim("Authenticated", "true"));
+ identity.AddClaim(new Claim(identity.RoleClaimType, "Guest", ClaimValueTypes.String));
+ }
+
+ return Task.FromResult(0);
+ },
+ OnAuthenticationFailed = context =>
+ {
+ context.HttpContext.Items["AuthenticationFailed"] = true;
+ //Change the request url to something different and skip Wsfed. This new url will handle the request and let us know if this notification was invoked.
+ context.HttpContext.Request.Path = new PathString("/AuthenticationFailed");
+ context.SkipHandler();
+ return Task.FromResult(0);
+ },
+ OnRemoteSignOut = context =>
+ {
+ context.Response.Headers["EventHeader"] = "OnRemoteSignOut";
+ return Task.FromResult(0);
+ }
+ };
+ });
+ });
+ var server = new TestServer(builder);
+ return server.CreateClient();
+ }
+
+ private void ConfigureApp(IApplicationBuilder app)
+ {
+ app.Map("/PreMapped-Challenge", mapped =>
+ {
+ mapped.UseAuthentication();
+ mapped.Run(async context =>
+ {
+ await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme);
+ });
+ });
+
+ app.UseAuthentication();
+
+ app.Map("/Logout", subApp =>
+ {
+ subApp.Run(async context =>
+ {
+ if (context.User.Identity.IsAuthenticated)
+ {
+ var authProperties = new AuthenticationProperties() { RedirectUri = context.Request.GetEncodedUrl() };
+ await context.SignOutAsync(WsFederationDefaults.AuthenticationScheme, authProperties);
+ await context.Response.WriteAsync("Signing out...");
+ }
+ else
+ {
+ await context.Response.WriteAsync("SignedOut");
+ }
+ });
+ });
+
+ app.Map("/AuthenticationFailed", subApp =>
+ {
+ subApp.Run(async context =>
+ {
+ await context.Response.WriteAsync("AuthenticationFailed");
+ });
+ });
+
+ app.Map("/signout-wsfed", subApp =>
+ {
+ subApp.Run(async context =>
+ {
+ await context.Response.WriteAsync("signout-wsfed");
+ });
+ });
+
+ app.Map("/mapped-challenge", subApp =>
+ {
+ subApp.Run(async context =>
+ {
+ await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme);
+ });
+ });
+
+ app.Run(async context =>
+ {
+ var result = context.AuthenticateAsync();
+ if (context.User == null || !context.User.Identity.IsAuthenticated)
+ {
+ await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme);
+ await context.Response.WriteAsync("Unauthorized");
+ }
+ else
+ {
+ var identity = context.User.Identities.Single();
+ if (identity.NameClaimType == "Name_Failed" && identity.RoleClaimType == "Role_Failed")
+ {
+ context.Response.StatusCode = 500;
+ await context.Response.WriteAsync("SignIn_Failed");
+ }
+ else if (!identity.HasClaim("Authenticated", "true") || !identity.HasClaim("ReturnEndpoint", "true") || !identity.HasClaim(identity.RoleClaimType, "Guest"))
+ {
+ await context.Response.WriteAsync("Provider not invoked");
+ return;
+ }
+ else
+ {
+ await context.Response.WriteAsync(WsFederationDefaults.AuthenticationScheme);
+ }
+ }
+ });
+ }
+
+ private class WaadMetadataDocumentHandler : HttpMessageHandler
+ {
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ var metadata = File.ReadAllText(@"WsFederation/federationmetadata.xml");
+ var newResponse = new HttpResponseMessage() { Content = new StringContent(metadata, Encoding.UTF8, "text/xml") };
+ return Task.FromResult<HttpResponseMessage>(newResponse);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/federationmetadata.xml b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/federationmetadata.xml
new file mode 100644
index 0000000000..920ed66a4f
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/federationmetadata.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<EntityDescriptor ID="_4d16ee22-bedb-4eca-a532-1e5551c7d66e" entityID="https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/" xmlns="urn:oasis:names:tc:SAML:2.0:metadata">
+ <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+ <ds:SignedInfo>
+ <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
+ <ds:Reference URI="#_4d16ee22-bedb-4eca-a532-1e5551c7d66e">
+ <ds:Transforms>
+ <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
+ <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
+ </ds:Transforms>
+ <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
+ <ds:DigestValue>wFJy/A1QstqtLHauYGcqwwHvn3HUW25DcWI/XLOmXOM=</ds:DigestValue>
+ </ds:Reference>
+ </ds:SignedInfo>
+ <ds:SignatureValue>R6fPw+BiFS9XYdkhwNJRjGxVftA2j9TdkF5d5jgR8uG1QMyuEA/Eizeq1HnnUj2Yi+sqNG+HzaZQclECeiJfi88Ry+keorDCo9KgdnjlZZc+WFzrJZeHjaDIvFD6B4OAN0mTq5kbpwr7+idzSbvyRXAnpvJxOrViZKE4HpwltGAZGDTkjsVkd8Z/wfoN7ehN4Ei7u/mOAiEU4FkWYFU/BfSVRVIUDyyQ7DGfQFJvCwHWFvsq+M1wfOUzQO5K+M9EU2m4VEP1qqbexXaZMAbcjqyUn4eN7doWjWE59jkXGbn+GR8qgCJqLOaYwXnH5XD0pMjy71aKGyLNaUb3wCwjkA==</ds:SignatureValue>
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </ds:Signature>
+ <RoleDescriptor xsi:type="fed:SecurityTokenServiceType" protocolSupportEnumeration="http://docs.oasis-open.org/wsfed/federation/200706" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:fed="http://docs.oasis-open.org/wsfed/federation/200706">
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <fed:ClaimTypesOffered>
+ <auth:ClaimType Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" Optional="true" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706">
+ <auth:DisplayName>UPN</auth:DisplayName>
+ <auth:Description>UPN of the user</auth:Description>
+ </auth:ClaimType>
+ <auth:ClaimType Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" Optional="true" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706">
+ <auth:DisplayName>Name</auth:DisplayName>
+ <auth:Description>The display name for the user</auth:Description>
+ </auth:ClaimType>
+ <auth:ClaimType Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" Optional="true" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706">
+ <auth:DisplayName>Given Name</auth:DisplayName>
+ <auth:Description>First name of the user</auth:Description>
+ </auth:ClaimType>
+ <auth:ClaimType Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" Optional="true" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706">
+ <auth:DisplayName>Surname</auth:DisplayName>
+ <auth:Description>Last name of the user</auth:Description>
+ </auth:ClaimType>
+ <auth:ClaimType Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant" Optional="true" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706">
+ <auth:DisplayName>Authentication Instant</auth:DisplayName>
+ <auth:Description>The time (UTC) at which the user authenticated to the identity provider</auth:Description>
+ </auth:ClaimType>
+ <auth:ClaimType Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod" Optional="true" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706">
+ <auth:DisplayName>Authentication Method</auth:DisplayName>
+ <auth:Description>The method of authentication used by the identity provider</auth:Description>
+ </auth:ClaimType>
+ <auth:ClaimType Uri="http://schemas.microsoft.com/identity/claims/tenantid" Optional="true" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706">
+ <auth:DisplayName>TenantId</auth:DisplayName>
+ <auth:Description>Identifier for the user's tenant</auth:Description>
+ </auth:ClaimType>
+ <auth:ClaimType Uri="http://schemas.microsoft.com/identity/claims/identityprovider" Optional="true" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706">
+ <auth:DisplayName>IdentityProvider</auth:DisplayName>
+ <auth:Description>Identity provider for the user.</auth:Description>
+ </auth:ClaimType>
+ </fed:ClaimTypesOffered>
+ <fed:SecurityTokenServiceEndpoint>
+ <EndpointReference xmlns="http://www.w3.org/2005/08/addressing">
+ <Address>https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed</Address>
+ </EndpointReference>
+ </fed:SecurityTokenServiceEndpoint>
+ <fed:PassiveRequestorEndpoint>
+ <EndpointReference xmlns="http://www.w3.org/2005/08/addressing">
+ <Address>https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed</Address>
+ </EndpointReference>
+ </fed:PassiveRequestorEndpoint>
+ </RoleDescriptor>
+ <RoleDescriptor xsi:type="fed:ApplicationServiceType" protocolSupportEnumeration="http://docs.oasis-open.org/ws-sx/ws-trust/200512 http://docs.oasis-open.org/wsfed/federation/200706" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:fed="http://docs.oasis-open.org/wsfed/federation/200706">
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <fed:TargetScopes>
+ <EndpointReference xmlns="http://www.w3.org/2005/08/addressing">
+ <Address>https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/</Address>
+ </EndpointReference>
+ </fed:TargetScopes>
+ <fed:ApplicationServiceEndpoint>
+ <EndpointReference xmlns="http://www.w3.org/2005/08/addressing">
+ <Address>https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed</Address>
+ </EndpointReference>
+ </fed:ApplicationServiceEndpoint>
+ <fed:PassiveRequestorEndpoint>
+ <EndpointReference xmlns="http://www.w3.org/2005/08/addressing">
+ <Address>https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed</Address>
+ </EndpointReference>
+ </fed:PassiveRequestorEndpoint>
+ </RoleDescriptor>
+ <IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/saml2" />
+ <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/saml2" />
+ </IDPSSODescriptor>
+</EntityDescriptor> \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/katanatest.redmond.corp.microsoft.com.cer b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/katanatest.redmond.corp.microsoft.com.cer
new file mode 100644
index 0000000000..bfd5220e2c
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/katanatest.redmond.corp.microsoft.com.cer
Binary files differ
diff --git a/src/Security/test/Microsoft.AspNetCore.Authentication.Test/selfSigned.cer b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/selfSigned.cer
new file mode 100644
index 0000000000..6acc7af5a6
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authentication.Test/selfSigned.cer
Binary files differ
diff --git a/src/Security/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationPolicyFacts.cs b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationPolicyFacts.cs
new file mode 100644
index 0000000000..143be1b9be
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationPolicyFacts.cs
@@ -0,0 +1,159 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization.Infrastructure;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authorization.Test
+{
+ public class AuthorizationPolicyFacts
+ {
+ [Fact]
+ public void RequireRoleThrowsIfEmpty()
+ {
+ Assert.Throws<InvalidOperationException>(() => new AuthorizationPolicyBuilder().RequireRole());
+ }
+
+ [Fact]
+ public async Task CanCombineAuthorizeAttributes()
+ {
+ // Arrange
+ var attributes = new AuthorizeAttribute[] {
+ new AuthorizeAttribute(),
+ new AuthorizeAttribute("1") { AuthenticationSchemes = "dupe" },
+ new AuthorizeAttribute("2") { AuthenticationSchemes = "dupe" },
+ new AuthorizeAttribute { Roles = "r1,r2", AuthenticationSchemes = "roles" },
+ };
+ var options = new AuthorizationOptions();
+ options.AddPolicy("1", policy => policy.RequireClaim("1"));
+ options.AddPolicy("2", policy => policy.RequireClaim("2"));
+
+ var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options));
+
+ // Act
+ var combined = await AuthorizationPolicy.CombineAsync(provider, attributes);
+
+ // Assert
+ Assert.Equal(2, combined.AuthenticationSchemes.Count());
+ Assert.Contains("dupe", combined.AuthenticationSchemes);
+ Assert.Contains("roles", combined.AuthenticationSchemes);
+ Assert.Equal(4, combined.Requirements.Count());
+ Assert.Contains(combined.Requirements, r => r is DenyAnonymousAuthorizationRequirement);
+ Assert.Equal(2, combined.Requirements.OfType<ClaimsAuthorizationRequirement>().Count());
+ Assert.Single(combined.Requirements.OfType<RolesAuthorizationRequirement>());
+ }
+
+ [Fact]
+ public async Task CanReplaceDefaultPolicy()
+ {
+ // Arrange
+ var attributes = new AuthorizeAttribute[] {
+ new AuthorizeAttribute(),
+ new AuthorizeAttribute("2") { AuthenticationSchemes = "dupe" }
+ };
+ var options = new AuthorizationOptions();
+ options.DefaultPolicy = new AuthorizationPolicyBuilder("default").RequireClaim("default").Build();
+ options.AddPolicy("2", policy => policy.RequireClaim("2"));
+
+ var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options));
+
+ // Act
+ var combined = await AuthorizationPolicy.CombineAsync(provider, attributes);
+
+ // Assert
+ Assert.Equal(2, combined.AuthenticationSchemes.Count());
+ Assert.Contains("dupe", combined.AuthenticationSchemes);
+ Assert.Contains("default", combined.AuthenticationSchemes);
+ Assert.Equal(2, combined.Requirements.Count());
+ Assert.DoesNotContain(combined.Requirements, r => r is DenyAnonymousAuthorizationRequirement);
+ Assert.Equal(2, combined.Requirements.OfType<ClaimsAuthorizationRequirement>().Count());
+ }
+
+ [Fact]
+ public async Task CombineMustTrimRoles()
+ {
+ // Arrange
+ var attributes = new AuthorizeAttribute[] {
+ new AuthorizeAttribute() { Roles = "r1 , r2" }
+ };
+ var options = new AuthorizationOptions();
+ var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options));
+
+ // Act
+ var combined = await AuthorizationPolicy.CombineAsync(provider, attributes);
+
+ // Assert
+ Assert.Contains(combined.Requirements, r => r is RolesAuthorizationRequirement);
+ var rolesAuthorizationRequirement = combined.Requirements.OfType<RolesAuthorizationRequirement>().First();
+ Assert.Equal(2, rolesAuthorizationRequirement.AllowedRoles.Count());
+ Assert.Contains(rolesAuthorizationRequirement.AllowedRoles, r => r.Equals("r1"));
+ Assert.Contains(rolesAuthorizationRequirement.AllowedRoles, r => r.Equals("r2"));
+ }
+
+ [Fact]
+ public async Task CombineMustTrimAuthenticationScheme()
+ {
+ // Arrange
+ var attributes = new AuthorizeAttribute[] {
+ new AuthorizeAttribute() { AuthenticationSchemes = "a1 , a2" }
+ };
+ var options = new AuthorizationOptions();
+
+ var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options));
+
+ // Act
+ var combined = await AuthorizationPolicy.CombineAsync(provider, attributes);
+
+ // Assert
+ Assert.Equal(2, combined.AuthenticationSchemes.Count());
+ Assert.Contains(combined.AuthenticationSchemes, a => a.Equals("a1"));
+ Assert.Contains(combined.AuthenticationSchemes, a => a.Equals("a2"));
+ }
+
+ [Fact]
+ public async Task CombineMustIgnoreEmptyAuthenticationScheme()
+ {
+ // Arrange
+ var attributes = new AuthorizeAttribute[] {
+ new AuthorizeAttribute() { AuthenticationSchemes = "a1 , , ,,, a2" }
+ };
+ var options = new AuthorizationOptions();
+
+ var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options));
+
+ // Act
+ var combined = await AuthorizationPolicy.CombineAsync(provider, attributes);
+
+ // Assert
+ Assert.Equal(2, combined.AuthenticationSchemes.Count());
+ Assert.Contains(combined.AuthenticationSchemes, a => a.Equals("a1"));
+ Assert.Contains(combined.AuthenticationSchemes, a => a.Equals("a2"));
+ }
+
+ [Fact]
+ public async Task CombineMustIgnoreEmptyRoles()
+ {
+ // Arrange
+ var attributes = new AuthorizeAttribute[] {
+ new AuthorizeAttribute() { Roles = "r1 , ,, , r2" }
+ };
+ var options = new AuthorizationOptions();
+ var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options));
+
+ // Act
+ var combined = await AuthorizationPolicy.CombineAsync(provider, attributes);
+
+ // Assert
+ Assert.Contains(combined.Requirements, r => r is RolesAuthorizationRequirement);
+ var rolesAuthorizationRequirement = combined.Requirements.OfType<RolesAuthorizationRequirement>().First();
+ Assert.Equal(2, rolesAuthorizationRequirement.AllowedRoles.Count());
+ Assert.Contains(rolesAuthorizationRequirement.AllowedRoles, r => r.Equals("r1"));
+ Assert.Contains(rolesAuthorizationRequirement.AllowedRoles, r => r.Equals("r2"));
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs
new file mode 100644
index 0000000000..ef17b94620
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs
@@ -0,0 +1,1168 @@
+// 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 System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authorization.Test
+{
+ public class DefaultAuthorizationServiceTests
+ {
+ private IAuthorizationService BuildAuthorizationService(Action<IServiceCollection> setupServices = null)
+ {
+ var services = new ServiceCollection();
+ services.AddAuthorization();
+ services.AddLogging();
+ services.AddOptions();
+ setupServices?.Invoke(services);
+ return services.BuildServiceProvider().GetRequiredService<IAuthorizationService>();
+ }
+
+ [Fact]
+ public async Task AuthorizeCombineThrowsOnUnknownPolicy()
+ {
+ var provider = new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions()));
+
+ // Act
+ await Assert.ThrowsAsync<InvalidOperationException>(() => AuthorizationPolicy.CombineAsync(provider, new AuthorizeAttribute[] {
+ new AuthorizeAttribute { Policy = "Wut" }
+ }));
+ }
+
+ [Fact]
+ public async Task Authorize_ShouldAllowIfClaimIsPresent()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage"));
+ });
+ });
+ var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }));
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task Authorize_ShouldAllowIfClaimIsPresentWithSpecifiedAuthType()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => {
+ policy.AddAuthenticationSchemes("Basic");
+ policy.RequireClaim("Permission", "CanViewPage");
+ });
+ });
+ });
+ var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic"));
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task Authorize_ShouldAllowIfClaimIsAmongValues()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything"));
+ });
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("Permission", "CanViewPage"),
+ new Claim("Permission", "CanViewAnything")
+ },
+ "Basic")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task Authorize_ShouldInvokeAllHandlersByDefault()
+ {
+ // Arrange
+ var handler1 = new FailHandler();
+ var handler2 = new FailHandler();
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddSingleton<IAuthorizationHandler>(handler1);
+ services.AddSingleton<IAuthorizationHandler>(handler2);
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement()));
+ });
+ });
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(new ClaimsPrincipal(), "Custom");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ Assert.True(allowed.Failure.FailCalled);
+ Assert.True(handler1.Invoked);
+ Assert.True(handler2.Invoked);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task Authorize_ShouldInvokeAllHandlersDependingOnSetting(bool invokeAllHandlers)
+ {
+ // Arrange
+ var handler1 = new FailHandler();
+ var handler2 = new FailHandler();
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddSingleton<IAuthorizationHandler>(handler1);
+ services.AddSingleton<IAuthorizationHandler>(handler2);
+ services.AddAuthorization(options =>
+ {
+ options.InvokeHandlersAfterFailure = invokeAllHandlers;
+ options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement()));
+ });
+ });
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(new ClaimsPrincipal(), "Custom");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ Assert.True(handler1.Invoked);
+ Assert.Equal(invokeAllHandlers, handler2.Invoked);
+ }
+
+ private class FailHandler : IAuthorizationHandler
+ {
+ public bool Invoked { get; set; }
+
+ public Task HandleAsync(AuthorizationHandlerContext context)
+ {
+ Invoked = true;
+ context.Fail();
+ return Task.FromResult(0);
+ }
+ }
+
+ [Fact]
+ public async Task Authorize_ShouldFailWhenAllRequirementsNotHandled()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything"));
+ });
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("SomethingElse", "CanViewPage"),
+ },
+ "Basic")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ Assert.IsType<ClaimsAuthorizationRequirement>(allowed.Failure.FailedRequirements.First());
+ }
+
+ [Fact]
+ public async Task Authorize_ShouldNotAllowIfClaimTypeIsNotPresent()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything"));
+ });
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("SomethingElse", "CanViewPage"),
+ },
+ "Basic")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task Authorize_ShouldNotAllowIfClaimValueIsNotPresent()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage"));
+ });
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("Permission", "CanViewComment"),
+ },
+ "Basic")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task Authorize_ShouldNotAllowIfNoClaims()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage"));
+ });
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[0],
+ "Basic")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task Authorize_ShouldNotAllowIfUserIsNull()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage"));
+ });
+ });
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(null, null, "Basic");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task Authorize_ShouldNotAllowIfNotCorrectAuthType()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage"));
+ });
+ });
+ var user = new ClaimsPrincipal(new ClaimsIdentity());
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task Authorize_ShouldAllowWithNoAuthType()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage"));
+ });
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("Permission", "CanViewPage"),
+ },
+ "Basic")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task Authorize_ThrowsWithUnknownPolicy()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService();
+
+ // Act
+ // Assert
+ var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => authorizationService.AuthorizeAsync(new ClaimsPrincipal(), "whatever", "BogusPolicy"));
+ Assert.Equal("No policy found: BogusPolicy.", exception.Message);
+ }
+
+ [Fact]
+ public async Task Authorize_CustomRolePolicy()
+ {
+ // Arrange
+ var policy = new AuthorizationPolicyBuilder().RequireRole("Administrator")
+ .RequireClaim(ClaimTypes.Role, "User");
+ var authorizationService = BuildAuthorizationService();
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim(ClaimTypes.Role, "User"),
+ new Claim(ClaimTypes.Role, "Administrator")
+ },
+ "Basic")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, policy.Build());
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task Authorize_HasAnyClaimOfTypePolicy()
+ {
+ // Arrange
+ var policy = new AuthorizationPolicyBuilder().RequireClaim(ClaimTypes.Role);
+ var authorizationService = BuildAuthorizationService();
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim(ClaimTypes.Role, "none"),
+ },
+ "Basic")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, policy.Build());
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task Authorize_PolicyCanAuthenticationSchemeWithNameClaim()
+ {
+ // Arrange
+ var policy = new AuthorizationPolicyBuilder("AuthType").RequireClaim(ClaimTypes.Name);
+ var authorizationService = BuildAuthorizationService();
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, "Name") }, "AuthType")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, policy.Build());
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task RolePolicyCanRequireSingleRole()
+ {
+ // Arrange
+ var policy = new AuthorizationPolicyBuilder("AuthType").RequireRole("Admin");
+ var authorizationService = BuildAuthorizationService();
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Role, "Admin") }, "AuthType")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, null, policy.Build());
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task RolePolicyCanRequireOneOfManyRoles()
+ {
+ // Arrange
+ var policy = new AuthorizationPolicyBuilder("AuthType").RequireRole("Admin", "Users");
+ var authorizationService = BuildAuthorizationService();
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Role, "Users") }, "AuthType"));
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, policy.Build());
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task RolePolicyCanBlockWrongRole()
+ {
+ // Arrange
+ var policy = new AuthorizationPolicyBuilder().RequireClaim("Permission", "CanViewPage");
+ var authorizationService = BuildAuthorizationService();
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim(ClaimTypes.Role, "Nope"),
+ },
+ "AuthType")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, policy.Build());
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task RolePolicyCanBlockNoRole()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => policy.RequireRole("Admin", "Users"));
+ });
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ },
+ "AuthType")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
+ [Fact]
+ public void PolicyThrowsWithNoRequirements()
+ {
+ Assert.Throws<InvalidOperationException>(() => BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => { });
+ });
+ }));
+ }
+
+ [Fact]
+ public async Task RequireUserNameFailsForWrongUserName()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Hao", policy => policy.RequireUserName("Hao"));
+ });
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim(ClaimTypes.Name, "Tek"),
+ },
+ "AuthType")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Hao");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task CanRequireUserName()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Hao", policy => policy.RequireUserName("Hao"));
+ });
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim(ClaimTypes.Name, "Hao"),
+ },
+ "AuthType")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Hao");
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task CanRequireUserNameWithDiffClaimType()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Hao", policy => policy.RequireUserName("Hao"));
+ });
+ });
+ var identity = new ClaimsIdentity("AuthType", "Name", "Role");
+ identity.AddClaim(new Claim("Name", "Hao"));
+ var user = new ClaimsPrincipal(identity);
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Hao");
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task CanRequireRoleWithDiffClaimType()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Hao", policy => policy.RequireRole("Hao"));
+ });
+ });
+ var identity = new ClaimsIdentity("AuthType", "Name", "Role");
+ identity.AddClaim(new Claim("Role", "Hao"));
+ var user = new ClaimsPrincipal(identity);
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Hao");
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task CanApproveAnyAuthenticatedUser()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Any", policy => policy.RequireAuthenticatedUser());
+ });
+ });
+ var user = new ClaimsPrincipal(new ClaimsIdentity());
+ user.AddIdentity(new ClaimsIdentity(
+ new Claim[] {
+ new Claim(ClaimTypes.Name, "Name"),
+ },
+ "AuthType"));
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, null, "Any");
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task CanBlockNonAuthenticatedUser()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Any", policy => policy.RequireAuthenticatedUser());
+ });
+ });
+ var user = new ClaimsPrincipal(new ClaimsIdentity());
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, null, "Any");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
+ public class CustomRequirement : IAuthorizationRequirement { }
+ public class CustomHandler : AuthorizationHandler<CustomRequirement>
+ {
+ public bool Invoked { get; set; }
+
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomRequirement requirement)
+ {
+ Invoked = true;
+ context.Succeed(requirement);
+ return Task.FromResult(0);
+ }
+ }
+
+ [Fact]
+ public async Task CustomReqWithNoHandlerFails()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement()));
+ });
+ });
+ var user = new ClaimsPrincipal();
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, null, "Custom");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task CustomReqWithHandlerSucceeds()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddTransient<IAuthorizationHandler, CustomHandler>();
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement()));
+ });
+ });
+ var user = new ClaimsPrincipal();
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, null, "Custom");
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ public class PassThroughRequirement : AuthorizationHandler<PassThroughRequirement>, IAuthorizationRequirement
+ {
+ public PassThroughRequirement(bool succeed)
+ {
+ Succeed = succeed;
+ }
+
+ public bool Succeed { get; set; }
+
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PassThroughRequirement requirement)
+ {
+ if (Succeed) {
+ context.Succeed(requirement);
+ }
+ return Task.FromResult(0);
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task PassThroughRequirementWillSucceedWithoutCustomHandler(bool shouldSucceed)
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Passthrough", policy => policy.Requirements.Add(new PassThroughRequirement(shouldSucceed)));
+ });
+ });
+ var user = new ClaimsPrincipal();
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, null, "Passthrough");
+
+ // Assert
+ Assert.Equal(shouldSucceed, allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task CanCombinePolicies()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ var basePolicy = new AuthorizationPolicyBuilder().RequireClaim("Base", "Value").Build();
+ options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequireClaim("Claim", "Exists"));
+ });
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("Base", "Value"),
+ new Claim("Claim", "Exists")
+ },
+ "AuthType")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined");
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task CombinePoliciesWillFailIfBasePolicyFails()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ var basePolicy = new AuthorizationPolicyBuilder().RequireClaim("Base", "Value").Build();
+ options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequireClaim("Claim", "Exists"));
+ });
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("Claim", "Exists")
+ },
+ "AuthType")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task CombinedPoliciesWillFailIfExtraRequirementFails()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ var basePolicy = new AuthorizationPolicyBuilder().RequireClaim("Base", "Value").Build();
+ options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequireClaim("Claim", "Exists"));
+ });
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("Base", "Value"),
+ },
+ "AuthType")
+ );
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
+ public class ExpenseReport { }
+
+ public static class Operations
+ {
+ public static OperationAuthorizationRequirement Edit = new OperationAuthorizationRequirement { Name = "Edit" };
+ public static OperationAuthorizationRequirement Create = new OperationAuthorizationRequirement { Name = "Create" };
+ public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = "Delete" };
+ }
+
+ public class ExpenseReportAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, ExpenseReport>
+ {
+ public ExpenseReportAuthorizationHandler(IEnumerable<OperationAuthorizationRequirement> authorized)
+ {
+ _allowed = authorized;
+ }
+
+ private IEnumerable<OperationAuthorizationRequirement> _allowed;
+
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, ExpenseReport resource)
+ {
+ if (_allowed.Contains(requirement))
+ {
+ context.Succeed(requirement);
+ }
+ return Task.FromResult(0);
+ }
+ }
+
+ public class SuperUserHandler : AuthorizationHandler<OperationAuthorizationRequirement>
+ {
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement)
+ {
+ if (context.User.HasClaim("SuperUser", "yes"))
+ {
+ context.Succeed(requirement);
+ }
+ return Task.FromResult(0);
+ }
+ }
+
+ [Fact]
+ public async Task CanAuthorizeAllSuperuserOperations()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddSingleton<IAuthorizationHandler>(new ExpenseReportAuthorizationHandler(new OperationAuthorizationRequirement[] { Operations.Edit }));
+ services.AddTransient<IAuthorizationHandler, SuperUserHandler>();
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("SuperUser", "yes"),
+ },
+ "AuthType")
+ );
+
+ // Act
+ // Assert
+ Assert.True((await authorizationService.AuthorizeAsync(user, null, Operations.Edit)).Succeeded);
+ Assert.True((await authorizationService.AuthorizeAsync(user, null, Operations.Delete)).Succeeded);
+ Assert.True((await authorizationService.AuthorizeAsync(user, null, Operations.Create)).Succeeded);
+ }
+
+ public class NotCalledHandler : AuthorizationHandler<OperationAuthorizationRequirement, string>
+ {
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, string resource)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class EvenHandler : AuthorizationHandler<OperationAuthorizationRequirement, int>
+ {
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, int id)
+ {
+ if (id % 2 == 0)
+ {
+ context.Succeed(requirement);
+ }
+ return Task.FromResult(0);
+ }
+ }
+
+ [Fact]
+ public async Task CanUseValueTypeResource()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddTransient<IAuthorizationHandler, EvenHandler>();
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ },
+ "AuthType")
+ );
+
+ // Act
+ // Assert
+ Assert.False((await authorizationService.AuthorizeAsync(user, 1, Operations.Edit)).Succeeded);
+ Assert.True((await authorizationService.AuthorizeAsync(user, 2, Operations.Edit)).Succeeded);
+ }
+
+
+ [Fact]
+ public async Task DoesNotCallHandlerWithWrongResourceType()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddTransient<IAuthorizationHandler, NotCalledHandler>();
+ });
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("SuperUser", "yes")
+ },
+ "AuthType")
+ );
+
+ // Act
+ // Assert
+ Assert.False((await authorizationService.AuthorizeAsync(user, 1, Operations.Edit)).Succeeded);
+ }
+
+ [Fact]
+ public async Task CanAuthorizeOnlyAllowedOperations()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddSingleton<IAuthorizationHandler>(new ExpenseReportAuthorizationHandler(new OperationAuthorizationRequirement[] { Operations.Edit }));
+ });
+ var user = new ClaimsPrincipal();
+
+ // Act
+ // Assert
+ Assert.True((await authorizationService.AuthorizeAsync(user, new ExpenseReport(), Operations.Edit)).Succeeded);
+ Assert.False((await authorizationService.AuthorizeAsync(user, new ExpenseReport(), Operations.Delete)).Succeeded);
+ Assert.False((await authorizationService.AuthorizeAsync(user, new ExpenseReport(), Operations.Create)).Succeeded);
+ }
+
+ [Fact]
+ public async Task AuthorizeHandlerNotCalledWithNullResource()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddSingleton<IAuthorizationHandler>(new ExpenseReportAuthorizationHandler(new OperationAuthorizationRequirement[] { Operations.Edit }));
+ });
+ var user = new ClaimsPrincipal();
+
+ // Act
+ // Assert
+ Assert.False((await authorizationService.AuthorizeAsync(user, null, Operations.Edit)).Succeeded);
+ }
+
+ [Fact]
+ public async Task CanAuthorizeWithAssertionRequirement()
+ {
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => policy.RequireAssertion(context => true));
+ });
+ });
+ var user = new ClaimsPrincipal();
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ [Fact]
+ public async Task CanAuthorizeWithAsyncAssertionRequirement()
+ {
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => policy.RequireAssertion(context => Task.FromResult(true)));
+ });
+ });
+ var user = new ClaimsPrincipal();
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
+ public class StaticPolicyProvider : IAuthorizationPolicyProvider
+ {
+ public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
+ {
+ return Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
+ }
+
+ public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
+ {
+ return Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
+ }
+ }
+
+ [Fact]
+ public async Task CanReplaceDefaultPolicyProvider()
+ {
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ // This will ignore the policy options
+ services.AddSingleton<IAuthorizationPolicyProvider, StaticPolicyProvider>();
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("Basic", policy => policy.RequireAssertion(context => true));
+ });
+ });
+ var user = new ClaimsPrincipal();
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
+ public class DynamicPolicyProvider : IAuthorizationPolicyProvider
+ {
+ public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
+ {
+ return Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
+ }
+
+ public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
+ {
+ return Task.FromResult(new AuthorizationPolicyBuilder().RequireClaim(policyName).Build());
+ }
+ }
+
+ [Fact]
+ public async Task CanUseDynamicPolicyProvider()
+ {
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ // This will ignore the policy options
+ services.AddSingleton<IAuthorizationPolicyProvider, DynamicPolicyProvider>();
+ services.AddAuthorization(options => { });
+ });
+ var id = new ClaimsIdentity();
+ id.AddClaim(new Claim("1", "1"));
+ id.AddClaim(new Claim("2", "2"));
+ var user = new ClaimsPrincipal(id);
+
+ // Act
+ // Assert
+ Assert.False((await authorizationService.AuthorizeAsync(user, "0")).Succeeded);
+ Assert.True((await authorizationService.AuthorizeAsync(user, "1")).Succeeded);
+ Assert.True((await authorizationService.AuthorizeAsync(user, "2")).Succeeded);
+ Assert.False((await authorizationService.AuthorizeAsync(user, "3")).Succeeded);
+ }
+
+ public class SuccessEvaluator : IAuthorizationEvaluator
+ {
+ public AuthorizationResult Evaluate(AuthorizationHandlerContext context) => AuthorizationResult.Success();
+ }
+
+ [Fact]
+ public async Task CanUseCustomEvaluatorThatOverridesRequirement()
+ {
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddSingleton<IAuthorizationEvaluator, SuccessEvaluator>();
+ services.AddAuthorization(options => options.AddPolicy("Fail", p => p.RequireAssertion(c => false)));
+ });
+ var result = await authorizationService.AuthorizeAsync(null, "Fail");
+ Assert.True(result.Succeeded);
+ }
+
+
+ public class BadContextMaker : IAuthorizationHandlerContextFactory
+ {
+ public AuthorizationHandlerContext CreateContext(IEnumerable<IAuthorizationRequirement> requirements, ClaimsPrincipal user, object resource)
+ {
+ return new BadContext();
+ }
+ }
+
+ public class BadContext : AuthorizationHandlerContext
+ {
+ public BadContext() : base(new List<IAuthorizationRequirement>(), null, null) { }
+
+ public override bool HasFailed
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public override bool HasSucceeded
+ {
+ get
+ {
+ return false;
+ }
+ }
+ }
+
+ [Fact]
+ public async Task CanUseCustomContextThatAlwaysFails()
+ {
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddSingleton<IAuthorizationHandlerContextFactory, BadContextMaker>();
+ services.AddAuthorization(options => options.AddPolicy("Success", p => p.RequireAssertion(c => true)));
+ });
+ Assert.False((await authorizationService.AuthorizeAsync(null, "Success")).Succeeded);
+ }
+
+ public class SadHandlerProvider : IAuthorizationHandlerProvider
+ {
+ public Task<IEnumerable<IAuthorizationHandler>> GetHandlersAsync(AuthorizationHandlerContext context)
+ {
+ return Task.FromResult<IEnumerable<IAuthorizationHandler>>(new IAuthorizationHandler[1] { new FailHandler() });
+ }
+ }
+
+ [Fact]
+ public async Task CanUseCustomHandlerProvider()
+ {
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddSingleton<IAuthorizationHandlerProvider, SadHandlerProvider>();
+ services.AddAuthorization(options => options.AddPolicy("Success", p => p.RequireAssertion(c => true)));
+ });
+ Assert.False((await authorizationService.AuthorizeAsync(null, "Success")).Succeeded);
+ }
+
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj
new file mode 100644
index 0000000000..d4379c3aab
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authorization\Microsoft.AspNetCore.Authorization.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authorization.Policy\Microsoft.AspNetCore.Authorization.Policy.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/test/Microsoft.AspNetCore.Authorization.Test/PolicyEvaluatorTests.cs b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/PolicyEvaluatorTests.cs
new file mode 100644
index 0000000000..2384e6db5f
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.Authorization.Test/PolicyEvaluatorTests.cs
@@ -0,0 +1,209 @@
+// 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 System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Authorization.Policy.Test
+{
+ public class PolicyEvaluatorTests
+ {
+ [Fact]
+ public async Task AuthenticateFailsIfNoPrincipalReturned()
+ {
+ // Arrange
+ var evaluator = BuildEvaluator();
+ var context = new DefaultHttpContext();
+ var services = new ServiceCollection().AddSingleton<IAuthenticationService, SadAuthentication>();
+ context.RequestServices = services.BuildServiceProvider();
+ var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build();
+
+ // Act
+ var result = await evaluator.AuthenticateAsync(policy, context);
+
+ // Assert
+ Assert.False(result.Succeeded);
+ }
+
+ [Fact]
+ public async Task AuthenticateMergeSchemes()
+ {
+ // Arrange
+ var evaluator = BuildEvaluator();
+ var context = new DefaultHttpContext();
+ var services = new ServiceCollection().AddSingleton<IAuthenticationService, EchoAuthentication>();
+ context.RequestServices = services.BuildServiceProvider();
+ var policy = new AuthorizationPolicyBuilder().AddAuthenticationSchemes("A","B","C").RequireAssertion(_ => true).Build();
+
+ // Act
+ var result = await evaluator.AuthenticateAsync(policy, context);
+
+ // Assert
+ Assert.True(result.Succeeded);
+ Assert.Equal(3, result.Principal.Identities.Count());
+ }
+
+
+ [Fact]
+ public async Task AuthorizeSucceedsEvenIfAuthenticationFails()
+ {
+ // Arrange
+ var evaluator = BuildEvaluator();
+ var context = new DefaultHttpContext();
+ var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build();
+
+ // Act
+ var result = await evaluator.AuthorizeAsync(policy, AuthenticateResult.Fail("Nooo"), context, resource: null);
+
+ // Assert
+ Assert.True(result.Succeeded);
+ Assert.False(result.Challenged);
+ Assert.False(result.Forbidden);
+ }
+
+ [Fact]
+ public async Task AuthorizeSucceedsOnlyIfResourceSpecified()
+ {
+ // Arrange
+ var evaluator = BuildEvaluator();
+ var context = new DefaultHttpContext();
+ var policy = new AuthorizationPolicyBuilder().RequireAssertion(c => c.Resource != null).Build();
+ var success = AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "whatever"));
+
+ // Act
+ var result = await evaluator.AuthorizeAsync(policy, success, context, resource: null);
+ var result2 = await evaluator.AuthorizeAsync(policy, success, context, resource: new object());
+
+ // Assert
+ Assert.False(result.Succeeded);
+ Assert.True(result2.Succeeded);
+ }
+
+ [Fact]
+ public async Task AuthorizeChallengesIfAuthenticationFails()
+ {
+ // Arrange
+ var evaluator = BuildEvaluator();
+ var context = new DefaultHttpContext();
+ var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => false).Build();
+
+ // Act
+ var result = await evaluator.AuthorizeAsync(policy, AuthenticateResult.Fail("Nooo"), context, resource: null);
+
+ // Assert
+ Assert.False(result.Succeeded);
+ Assert.True(result.Challenged);
+ Assert.False(result.Forbidden);
+ }
+
+ [Fact]
+ public async Task AuthorizeForbidsIfAuthenticationSuceeds()
+ {
+ // Arrange
+ var evaluator = BuildEvaluator();
+ var context = new DefaultHttpContext();
+ var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => false).Build();
+
+ // Act
+ var result = await evaluator.AuthorizeAsync(policy, AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "scheme")), context, resource: null);
+
+ // Assert
+ Assert.False(result.Succeeded);
+ Assert.False(result.Challenged);
+ Assert.True(result.Forbidden);
+ }
+
+ private IPolicyEvaluator BuildEvaluator(Action<IServiceCollection> setupServices = null)
+ {
+ var services = new ServiceCollection()
+ .AddAuthorization()
+ .AddAuthorizationPolicyEvaluator()
+ .AddLogging()
+ .AddOptions();
+ setupServices?.Invoke(services);
+ return services.BuildServiceProvider().GetRequiredService<IPolicyEvaluator>();
+ }
+
+ public class HappyAuthorization : IAuthorizationService
+ {
+ public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
+ => Task.FromResult(AuthorizationResult.Success());
+
+ public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
+ => Task.FromResult(AuthorizationResult.Success());
+ }
+
+ public class SadAuthorization : IAuthorizationService
+ {
+ public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
+ => Task.FromResult(AuthorizationResult.Failed());
+
+ public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
+ => Task.FromResult(AuthorizationResult.Failed());
+ }
+
+ public class SadAuthentication : IAuthenticationService
+ {
+ public Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
+ {
+ return Task.FromResult(AuthenticateResult.Fail("Sad."));
+ }
+
+ public Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+
+ public class EchoAuthentication : IAuthenticationService
+ {
+ public Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
+ {
+ return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(scheme)), scheme)));
+ }
+
+ public Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+
+ }
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/CookieChunkingTests.cs b/src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/CookieChunkingTests.cs
new file mode 100644
index 0000000000..e645745b35
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/CookieChunkingTests.cs
@@ -0,0 +1,131 @@
+// 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;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Internal
+{
+ public class CookieChunkingTests
+ {
+ [Fact]
+ public void AppendLargeCookie_Appended()
+ {
+ HttpContext context = new DefaultHttpContext();
+
+ string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ new ChunkingCookieManager() { ChunkSize = null }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
+ var values = context.Response.Headers["Set-Cookie"];
+ Assert.Single(values);
+ Assert.Equal("TestCookie=" + testString + "; path=/; samesite=lax", values[0]);
+ }
+
+ [Fact]
+ public void AppendLargeCookieWithLimit_Chunked()
+ {
+ HttpContext context = new DefaultHttpContext();
+
+ string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ new ChunkingCookieManager() { ChunkSize = 44 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
+ var values = context.Response.Headers["Set-Cookie"];
+ Assert.Equal(9, values.Count);
+ Assert.Equal<string[]>(new[]
+ {
+ "TestCookie=chunks-8; path=/; samesite=lax",
+ "TestCookieC1=abcdefgh; path=/; samesite=lax",
+ "TestCookieC2=ijklmnop; path=/; samesite=lax",
+ "TestCookieC3=qrstuvwx; path=/; samesite=lax",
+ "TestCookieC4=yz012345; path=/; samesite=lax",
+ "TestCookieC5=6789ABCD; path=/; samesite=lax",
+ "TestCookieC6=EFGHIJKL; path=/; samesite=lax",
+ "TestCookieC7=MNOPQRST; path=/; samesite=lax",
+ "TestCookieC8=UVWXYZ; path=/; samesite=lax",
+ }, values);
+ }
+
+ [Fact]
+ public void GetLargeChunkedCookie_Reassembled()
+ {
+ HttpContext context = new DefaultHttpContext();
+ context.Request.Headers["Cookie"] = new[]
+ {
+ "TestCookie=chunks-7",
+ "TestCookieC1=abcdefghi",
+ "TestCookieC2=jklmnopqr",
+ "TestCookieC3=stuvwxyz0",
+ "TestCookieC4=123456789",
+ "TestCookieC5=ABCDEFGHI",
+ "TestCookieC6=JKLMNOPQR",
+ "TestCookieC7=STUVWXYZ"
+ };
+
+ string result = new ChunkingCookieManager().GetRequestCookie(context, "TestCookie");
+ string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ Assert.Equal(testString, result);
+ }
+
+ [Fact]
+ public void GetLargeChunkedCookieWithMissingChunk_ThrowingEnabled_Throws()
+ {
+ HttpContext context = new DefaultHttpContext();
+ context.Request.Headers["Cookie"] = new[]
+ {
+ "TestCookie=chunks-7",
+ "TestCookieC1=abcdefghi",
+ // Missing chunk "TestCookieC2=jklmnopqr",
+ "TestCookieC3=stuvwxyz0",
+ "TestCookieC4=123456789",
+ "TestCookieC5=ABCDEFGHI",
+ "TestCookieC6=JKLMNOPQR",
+ "TestCookieC7=STUVWXYZ"
+ };
+
+ Assert.Throws<FormatException>(() => new ChunkingCookieManager() { ThrowForPartialCookies = true }
+ .GetRequestCookie(context, "TestCookie"));
+ }
+
+ [Fact]
+ public void GetLargeChunkedCookieWithMissingChunk_ThrowingDisabled_NotReassembled()
+ {
+ HttpContext context = new DefaultHttpContext();
+ context.Request.Headers["Cookie"] = new[]
+ {
+ "TestCookie=chunks-7",
+ "TestCookieC1=abcdefghi",
+ // Missing chunk "TestCookieC2=jklmnopqr",
+ "TestCookieC3=stuvwxyz0",
+ "TestCookieC4=123456789",
+ "TestCookieC5=ABCDEFGHI",
+ "TestCookieC6=JKLMNOPQR",
+ "TestCookieC7=STUVWXYZ"
+ };
+
+ string result = new ChunkingCookieManager() { ThrowForPartialCookies = false }.GetRequestCookie(context, "TestCookie");
+ string testString = "chunks-7";
+ Assert.Equal(testString, result);
+ }
+
+ [Fact]
+ public void DeleteChunkedCookieWithOptions_AllDeleted()
+ {
+ HttpContext context = new DefaultHttpContext();
+ context.Request.Headers.Append("Cookie", "TestCookie=chunks-7");
+
+ new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com" });
+ var cookies = context.Response.Headers["Set-Cookie"];
+ Assert.Equal(8, cookies.Count);
+ Assert.Equal(new[]
+ {
+ "TestCookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
+ "TestCookieC1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
+ "TestCookieC2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
+ "TestCookieC3=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
+ "TestCookieC4=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
+ "TestCookieC5=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
+ "TestCookieC6=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
+ "TestCookieC7=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
+ }, cookies);
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj b/src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj
new file mode 100644
index 0000000000..20cd400ce7
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\..\shared\Microsoft.AspNetCore.ChunkingCookieManager.Sources\**\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs
new file mode 100644
index 0000000000..fffb8cc883
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs
@@ -0,0 +1,660 @@
+// 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 System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.CookiePolicy.Test
+{
+ public class CookieConsentTests
+ {
+ [Fact]
+ public async Task ConsentChecksOffByDefault()
+ {
+ var httpContext = await RunTestAsync(options => { }, requestContext => { }, context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.False(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+ Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task ConsentEnabledForTemplateScenario()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext => { }, context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task NonEssentialCookiesWithOptionsExcluded()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext => { }, context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = false });
+ return Task.CompletedTask;
+ });
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task NonEssentialCookiesCanBeAllowedViaOnAppendCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ options.OnAppendCookie = context =>
+ {
+ Assert.True(context.IsConsentNeeded);
+ Assert.False(context.HasConsent);
+ Assert.False(context.IssueCookie);
+ context.IssueCookie = true;
+ };
+ },
+ requestContext => { }, context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = false });
+ return Task.CompletedTask;
+ });
+ Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task NeedsConsentDoesNotPreventEssentialCookies()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext => { }, context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = true });
+ return Task.CompletedTask;
+ });
+ Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task EssentialCookiesCanBeExcludedByOnAppendCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ options.OnAppendCookie = context =>
+ {
+ Assert.True(context.IsConsentNeeded);
+ Assert.True(context.HasConsent);
+ Assert.True(context.IssueCookie);
+ context.IssueCookie = false;
+ };
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
+ },
+ context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = true });
+ return Task.CompletedTask;
+ });
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task HasConsentReadsRequestCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
+ },
+ context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+ Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task HasConsentIgnoresInvalidRequestCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=IAmATeapot";
+ },
+ context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task GrantConsentSetsCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext => { },
+ context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ feature.GrantConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+
+ var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ Assert.Equal(2, cookies.Count);
+ var consentCookie = cookies[0];
+ Assert.Equal(".AspNet.Consent", consentCookie.Name);
+ Assert.Equal("yes", consentCookie.Value);
+ Assert.True(consentCookie.Expires.HasValue);
+ Assert.True(consentCookie.Expires.Value > DateTimeOffset.Now + TimeSpan.FromDays(364));
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite);
+ Assert.NotNull(consentCookie.Expires);
+ var testCookie = cookies[1];
+ Assert.Equal("Test", testCookie.Name);
+ Assert.Equal("Value", testCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite);
+ Assert.Null(testCookie.Expires);
+ }
+
+ [Fact]
+ public async Task GrantConsentAppliesPolicyToConsentCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ options.MinimumSameSitePolicy = Http.SameSiteMode.Strict;
+ options.OnAppendCookie = context =>
+ {
+ Assert.Equal(".AspNet.Consent", context.CookieName);
+ Assert.Equal("yes", context.CookieValue);
+ Assert.Equal(Http.SameSiteMode.Strict, context.CookieOptions.SameSite);
+ context.CookieName += "1";
+ context.CookieValue += "1";
+ };
+ },
+ requestContext => { },
+ context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ feature.GrantConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+
+ return Task.CompletedTask;
+ });
+
+ var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ Assert.Equal(1, cookies.Count);
+ var consentCookie = cookies[0];
+ Assert.Equal(".AspNet.Consent1", consentCookie.Name);
+ Assert.Equal("yes1", consentCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite);
+ Assert.NotNull(consentCookie.Expires);
+ }
+
+ [Fact]
+ public async Task GrantConsentWhenAlreadyHasItDoesNotSetCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
+ },
+ context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+
+ feature.GrantConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+
+ Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task GrantConsentAfterResponseStartsSetsHasConsentButDoesNotSetCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext => { },
+ async context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ await context.Response.WriteAsync("Started.");
+
+ feature.GrantConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+
+ Assert.Throws<InvalidOperationException>(() => context.Response.Cookies.Append("Test", "Value"));
+
+ await context.Response.WriteAsync("Granted.");
+ });
+
+ var reader = new StreamReader(httpContext.Response.Body);
+ Assert.Equal("Started.Granted.", await reader.ReadToEndAsync());
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task WithdrawConsentWhenNotHasConsentNoOps()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext => { },
+ context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ feature.WithdrawConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task WithdrawConsentDeletesCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
+ },
+ context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value1");
+
+ feature.WithdrawConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ context.Response.Cookies.Append("Test", "Value2");
+ return Task.CompletedTask;
+ });
+
+ var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ Assert.Equal(2, cookies.Count);
+ var testCookie = cookies[0];
+ Assert.Equal("Test", testCookie.Name);
+ Assert.Equal("Value1", testCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite);
+ Assert.Null(testCookie.Expires);
+ var consentCookie = cookies[1];
+ Assert.Equal(".AspNet.Consent", consentCookie.Name);
+ Assert.Equal("", consentCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite);
+ Assert.NotNull(consentCookie.Expires);
+ }
+
+ [Fact]
+ public async Task WithdrawConsentAppliesPolicyToDeleteCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ options.MinimumSameSitePolicy = Http.SameSiteMode.Strict;
+ options.OnDeleteCookie = context =>
+ {
+ Assert.Equal(".AspNet.Consent", context.CookieName);
+ context.CookieName += "1";
+ };
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
+ },
+ context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+
+ feature.WithdrawConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ return Task.CompletedTask;
+ });
+
+ var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ Assert.Equal(1, cookies.Count);
+ var consentCookie = cookies[0];
+ Assert.Equal(".AspNet.Consent1", consentCookie.Name);
+ Assert.Equal("", consentCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite);
+ Assert.NotNull(consentCookie.Expires);
+ }
+
+ [Fact]
+ public async Task WithdrawConsentAfterResponseHasStartedDoesNotDeleteCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
+ },
+ async context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value1");
+
+ await context.Response.WriteAsync("Started.");
+
+ feature.WithdrawConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ // Doesn't throw the normal InvalidOperationException because the cookie is never written
+ context.Response.Cookies.Append("Test", "Value2");
+
+ await context.Response.WriteAsync("Withdrawn.");
+ });
+
+ var reader = new StreamReader(httpContext.Response.Body);
+ Assert.Equal("Started.Withdrawn.", await reader.ReadToEndAsync());
+ Assert.Equal("Test=Value1; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task DeleteCookieDoesNotRequireConsent()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext => { },
+ context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+ context.Response.Cookies.Delete("Test");
+ return Task.CompletedTask;
+ });
+
+ var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ Assert.Equal(1, cookies.Count);
+ var testCookie = cookies[0];
+ Assert.Equal("Test", testCookie.Name);
+ Assert.Equal("", testCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite);
+ Assert.NotNull(testCookie.Expires);
+ }
+
+ [Fact]
+ public async Task OnDeleteCookieCanSuppressCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ options.OnDeleteCookie = context =>
+ {
+ Assert.True(context.IsConsentNeeded);
+ Assert.False(context.HasConsent);
+ Assert.True(context.IssueCookie);
+ context.IssueCookie = false;
+ };
+ },
+ requestContext => { },
+ context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+ context.Response.Cookies.Delete("Test");
+ return Task.CompletedTask;
+ });
+
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task CreateConsentCookieMatchesGrantConsentCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext => { },
+ context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ feature.GrantConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+
+ var cookie = feature.CreateConsentCookie();
+ context.Response.Headers["ManualCookie"] = cookie;
+
+ return Task.CompletedTask;
+ });
+
+ var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ Assert.Equal(1, cookies.Count);
+ var consentCookie = cookies[0];
+ Assert.Equal(".AspNet.Consent", consentCookie.Name);
+ Assert.Equal("yes", consentCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite);
+ Assert.NotNull(consentCookie.Expires);
+
+ cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers["ManualCookie"]);
+ Assert.Equal(1, cookies.Count);
+ var manualCookie = cookies[0];
+ Assert.Equal(consentCookie.Name, manualCookie.Name);
+ Assert.Equal(consentCookie.Value, manualCookie.Value);
+ Assert.Equal(consentCookie.SameSite, manualCookie.SameSite);
+ Assert.NotNull(manualCookie.Expires); // Expires may not exactly match to the second.
+ }
+
+ [Fact]
+ public async Task CreateConsentCookieAppliesPolicy()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ options.MinimumSameSitePolicy = Http.SameSiteMode.Strict;
+ options.OnAppendCookie = context =>
+ {
+ Assert.Equal(".AspNet.Consent", context.CookieName);
+ Assert.Equal("yes", context.CookieValue);
+ Assert.Equal(Http.SameSiteMode.Strict, context.CookieOptions.SameSite);
+ context.CookieName += "1";
+ context.CookieValue += "1";
+ };
+ },
+ requestContext => { },
+ context =>
+ {
+ var feature = context.Features.Get<ITrackingConsentFeature>();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ feature.GrantConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+
+ var cookie = feature.CreateConsentCookie();
+ context.Response.Headers["ManualCookie"] = cookie;
+
+ return Task.CompletedTask;
+ });
+
+ var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ Assert.Equal(1, cookies.Count);
+ var consentCookie = cookies[0];
+ Assert.Equal(".AspNet.Consent1", consentCookie.Name);
+ Assert.Equal("yes1", consentCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite);
+ Assert.NotNull(consentCookie.Expires);
+
+ cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers["ManualCookie"]);
+ Assert.Equal(1, cookies.Count);
+ var manualCookie = cookies[0];
+ Assert.Equal(consentCookie.Name, manualCookie.Name);
+ Assert.Equal(consentCookie.Value, manualCookie.Value);
+ Assert.Equal(consentCookie.SameSite, manualCookie.SameSite);
+ Assert.NotNull(manualCookie.Expires); // Expires may not exactly match to the second.
+ }
+
+ private Task<HttpContext> RunTestAsync(Action<CookiePolicyOptions> configureOptions, Action<HttpContext> configureRequest, RequestDelegate handleRequest)
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.Configure(configureOptions);
+ })
+ .Configure(app =>
+ {
+ app.UseCookiePolicy();
+ app.Run(handleRequest);
+ });
+ var server = new TestServer(builder);
+ return server.SendAsync(configureRequest);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs
new file mode 100644
index 0000000000..a2592e5575
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs
@@ -0,0 +1,471 @@
+// 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.Principal;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.CookiePolicy.Test
+{
+ public class CookiePolicyTests
+ {
+ private RequestDelegate SecureCookieAppends = context =>
+ {
+ context.Response.Cookies.Append("A", "A");
+ context.Response.Cookies.Append("B", "B", new CookieOptions { Secure = false });
+ context.Response.Cookies.Append("C", "C", new CookieOptions());
+ context.Response.Cookies.Append("D", "D", new CookieOptions { Secure = true });
+ return Task.FromResult(0);
+ };
+ private RequestDelegate HttpCookieAppends = context =>
+ {
+ context.Response.Cookies.Append("A", "A");
+ context.Response.Cookies.Append("B", "B", new CookieOptions { HttpOnly = false });
+ context.Response.Cookies.Append("C", "C", new CookieOptions());
+ context.Response.Cookies.Append("D", "D", new CookieOptions { HttpOnly = true });
+ return Task.FromResult(0);
+ };
+ private RequestDelegate SameSiteCookieAppends = context =>
+ {
+ context.Response.Cookies.Append("A", "A");
+ context.Response.Cookies.Append("B", "B", new CookieOptions { SameSite = Http.SameSiteMode.None });
+ context.Response.Cookies.Append("C", "C", new CookieOptions());
+ context.Response.Cookies.Append("D", "D", new CookieOptions { SameSite = Http.SameSiteMode.Lax });
+ context.Response.Cookies.Append("E", "E", new CookieOptions { SameSite = Http.SameSiteMode.Strict });
+ return Task.FromResult(0);
+ };
+
+ [Fact]
+ public async Task SecureAlwaysSetsSecure()
+ {
+ await RunTest("/secureAlways",
+ new CookiePolicyOptions
+ {
+ Secure = CookieSecurePolicy.Always
+ },
+ SecureCookieAppends,
+ new RequestTest("http://example.com/secureAlways",
+ transaction =>
+ {
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal("A=A; path=/; secure; samesite=lax", transaction.SetCookie[0]);
+ Assert.Equal("B=B; path=/; secure; samesite=lax", transaction.SetCookie[1]);
+ Assert.Equal("C=C; path=/; secure; samesite=lax", transaction.SetCookie[2]);
+ Assert.Equal("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]);
+ }));
+ }
+
+ [Fact]
+ public async Task SecureNoneLeavesSecureUnchanged()
+ {
+ await RunTest("/secureNone",
+ new CookiePolicyOptions
+ {
+ Secure = CookieSecurePolicy.None
+ },
+ SecureCookieAppends,
+ new RequestTest("http://example.com/secureNone",
+ transaction =>
+ {
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]);
+ Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]);
+ Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]);
+ Assert.Equal("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]);
+ }));
+ }
+
+ [Fact]
+ public async Task SecureSameUsesRequest()
+ {
+ await RunTest("/secureSame",
+ new CookiePolicyOptions
+ {
+ Secure = CookieSecurePolicy.SameAsRequest
+ },
+ SecureCookieAppends,
+ new RequestTest("http://example.com/secureSame",
+ transaction =>
+ {
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]);
+ Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]);
+ Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]);
+ Assert.Equal("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]);
+ }),
+ new RequestTest("https://example.com/secureSame",
+ transaction =>
+ {
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal("A=A; path=/; secure; samesite=lax", transaction.SetCookie[0]);
+ Assert.Equal("B=B; path=/; secure; samesite=lax", transaction.SetCookie[1]);
+ Assert.Equal("C=C; path=/; secure; samesite=lax", transaction.SetCookie[2]);
+ Assert.Equal("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]);
+ }));
+ }
+
+ [Fact]
+ public async Task HttpOnlyAlwaysSetsItAlways()
+ {
+ await RunTest("/httpOnlyAlways",
+ new CookiePolicyOptions
+ {
+ HttpOnly = HttpOnlyPolicy.Always
+ },
+ HttpCookieAppends,
+ new RequestTest("http://example.com/httpOnlyAlways",
+ transaction =>
+ {
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal("A=A; path=/; samesite=lax; httponly", transaction.SetCookie[0]);
+ Assert.Equal("B=B; path=/; samesite=lax; httponly", transaction.SetCookie[1]);
+ Assert.Equal("C=C; path=/; samesite=lax; httponly", transaction.SetCookie[2]);
+ Assert.Equal("D=D; path=/; samesite=lax; httponly", transaction.SetCookie[3]);
+ }));
+ }
+
+ [Fact]
+ public async Task HttpOnlyNoneLeavesItAlone()
+ {
+ await RunTest("/httpOnlyNone",
+ new CookiePolicyOptions
+ {
+ HttpOnly = HttpOnlyPolicy.None
+ },
+ HttpCookieAppends,
+ new RequestTest("http://example.com/httpOnlyNone",
+ transaction =>
+ {
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]);
+ Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]);
+ Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]);
+ Assert.Equal("D=D; path=/; samesite=lax; httponly", transaction.SetCookie[3]);
+ }));
+ }
+
+ [Fact]
+ public async Task SameSiteStrictSetsItAlways()
+ {
+ await RunTest("/sameSiteStrict",
+ new CookiePolicyOptions
+ {
+ MinimumSameSitePolicy = Http.SameSiteMode.Strict
+ },
+ SameSiteCookieAppends,
+ new RequestTest("http://example.com/sameSiteStrict",
+ transaction =>
+ {
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal("A=A; path=/; samesite=strict", transaction.SetCookie[0]);
+ Assert.Equal("B=B; path=/; samesite=strict", transaction.SetCookie[1]);
+ Assert.Equal("C=C; path=/; samesite=strict", transaction.SetCookie[2]);
+ Assert.Equal("D=D; path=/; samesite=strict", transaction.SetCookie[3]);
+ Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
+ }));
+ }
+
+ [Fact]
+ public async Task SameSiteLaxSetsItAlways()
+ {
+ await RunTest("/sameSiteLax",
+ new CookiePolicyOptions
+ {
+ MinimumSameSitePolicy = Http.SameSiteMode.Lax
+ },
+ SameSiteCookieAppends,
+ new RequestTest("http://example.com/sameSiteLax",
+ transaction =>
+ {
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]);
+ Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]);
+ Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]);
+ Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]);
+ Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
+ }));
+ }
+
+ [Fact]
+ public async Task SameSiteNoneLeavesItAlone()
+ {
+ await RunTest("/sameSiteNone",
+ new CookiePolicyOptions
+ {
+ MinimumSameSitePolicy = Http.SameSiteMode.None
+ },
+ SameSiteCookieAppends,
+ new RequestTest("http://example.com/sameSiteNone",
+ transaction =>
+ {
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal("A=A; path=/", transaction.SetCookie[0]);
+ Assert.Equal("B=B; path=/", transaction.SetCookie[1]);
+ Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]);
+ Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]);
+ Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
+ }));
+ }
+
+ [Fact]
+ public async Task CookiePolicyCanHijackAppend()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseCookiePolicy(new CookiePolicyOptions
+ {
+ OnAppendCookie = ctx => ctx.CookieName = ctx.CookieValue = "Hao"
+ });
+ app.Run(context =>
+ {
+ context.Response.Cookies.Append("A", "A");
+ context.Response.Cookies.Append("B", "B", new CookieOptions { Secure = false });
+ context.Response.Cookies.Append("C", "C", new CookieOptions());
+ context.Response.Cookies.Append("D", "D", new CookieOptions { Secure = true });
+ return Task.FromResult(0);
+ });
+ });
+ var server = new TestServer(builder);
+
+ var transaction = await server.SendAsync("http://example.com/login");
+
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal("Hao=Hao; path=/; samesite=lax", transaction.SetCookie[0]);
+ Assert.Equal("Hao=Hao; path=/; samesite=lax", transaction.SetCookie[1]);
+ Assert.Equal("Hao=Hao; path=/; samesite=lax", transaction.SetCookie[2]);
+ Assert.Equal("Hao=Hao; path=/; secure; samesite=lax", transaction.SetCookie[3]);
+ }
+
+ [Fact]
+ public async Task CookiePolicyCanHijackDelete()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseCookiePolicy(new CookiePolicyOptions
+ {
+ OnDeleteCookie = ctx => ctx.CookieName = "A"
+ });
+ app.Run(context =>
+ {
+ context.Response.Cookies.Delete("A");
+ context.Response.Cookies.Delete("B", new CookieOptions { Secure = false });
+ context.Response.Cookies.Delete("C", new CookieOptions());
+ context.Response.Cookies.Delete("D", new CookieOptions { Secure = true });
+ return Task.FromResult(0);
+ });
+ });
+ var server = new TestServer(builder);
+
+ var transaction = await server.SendAsync("http://example.com/login");
+
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal(1, transaction.SetCookie.Count);
+ Assert.Equal("A=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure; samesite=lax", transaction.SetCookie[0]);
+ }
+
+ [Fact]
+ public async Task CookiePolicyCallsCookieFeature()
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.Use(next => context =>
+ {
+ context.Features.Set<IResponseCookiesFeature>(new TestCookieFeature());
+ return next(context);
+ });
+ app.UseCookiePolicy(new CookiePolicyOptions
+ {
+ OnDeleteCookie = ctx => ctx.CookieName = "A"
+ });
+ app.Run(context =>
+ {
+ Assert.Throws<NotImplementedException>(() => context.Response.Cookies.Delete("A"));
+ Assert.Throws<NotImplementedException>(() => context.Response.Cookies.Delete("A", new CookieOptions()));
+ Assert.Throws<NotImplementedException>(() => context.Response.Cookies.Append("A", "A"));
+ Assert.Throws<NotImplementedException>(() => context.Response.Cookies.Append("A", "A", new CookieOptions()));
+ return context.Response.WriteAsync("Done");
+ });
+ });
+ var server = new TestServer(builder);
+
+ var transaction = await server.SendAsync("http://example.com/login");
+ Assert.Equal("Done", transaction.ResponseText);
+ }
+
+ [Fact]
+ public async Task CookiePolicyAppliesToCookieAuth()
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddAuthentication().AddCookie(o =>
+ {
+ o.Cookie.Name = "TestCookie";
+ o.Cookie.HttpOnly = false;
+ o.Cookie.SecurePolicy = CookieSecurePolicy.None;
+ });
+ })
+ .Configure(app =>
+ {
+ app.UseCookiePolicy(new CookiePolicyOptions
+ {
+ HttpOnly = HttpOnlyPolicy.Always,
+ Secure = CookieSecurePolicy.Always,
+ });
+ app.UseAuthentication();
+ app.Run(context =>
+ {
+ return context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("TestUser", "Cookies"))));
+ });
+ });
+ var server = new TestServer(builder);
+
+ var transaction = await server.SendAsync("http://example.com/login");
+
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal(1, transaction.SetCookie.Count);
+ var cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[0]);
+ Assert.Equal("TestCookie", cookie.Name);
+ Assert.True(cookie.HttpOnly);
+ Assert.True(cookie.Secure);
+ Assert.Equal("/", cookie.Path);
+ }
+
+ [Fact]
+ public async Task CookiePolicyAppliesToCookieAuthChunks()
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddAuthentication().AddCookie(o =>
+ {
+ o.Cookie.Name = "TestCookie";
+ o.Cookie.HttpOnly = false;
+ o.Cookie.SecurePolicy = CookieSecurePolicy.None;
+ });
+ })
+ .Configure(app =>
+ {
+ app.UseCookiePolicy(new CookiePolicyOptions
+ {
+ HttpOnly = HttpOnlyPolicy.Always,
+ Secure = CookieSecurePolicy.Always,
+ });
+ app.UseAuthentication();
+ app.Run(context =>
+ {
+ return context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
+ new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity(new string('c', 1024 * 5), "Cookies"))));
+ });
+ });
+ var server = new TestServer(builder);
+
+ var transaction = await server.SendAsync("http://example.com/login");
+
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal(3, transaction.SetCookie.Count);
+
+ var cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[0]);
+ Assert.Equal("TestCookie", cookie.Name);
+ Assert.Equal("chunks-2", cookie.Value);
+ Assert.True(cookie.HttpOnly);
+ Assert.True(cookie.Secure);
+ Assert.Equal("/", cookie.Path);
+
+ cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[1]);
+ Assert.Equal("TestCookieC1", cookie.Name);
+ Assert.True(cookie.HttpOnly);
+ Assert.True(cookie.Secure);
+ Assert.Equal("/", cookie.Path);
+
+ cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[2]);
+ Assert.Equal("TestCookieC2", cookie.Name);
+ Assert.True(cookie.HttpOnly);
+ Assert.True(cookie.Secure);
+ Assert.Equal("/", cookie.Path);
+ }
+
+ private class TestCookieFeature : IResponseCookiesFeature
+ {
+ public IResponseCookies Cookies { get; } = new BadCookies();
+
+ private class BadCookies : IResponseCookies
+ {
+ public void Append(string key, string value)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Append(string key, string value, CookieOptions options)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Delete(string key)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Delete(string key, CookieOptions options)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+
+ private class RequestTest
+ {
+ public RequestTest(string testUri, Action<Transaction> verify)
+ {
+ TestUri = testUri;
+ Verification = verify;
+ }
+
+ public async Task Execute(TestServer server)
+ {
+ var transaction = await server.SendAsync(TestUri);
+ Verification(transaction);
+ }
+
+ public string TestUri { get; set; }
+ public Action<Transaction> Verification { get; set; }
+ }
+
+ private async Task RunTest(
+ string path,
+ CookiePolicyOptions cookiePolicy,
+ RequestDelegate configureSetup,
+ params RequestTest[] tests)
+ {
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.Map(path, map =>
+ {
+ map.UseCookiePolicy(cookiePolicy);
+ map.Run(configureSetup);
+ });
+ });
+ var server = new TestServer(builder);
+ foreach (var test in tests)
+ {
+ await test.Execute(server);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj
new file mode 100644
index 0000000000..d7a42f3efb
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.CookiePolicy\Microsoft.AspNetCore.CookiePolicy.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/TestExtensions.cs b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/TestExtensions.cs
new file mode 100644
index 0000000000..9456094d41
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/TestExtensions.cs
@@ -0,0 +1,68 @@
+// 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.Linq;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+
+namespace Microsoft.AspNetCore.CookiePolicy
+{
+ // REVIEW: Should find a shared home for these potentially (Copied from Auth tests)
+ public static class TestExtensions
+ {
+ public const string CookieAuthenticationScheme = "External";
+
+ public static async Task<Transaction> SendAsync(this TestServer server, string uri, string cookieHeader = null)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, uri);
+ if (!string.IsNullOrEmpty(cookieHeader))
+ {
+ request.Headers.Add("Cookie", cookieHeader);
+ }
+ var transaction = new Transaction
+ {
+ Request = request,
+ Response = await server.CreateClient().SendAsync(request),
+ };
+ if (transaction.Response.Headers.Contains("Set-Cookie"))
+ {
+ transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList();
+ }
+ transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync();
+
+ if (transaction.Response.Content != null &&
+ transaction.Response.Content.Headers.ContentType != null &&
+ transaction.Response.Content.Headers.ContentType.MediaType == "text/xml")
+ {
+ transaction.ResponseElement = XElement.Parse(transaction.ResponseText);
+ }
+ return transaction;
+ }
+
+ public static void Describe(this HttpResponse res, ClaimsPrincipal principal)
+ {
+ res.StatusCode = 200;
+ res.ContentType = "text/xml";
+ var xml = new XElement("xml");
+ if (principal != null)
+ {
+ foreach (var identity in principal.Identities)
+ {
+ xml.Add(identity.Claims.Select(claim =>
+ new XElement("claim", new XAttribute("type", claim.Type),
+ new XAttribute("value", claim.Value),
+ new XAttribute("issuer", claim.Issuer))));
+ }
+ }
+ var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString());
+ res.Body.Write(xmlBytes, 0, xmlBytes.Length);
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Transaction.cs b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Transaction.cs
new file mode 100644
index 0000000000..040e0b3391
--- /dev/null
+++ b/src/Security/test/Microsoft.AspNetCore.CookiePolicy.Test/Transaction.cs
@@ -0,0 +1,51 @@
+// 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.Linq;
+using System.Net.Http;
+using System.Xml.Linq;
+
+namespace Microsoft.AspNetCore.CookiePolicy
+{
+ // REVIEW: Should find a shared home for these potentially (Copied from Auth tests)
+ public class Transaction
+ {
+ public HttpRequestMessage Request { get; set; }
+ public HttpResponseMessage Response { get; set; }
+
+ public IList<string> SetCookie { get; set; }
+
+ public string ResponseText { get; set; }
+ public XElement ResponseElement { get; set; }
+
+ public string AuthenticationCookieValue
+ {
+ get
+ {
+ if (SetCookie != null && SetCookie.Count > 0)
+ {
+ var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme + "="));
+ if (authCookie != null)
+ {
+ return authCookie.Substring(0, authCookie.IndexOf(';'));
+ }
+ }
+
+ return null;
+ }
+ }
+
+ public string FindClaimValue(string claimType, string issuer = null)
+ {
+ var claim = ResponseElement.Elements("claim")
+ .SingleOrDefault(elt => elt.Attribute("type").Value == claimType &&
+ (issuer == null || elt.Attribute("issuer").Value == issuer));
+ if (claim == null)
+ {
+ return null;
+ }
+ return claim.Attribute("value").Value;
+ }
+ }
+}
diff --git a/src/Security/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs b/src/Security/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs
new file mode 100644
index 0000000000..e2e4fd7d07
--- /dev/null
+++ b/src/Security/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs
@@ -0,0 +1,332 @@
+// 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.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+using Microsoft.Owin.Security.Cookies;
+using Microsoft.Owin.Testing;
+using Owin;
+using Xunit;
+
+namespace Microsoft.Owin.Security.Interop
+{
+ public class CookiesInteropTests
+ {
+ [Fact]
+ public async Task AspNetCoreWithInteropCookieContainsIdentity()
+ {
+ var identity = new ClaimsIdentity("Cookies");
+ identity.AddClaim(new Claim(ClaimTypes.Name, "Alice"));
+
+ var dataProtection = DataProtectionProvider.Create(new DirectoryInfo("..\\..\\artifacts"));
+ var dataProtector = dataProtection.CreateProtector(
+ "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", // full name of the ASP.NET Core type
+ Cookies.CookieAuthenticationDefaults.AuthenticationType, "v2");
+
+ var interopServer = TestServer.Create(app =>
+ {
+ app.Properties["host.AppName"] = "Microsoft.Owin.Security.Tests";
+
+ app.UseCookieAuthentication(new Cookies.CookieAuthenticationOptions
+ {
+ TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)),
+ CookieName = AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.CookiePrefix
+ + AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme,
+ });
+
+ app.Run(context =>
+ {
+ context.Authentication.SignIn(identity);
+ return Task.FromResult(0);
+ });
+ });
+
+ var transaction = await SendAsync(interopServer, "http://example.com");
+
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Run(async context =>
+ {
+ var result = await context.AuthenticateAsync("Cookies");
+ await context.Response.WriteAsync(result.Ticket.Principal.Identity.Name);
+ });
+ })
+ .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.DataProtectionProvider = dataProtection));
+ var newServer = new AspNetCore.TestHost.TestServer(builder);
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/login");
+ foreach (var cookie in SetCookieHeaderValue.ParseList(transaction.SetCookie))
+ {
+ request.Headers.Add("Cookie", cookie.Name + "=" + cookie.Value);
+ }
+ var response = await newServer.CreateClient().SendAsync(request);
+
+ Assert.Equal("Alice", await response.Content.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task AspNetCoreWithLargeInteropCookieContainsIdentity()
+ {
+ var identity = new ClaimsIdentity("Cookies");
+ identity.AddClaim(new Claim(ClaimTypes.Name, new string('a', 1024 * 5)));
+
+ var dataProtection = DataProtectionProvider.Create(new DirectoryInfo("..\\..\\artifacts"));
+ var dataProtector = dataProtection.CreateProtector(
+ "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", // full name of the ASP.NET Core type
+ Cookies.CookieAuthenticationDefaults.AuthenticationType, "v2");
+
+ var interopServer = TestServer.Create(app =>
+ {
+ app.Properties["host.AppName"] = "Microsoft.Owin.Security.Tests";
+
+ app.UseCookieAuthentication(new Cookies.CookieAuthenticationOptions
+ {
+ TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)),
+ CookieName = AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.CookiePrefix
+ + AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme,
+ CookieManager = new ChunkingCookieManager(),
+ });
+
+ app.Run(context =>
+ {
+ context.Authentication.SignIn(identity);
+ return Task.FromResult(0);
+ });
+ });
+
+ var transaction = await SendAsync(interopServer, "http://example.com");
+
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Run(async context =>
+ {
+ var result = await context.AuthenticateAsync("Cookies");
+ await context.Response.WriteAsync(result.Ticket.Principal.Identity.Name);
+ });
+ })
+ .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.DataProtectionProvider = dataProtection));
+ var newServer = new AspNetCore.TestHost.TestServer(builder);
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/login");
+ foreach (var cookie in SetCookieHeaderValue.ParseList(transaction.SetCookie))
+ {
+ request.Headers.Add("Cookie", cookie.Name + "=" + cookie.Value);
+ }
+ var response = await newServer.CreateClient().SendAsync(request);
+
+ Assert.Equal(1024 * 5, (await response.Content.ReadAsStringAsync()).Length);
+ }
+
+ [Fact]
+ public async Task InteropWithNewCookieContainsIdentity()
+ {
+ var user = new ClaimsPrincipal();
+ var identity = new ClaimsIdentity("scheme");
+ identity.AddClaim(new Claim(ClaimTypes.Name, "Alice"));
+ user.AddIdentity(identity);
+
+ var dataProtection = DataProtectionProvider.Create(new DirectoryInfo("..\\..\\artifacts"));
+ var dataProtector = dataProtection.CreateProtector(
+ "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", // full name of the ASP.NET Core type
+ Cookies.CookieAuthenticationDefaults.AuthenticationType, "v2");
+
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Run(context => context.SignInAsync("Cookies", user));
+ })
+ .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.DataProtectionProvider = dataProtection));
+ var newServer = new AspNetCore.TestHost.TestServer(builder);
+
+ var cookies = await SendAndGetCookies(newServer, "http://example.com/login");
+
+ var server = TestServer.Create(app =>
+ {
+ app.Properties["host.AppName"] = "Microsoft.Owin.Security.Tests";
+
+ app.UseCookieAuthentication(new Cookies.CookieAuthenticationOptions
+ {
+ TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)),
+ CookieName = AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.CookiePrefix
+ + AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme,
+ });
+
+ app.Run(async context =>
+ {
+ var result = await context.Authentication.AuthenticateAsync("Cookies");
+ Describe(context.Response, result);
+ });
+ });
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", cookies);
+
+ Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name));
+ }
+
+ [Fact]
+ public async Task InteropWithLargeNewCookieContainsIdentity()
+ {
+ var user = new ClaimsPrincipal();
+ var identity = new ClaimsIdentity("scheme");
+ identity.AddClaim(new Claim(ClaimTypes.Name, new string('a', 1024 * 5)));
+ user.AddIdentity(identity);
+
+ var dataProtection = DataProtectionProvider.Create(new DirectoryInfo("..\\..\\artifacts"));
+ var dataProtector = dataProtection.CreateProtector(
+ "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", // full name of the ASP.NET Core type
+ Cookies.CookieAuthenticationDefaults.AuthenticationType, "v2");
+
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseAuthentication();
+ app.Run(context => context.SignInAsync("Cookies", user));
+ })
+ .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.DataProtectionProvider = dataProtection));
+ var newServer = new AspNetCore.TestHost.TestServer(builder);
+
+ var cookies = await SendAndGetCookies(newServer, "http://example.com/login");
+
+ var server = TestServer.Create(app =>
+ {
+ app.Properties["host.AppName"] = "Microsoft.Owin.Security.Tests";
+
+ app.UseCookieAuthentication(new Cookies.CookieAuthenticationOptions
+ {
+ TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)),
+ CookieName = AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.CookiePrefix
+ + AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme,
+ CookieManager = new ChunkingCookieManager(),
+ });
+
+ app.Run(async context =>
+ {
+ var result = await context.Authentication.AuthenticateAsync("Cookies");
+ Describe(context.Response, result);
+ });
+ });
+
+ var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", cookies);
+
+ Assert.Equal(1024 * 5, FindClaimValue(transaction2, ClaimTypes.Name).Length);
+ }
+
+ private static async Task<IList<string>> SendAndGetCookies(AspNetCore.TestHost.TestServer server, string uri)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, uri);
+ var response = await server.CreateClient().SendAsync(request);
+ if (response.Headers.Contains("Set-Cookie"))
+ {
+ IList<string> cookieHeaders = new List<string>();
+ foreach (var cookie in SetCookieHeaderValue.ParseList(response.Headers.GetValues("Set-Cookie").ToList()))
+ {
+ cookieHeaders.Add(cookie.Name + "=" + cookie.Value);
+ }
+ return cookieHeaders;
+ }
+ return null;
+ }
+
+ private static string FindClaimValue(Transaction transaction, string claimType)
+ {
+ XElement claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType);
+ if (claim == null)
+ {
+ return null;
+ }
+ return claim.Attribute("value").Value;
+ }
+
+ private static void Describe(IOwinResponse res, AuthenticateResult result)
+ {
+ res.StatusCode = 200;
+ res.ContentType = "text/xml";
+ var xml = new XElement("xml");
+ if (result != null && result.Identity != null)
+ {
+ xml.Add(result.Identity.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value))));
+ }
+ if (result != null && result.Properties != null)
+ {
+ xml.Add(result.Properties.Dictionary.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value))));
+ }
+ using (var memory = new MemoryStream())
+ {
+ using (var writer = new XmlTextWriter(memory, Encoding.UTF8))
+ {
+ xml.WriteTo(writer);
+ }
+ res.Body.Write(memory.ToArray(), 0, memory.ToArray().Length);
+ }
+ }
+
+ private static async Task<Transaction> SendAsync(TestServer server, string uri, IList<string> cookieHeaders = null, bool ajaxRequest = false)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, uri);
+ if (cookieHeaders != null)
+ {
+ request.Headers.Add("Cookie", cookieHeaders);
+ }
+ if (ajaxRequest)
+ {
+ request.Headers.Add("X-Requested-With", "XMLHttpRequest");
+ }
+ var transaction = new Transaction
+ {
+ Request = request,
+ Response = await server.HttpClient.SendAsync(request),
+ };
+ if (transaction.Response.Headers.Contains("Set-Cookie"))
+ {
+ transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList();
+ }
+ if (transaction.SetCookie != null && transaction.SetCookie.Any())
+ {
+ transaction.CookieNameValue = transaction.SetCookie.First().Split(new[] { ';' }, 2).First();
+ }
+ transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync();
+
+ if (transaction.Response.Content != null &&
+ transaction.Response.Content.Headers.ContentType != null &&
+ transaction.Response.Content.Headers.ContentType.MediaType == "text/xml")
+ {
+ transaction.ResponseElement = XElement.Parse(transaction.ResponseText);
+ }
+ return transaction;
+ }
+
+ private class Transaction
+ {
+ public HttpRequestMessage Request { get; set; }
+ public HttpResponseMessage Response { get; set; }
+
+ public IList<string> SetCookie { get; set; }
+ public string CookieNameValue { get; set; }
+
+ public string ResponseText { get; set; }
+ public XElement ResponseElement { get; set; }
+ }
+
+ }
+}
+
diff --git a/src/Security/test/Microsoft.Owin.Security.Interop.Test/Microsoft.Owin.Security.Interop.Test.csproj b/src/Security/test/Microsoft.Owin.Security.Interop.Test/Microsoft.Owin.Security.Interop.Test.csproj
new file mode 100644
index 0000000000..f369f1f01a
--- /dev/null
+++ b/src/Security/test/Microsoft.Owin.Security.Interop.Test/Microsoft.Owin.Security.Interop.Test.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net461</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
+ <ProjectReference Include="..\..\src\Microsoft.Owin.Security.Interop\Microsoft.Owin.Security.Interop.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />
+ <PackageReference Include="Microsoft.Owin.Security.Cookies" Version="$(MicrosoftOwinSecurityCookiesPackageVersion)" />
+ <PackageReference Include="Microsoft.Owin.Testing" Version="$(MicrosoftOwinTestingPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Security/test/Microsoft.Owin.Security.Interop.Test/TicketInteropTests.cs b/src/Security/test/Microsoft.Owin.Security.Interop.Test/TicketInteropTests.cs
new file mode 100644
index 0000000000..769adc015b
--- /dev/null
+++ b/src/Security/test/Microsoft.Owin.Security.Interop.Test/TicketInteropTests.cs
@@ -0,0 +1,91 @@
+// 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 Microsoft.AspNetCore.Authentication;
+using Xunit;
+
+namespace Microsoft.Owin.Security.Interop.Test
+{
+ public class TicketInteropTests
+ {
+ [Fact]
+ public void NewSerializerCanReadInteropTicket()
+ {
+ var identity = new ClaimsIdentity("scheme");
+ identity.AddClaim(new Claim("Test", "Value"));
+
+ var expires = DateTime.Today;
+ var issued = new DateTime(1979, 11, 11);
+ var properties = new Owin.Security.AuthenticationProperties();
+ properties.IsPersistent = true;
+ properties.RedirectUri = "/redirect";
+ properties.Dictionary["key"] = "value";
+ properties.ExpiresUtc = expires;
+ properties.IssuedUtc = issued;
+
+ var interopTicket = new Owin.Security.AuthenticationTicket(identity, properties);
+ var interopSerializer = new AspNetTicketSerializer();
+
+ var bytes = interopSerializer.Serialize(interopTicket);
+
+ var newSerializer = new TicketSerializer();
+ var newTicket = newSerializer.Deserialize(bytes);
+
+ Assert.NotNull(newTicket);
+ Assert.Single(newTicket.Principal.Identities);
+ var newIdentity = newTicket.Principal.Identity as ClaimsIdentity;
+ Assert.NotNull(newIdentity);
+ Assert.Equal("scheme", newIdentity.AuthenticationType);
+ Assert.True(newIdentity.HasClaim(c => c.Type == "Test" && c.Value == "Value"));
+ Assert.NotNull(newTicket.Properties);
+ Assert.True(newTicket.Properties.IsPersistent);
+ Assert.Equal("/redirect", newTicket.Properties.RedirectUri);
+ Assert.Equal("value", newTicket.Properties.Items["key"]);
+ Assert.Equal(expires, newTicket.Properties.ExpiresUtc);
+ Assert.Equal(issued, newTicket.Properties.IssuedUtc);
+ }
+
+ [Fact]
+ public void InteropSerializerCanReadNewTicket()
+ {
+ var user = new ClaimsPrincipal();
+ var identity = new ClaimsIdentity("scheme");
+ identity.AddClaim(new Claim("Test", "Value"));
+ user.AddIdentity(identity);
+
+ var expires = DateTime.Today;
+ var issued = new DateTime(1979, 11, 11);
+ var properties = new AspNetCore.Authentication.AuthenticationProperties();
+ properties.IsPersistent = true;
+ properties.RedirectUri = "/redirect";
+ properties.Items["key"] = "value";
+ properties.ExpiresUtc = expires;
+ properties.IssuedUtc = issued;
+
+ var newTicket = new AspNetCore.Authentication.AuthenticationTicket(user, properties, "scheme");
+ var newSerializer = new TicketSerializer();
+
+ var bytes = newSerializer.Serialize(newTicket);
+
+ var interopSerializer = new AspNetTicketSerializer();
+ var interopTicket = interopSerializer.Deserialize(bytes);
+
+ Assert.NotNull(interopTicket);
+ var newIdentity = interopTicket.Identity;
+ Assert.NotNull(newIdentity);
+ Assert.Equal("scheme", newIdentity.AuthenticationType);
+ Assert.True(newIdentity.HasClaim(c => c.Type == "Test" && c.Value == "Value"));
+ Assert.NotNull(interopTicket.Properties);
+ Assert.True(interopTicket.Properties.IsPersistent);
+ Assert.Equal("/redirect", interopTicket.Properties.RedirectUri);
+ Assert.Equal("value", interopTicket.Properties.Dictionary["key"]);
+ Assert.Equal(expires, interopTicket.Properties.ExpiresUtc);
+ Assert.Equal(issued, interopTicket.Properties.IssuedUtc);
+ }
+ }
+}
+
+
diff --git a/src/Security/version.props b/src/Security/version.props
new file mode 100644
index 0000000000..478dfd16ed
--- /dev/null
+++ b/src/Security/version.props
@@ -0,0 +1,12 @@
+<Project>
+ <PropertyGroup>
+ <VersionPrefix>2.1.2</VersionPrefix>
+ <VersionSuffix>rtm</VersionSuffix>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' == 'rtm' ">$(VersionPrefix)</PackageVersion>
+ <PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' != 'rtm' ">$(VersionPrefix)-$(VersionSuffix)-final</PackageVersion>
+ <BuildNumber Condition="'$(BuildNumber)' == ''">t000</BuildNumber>
+ <FeatureBranchVersionPrefix Condition="'$(FeatureBranchVersionPrefix)' == ''">a-</FeatureBranchVersionPrefix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix>
+ <VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
+ </PropertyGroup>
+</Project>
diff --git a/src/ServerTests/Directory.Build.props b/src/ServerTests/Directory.Build.props
index fc93574bae..cefef093b1 100644
--- a/src/ServerTests/Directory.Build.props
+++ b/src/ServerTests/Directory.Build.props
@@ -9,7 +9,7 @@
<PropertyGroup>
<Product>Microsoft ASP.NET Core</Product>
- <RepositoryUrl>https://github.com/aspnet/servertests</RepositoryUrl>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
diff --git a/src/Session/Directory.Build.props b/src/Session/Directory.Build.props
index 48f12702ba..cd0187875b 100644
--- a/src/Session/Directory.Build.props
+++ b/src/Session/Directory.Build.props
@@ -9,7 +9,7 @@
<PropertyGroup>
<Product>Microsoft ASP.NET Core</Product>
- <RepositoryUrl>https://github.com/aspnet/Session</RepositoryUrl>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)build\Key.snk</AssemblyOriginatorKeyFile>
diff --git a/src/StaticFiles/Directory.Build.props b/src/StaticFiles/Directory.Build.props
index c46c5e323b..cd0187875b 100644
--- a/src/StaticFiles/Directory.Build.props
+++ b/src/StaticFiles/Directory.Build.props
@@ -9,7 +9,7 @@
<PropertyGroup>
<Product>Microsoft ASP.NET Core</Product>
- <RepositoryUrl>https://github.com/aspnet/StaticFiles</RepositoryUrl>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)build\Key.snk</AssemblyOriginatorKeyFile>
diff --git a/src/Templating/Directory.Build.props b/src/Templating/Directory.Build.props
index 9887edd5d5..d7eb964138 100644
--- a/src/Templating/Directory.Build.props
+++ b/src/Templating/Directory.Build.props
@@ -10,7 +10,7 @@
<PropertyGroup>
<Product>Microsoft ASP.NET Core</Product>
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
- <RepositoryUrl>https://github.com/aspnet/Templating</RepositoryUrl>
+ <RepositoryUrl>https://github.com/aspnet/AspNetCore</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>