From 9ee38c0efb05b2413a0f768bb2694134bbbb68aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 16 Aug 2019 22:56:18 +0200 Subject: [PATCH] Introduce the first experimental OpenIddict 3.0 bits and move the build infrastructure to Arcade --- .travis.yml | 2 +- Build.cmd | 3 + Directory.Build.props | 49 + Directory.Build.targets | 5 + LICENSE.md | 26 + NuGet.config | 6 +- OpenIddict.sln | 103 +- README.md | 12 +- build.cmd | 2 - build.ps1 | 67 - build.sh | 18 +- build/common.props | 25 - build/dependencies.props | 26 - build/packages.props | 20 - build/repo.props | 7 - build/version.props | 9 - eng/Signing.props | 13 + eng/Version.Details.xml | 19 + eng/Versions.props | 29 + eng/common/CIBuild.cmd | 2 + eng/common/CheckSymbols.ps1 | 158 + eng/common/PSScriptAnalyzerSettings.psd1 | 11 + eng/common/PublishToPackageFeed.proj | 83 + eng/common/PublishToSymbolServers.proj | 82 + eng/common/README.md | 28 + eng/common/SigningValidation.proj | 83 + eng/common/SourceLinkValidation.ps1 | 184 ++ eng/common/build.ps1 | 141 + eng/common/build.sh | 216 ++ eng/common/cibuild.sh | 16 + eng/common/cross/android/arm/toolchain.cmake | 41 + .../cross/android/arm64/toolchain.cmake | 42 + eng/common/cross/arm/sources.list.bionic | 11 + eng/common/cross/arm/sources.list.jessie | 3 + eng/common/cross/arm/sources.list.trusty | 11 + eng/common/cross/arm/sources.list.xenial | 11 + eng/common/cross/arm/sources.list.zesty | 11 + eng/common/cross/arm/trusty-lttng-2.4.patch | 71 + eng/common/cross/arm/trusty.patch | 97 + eng/common/cross/arm64/sources.list.bionic | 11 + eng/common/cross/arm64/sources.list.buster | 11 + eng/common/cross/arm64/sources.list.stretch | 12 + eng/common/cross/arm64/sources.list.trusty | 11 + eng/common/cross/arm64/sources.list.xenial | 11 + eng/common/cross/arm64/sources.list.zesty | 11 + eng/common/cross/armel/sources.list.jessie | 3 + eng/common/cross/armel/tizen-build-rootfs.sh | 44 + eng/common/cross/armel/tizen-fetch.sh | 171 ++ eng/common/cross/armel/tizen/tizen-dotnet.ks | 50 + eng/common/cross/armel/tizen/tizen.patch | 18 + eng/common/cross/build-android-rootfs.sh | 137 + eng/common/cross/build-rootfs.sh | 234 ++ eng/common/cross/toolchain.cmake | 138 + eng/common/cross/x86/sources.list.bionic | 11 + eng/common/cross/x86/sources.list.trusty | 11 + eng/common/cross/x86/sources.list.xenial | 11 + eng/common/darc-init.ps1 | 33 + eng/common/darc-init.sh | 64 + eng/common/dotnet-install.cmd | 2 + eng/common/dotnet-install.ps1 | 27 + eng/common/dotnet-install.sh | 49 + eng/common/generate-graph-files.ps1 | 87 + eng/common/helixpublish.proj | 26 + eng/common/init-tools-native.cmd | 3 + eng/common/init-tools-native.ps1 | 147 + eng/common/init-tools-native.sh | 141 + eng/common/internal-feed-operations.ps1 | 135 + eng/common/internal-feed-operations.sh | 142 + eng/common/internal/Directory.Build.props | 4 + eng/common/internal/Tools.csproj | 27 + eng/common/msbuild.ps1 | 27 + eng/common/msbuild.sh | 58 + eng/common/native/CommonLibrary.psm1 | 387 +++ eng/common/native/common-library.sh | 168 ++ eng/common/native/install-cmake-test.sh | 117 + eng/common/native/install-cmake.sh | 117 + eng/common/native/install-tool.ps1 | 130 + eng/common/performance/perfhelixpublish.proj | 77 + eng/common/performance/performance-setup.ps1 | 91 + eng/common/performance/performance-setup.sh | 176 ++ eng/common/pipeline-logging-functions.ps1 | 234 ++ eng/common/pipeline-logging-functions.sh | 172 ++ eng/common/post-build/darc-gather-drop.ps1 | 35 + eng/common/post-build/dotnetsymbol-init.ps1 | 29 + eng/common/post-build/nuget-validation.ps1 | 25 + eng/common/post-build/post-build-utils.ps1 | 90 + eng/common/post-build/promote-build.ps1 | 48 + eng/common/post-build/setup-maestro-vars.ps1 | 26 + eng/common/post-build/sourcelink-cli-init.ps1 | 29 + .../post-build/sourcelink-validation.ps1 | 227 ++ eng/common/post-build/symbols-validation.ps1 | 189 ++ .../post-build/trigger-subscriptions.ps1 | 57 + eng/common/sdk-task.ps1 | 79 + eng/common/sdl/NuGet.config | 13 + eng/common/sdl/execute-all-sdl-tools.ps1 | 99 + eng/common/sdl/extract-artifact-packages.ps1 | 70 + eng/common/sdl/init-sdl.ps1 | 48 + eng/common/sdl/packages.config | 4 + eng/common/sdl/push-gdn.ps1 | 51 + eng/common/sdl/run-sdl.ps1 | 67 + eng/common/templates/job/execute-sdl.yml | 54 + .../templates/job/generate-graph-files.yml | 48 + eng/common/templates/job/job.yml | 209 ++ eng/common/templates/job/performance.yml | 93 + .../templates/job/publish-build-assets.yml | 84 + eng/common/templates/jobs/jobs.yml | 90 + eng/common/templates/phases/base.yml | 130 + .../templates/phases/publish-build-assets.yml | 51 + .../channels/internal-servicing.yml | 147 + .../post-build/channels/netcore-dev-5.yml | 148 + .../channels/netcore-tools-latest.yml | 148 + .../channels/public-dev-release.yml | 148 + .../post-build/channels/public-release.yml | 147 + .../channels/public-validation-release.yml | 99 + .../templates/post-build/common-variables.yml | 47 + .../templates/post-build/darc-gather-drop.yml | 23 + .../templates/post-build/post-build.yml | 102 + .../templates/post-build/promote-build.yml | 25 + .../post-build/setup-maestro-vars.yml | 18 + .../post-build/trigger-subscription.yml | 13 + eng/common/templates/steps/build-reason.yml | 12 + .../templates/steps/perf-send-to-helix.yml | 66 + eng/common/templates/steps/run-on-unix.yml | 7 + eng/common/templates/steps/run-on-windows.yml | 7 + .../steps/run-script-ifequalelse.yml | 33 + eng/common/templates/steps/send-to-helix.yml | 91 + eng/common/templates/steps/telemetry-end.yml | 102 + .../templates/steps/telemetry-start.yml | 241 ++ eng/common/tools.ps1 | 587 ++++ eng/common/tools.sh | 394 +++ {build => eng}/key.snk | Bin global.json | 10 + korebuild-lock.txt | 2 - korebuild.json | 4 - run.cmd | 2 - run.ps1 | 196 -- run.sh | 231 -- samples/Mvc.Client/Mvc.Client.csproj | 17 +- samples/Mvc.Client/Startup.cs | 10 +- samples/Mvc.Client/web.config | 9 +- .../Controllers/AuthorizationController.cs | 214 +- .../Mvc.Server/Controllers/ErrorController.cs | 5 +- .../Controllers/ResourceController.cs | 4 +- .../Controllers/UserinfoController.cs | 15 +- samples/Mvc.Server/Mvc.Server.csproj | 27 +- samples/Mvc.Server/Startup.cs | 181 +- .../Authorization/AuthorizeViewModel.cs | 5 +- .../Authorization/LogoutViewModel.cs | 5 +- samples/Mvc.Server/Views/Account/Login.cshtml | 1 - .../Views/Authorization/Authorize.cshtml | 9 +- .../Views/Authorization/Logout.cshtml | 5 +- .../Views/Manage/ManageLogins.cshtml | 1 - samples/Mvc.Server/web.config | 9 +- .../OpenIddict.Extensions.csproj | 2 - .../OpenIddictAuthorizationDescriptor.cs | 12 +- .../IOpenIddictAuthorizationManager.cs | 9 +- .../OpenIddict.Abstractions.csproj | 15 +- .../OpenIddictBuilder.cs | 9 +- .../OpenIddictConstants.cs | 115 +- .../OpenIddictExtensions.cs | 2 +- .../Primitives/OpenIddictConverter.cs | 142 + .../Primitives/OpenIddictExtensions.cs | 1772 +++++++++++ .../Primitives/OpenIddictMessage.cs | 348 +++ .../Primitives/OpenIddictParameter.cs | 477 +++ .../Primitives/OpenIddictRequest.cs | 420 +++ .../Primitives/OpenIddictResponse.cs | 168 ++ .../OpenIddict.AspNetCore.csproj | 17 + .../OpenIddictAuthorizationManager.cs | 27 +- .../Managers/OpenIddictScopeManager.cs | 2 +- .../Managers/OpenIddictTokenManager.cs | 6 +- src/OpenIddict.Core/OpenIddict.Core.csproj | 13 +- src/OpenIddict.Core/OpenIddictCoreBuilder.cs | 9 +- .../OpenIddict.EntityFramework.Models.csproj | 5 +- .../OpenIddict.EntityFramework.csproj | 7 +- .../OpenIddictEntityFrameworkBuilder.cs | 9 +- ...enIddict.EntityFrameworkCore.Models.csproj | 7 +- .../OpenIddict.EntityFrameworkCore.csproj | 9 +- .../OpenIddictEntityFrameworkCoreBuilder.cs | 9 +- ...OpenIddictEntityFrameworkCoreExtensions.cs | 2 - .../Stores/OpenIddictApplicationStore.cs | 23 +- .../Stores/OpenIddictAuthorizationStore.cs | 64 +- .../Stores/OpenIddictScopeStore.cs | 17 +- .../Stores/OpenIddictTokenStore.cs | 75 +- .../OpenIddict.MongoDb.Models.csproj | 7 +- .../OpenIddict.MongoDb.csproj | 7 +- .../OpenIddictMongoDbBuilder.cs | 9 +- .../Stores/OpenIddictApplicationStore.cs | 1 - .../Stores/OpenIddictScopeStore.cs | 1 - .../Stores/OpenIddictTokenStore.cs | 1 - src/OpenIddict.Mvc/OpenIddict.Mvc.csproj | 24 - src/OpenIddict.Mvc/OpenIddictMvcBinder.cs | 97 - .../OpenIddictMvcBinderProvider.cs | 42 - .../OpenIddictMvcConfiguration.cs | 46 - src/OpenIddict.Mvc/OpenIddictMvcExtensions.cs | 68 - src/OpenIddict.Mvc/OpenIddictMvcOptions.cs | 24 - .../OpenIddict.NHibernate.Models.csproj | 7 +- .../OpenIddict.NHibernate.csproj | 7 +- .../OpenIddictNHibernateBuilder.cs | 9 +- .../OpenIddictApplicationStoreResolver.cs | 1 - .../OpenIddictAuthorizationStoreResolver.cs | 1 - .../Resolvers/OpenIddictScopeStoreResolver.cs | 1 - .../Resolvers/OpenIddictTokenStoreResolver.cs | 1 - src/OpenIddict.Owin/OpenIddict.Owin.csproj | 17 + .../OpenIddict.Server.AspNetCore.csproj | 25 + .../OpenIddictServerAspNetCoreBuilder.cs | 160 + ...OpenIddictServerAspNetCoreConfiguration.cs | 101 + .../OpenIddictServerAspNetCoreConstants.cs | 27 + .../OpenIddictServerAspNetCoreDefaults.cs} | 7 +- .../OpenIddictServerAspNetCoreExtensions.cs | 94 + .../OpenIddictServerAspNetCoreFeature.cs | 20 + .../OpenIddictServerAspNetCoreHandler.cs | 396 +++ ...penIddictServerAspNetCoreHandlerFilters.cs | 189 ++ ...ServerAspNetCoreHandlers.Authentication.cs | 793 +++++ ...ddictServerAspNetCoreHandlers.Discovery.cs | 108 + ...IddictServerAspNetCoreHandlers.Exchange.cs | 84 + ...tServerAspNetCoreHandlers.Serialization.cs | 160 + .../OpenIddictServerAspNetCoreHandlers.cs | 684 +++++ .../OpenIddictServerAspNetCoreHelpers.cs | 92 + .../OpenIddictServerAspNetCoreOptions.cs | 84 + .../OpenIddict.Server.DataProtection.csproj | 21 + .../OpenIddictServerDataProtectionBuilder.cs} | 55 +- ...IddictServerDataProtectionConfiguration.cs | 59 + ...penIddictServerDataProtectionExtensions.cs | 82 + ...ddictServerDataProtectionHandlerFilters.cs | 43 + ...verDataProtectionHandlers.Serialization.cs | 812 +++++ .../OpenIddictServerDataProtectionHandlers.cs | 15 + .../OpenIddictServerDataProtectionOptions.cs | 31 + .../OpenIddict.Server.Owin.csproj | 24 + .../OpenIddictServerOwinBuilder.cs | 152 + .../OpenIddictServerOwinConfiguration.cs | 55 + .../OpenIddictServerOwinConstants.cs | 27 + .../OpenIddictServerOwinDefaults.cs | 21 + .../OpenIddictServerOwinExtensions.cs | 93 + .../OpenIddictServerOwinHandler.cs | 398 +++ .../OpenIddictServerOwinHandlerFilters.cs | 166 ++ ...IddictServerOwinHandlers.Authentication.cs | 724 +++++ .../OpenIddictServerOwinHandlers.Discovery.cs | 108 + .../OpenIddictServerOwinHandlers.Exchange.cs | 75 + ...nIddictServerOwinHandlers.Serialization.cs | 160 + .../OpenIddictServerOwinHandlers.cs | 652 ++++ .../OpenIddictServerOwinHelpers.cs | 110 + .../OpenIddictServerOwinMiddleware.cs | 51 + .../OpenIddictServerOwinMiddlewareFactory.cs | 80 + .../OpenIddictServerOwinOptions.cs | 84 + .../IOpenIddictServerEventDispatcher.cs | 26 - ...Handler.cs => IOpenIddictServerHandler.cs} | 14 +- ...t.cs => IOpenIddictServerHandlerFilter.cs} | 12 +- .../IOpenIddictServerProvider.cs | 18 + .../OpenIddict.Server.csproj | 28 +- .../OpenIddictServerBuilder.cs | 1375 +++++++-- .../OpenIddictServerConfiguration.cs | 294 +- .../OpenIddictServerEndpointType.cs | 59 + .../OpenIddictServerEvent.cs | 30 - .../OpenIddictServerEventDispatcher.cs | 52 - .../OpenIddictServerEventHandler.cs | 40 - .../OpenIddictServerEventState.cs | 33 - .../OpenIddictServerEvents.Authentication.cs | 150 + .../OpenIddictServerEvents.Discovery.cs | 276 ++ .../OpenIddictServerEvents.Exchange.cs | 94 + .../OpenIddictServerEvents.Introspection.cs | 174 ++ .../OpenIddictServerEvents.Revocation.cs | 99 + .../OpenIddictServerEvents.Serialization.cs | 236 ++ .../OpenIddictServerEvents.Session.cs | 117 + .../OpenIddictServerEvents.Userinfo.cs | 183 ++ .../OpenIddictServerEvents.cs | 593 ++-- .../OpenIddictServerExtensions.cs | 59 +- .../OpenIddictServerHandler.cs | 38 +- .../OpenIddictServerHandlerDescriptor.cs | 199 ++ .../OpenIddictServerHandlerFilters.cs | 194 ++ ...OpenIddictServerHandlers.Authentication.cs | 1613 ++++++++++ .../OpenIddictServerHandlers.Discovery.cs | 1496 ++++++++++ .../OpenIddictServerHandlers.Exchange.cs | 1752 +++++++++++ .../OpenIddictServerHandlers.Serialization.cs | 635 ++++ .../OpenIddictServerHandlers.cs | 815 +++++ .../OpenIddictServerHelpers.cs | 89 - .../OpenIddictServerOptions.cs | 193 +- ...OpenIddictServerProvider.Authentication.cs | 547 ---- .../OpenIddictServerProvider.Discovery.cs | 71 - .../OpenIddictServerProvider.Exchange.cs | 446 --- .../OpenIddictServerProvider.Helpers.cs | 645 ---- .../OpenIddictServerProvider.Introspection.cs | 182 -- .../OpenIddictServerProvider.Revocation.cs | 240 -- .../OpenIddictServerProvider.Serialization.cs | 169 -- .../OpenIddictServerProvider.Session.cs | 252 -- .../OpenIddictServerProvider.Userinfo.cs | 42 - .../OpenIddictServerProvider.cs | 267 +- .../OpenIddictServerTransaction.cs | 50 + .../Properties/AssemblyInfo.cs | 9 - .../IOpenIddictValidationEvent.cs | 13 - .../IOpenIddictValidationEventDispatcher.cs | 25 - .../IOpenIddictValidationEventHandler.cs | 28 - .../OpenIddict.Validation.csproj | 29 - .../OpenIddictValidationBuilder.cs | 228 -- .../OpenIddictValidationConfiguration.cs | 134 - .../OpenIddictValidationDefaults.cs | 22 - .../OpenIddictValidationEvent.cs | 30 - .../OpenIddictValidationEventDispatcher.cs | 52 - .../OpenIddictValidationEventHandler.cs | 40 - .../OpenIddictValidationEventState.cs | 33 - .../OpenIddictValidationEvents.cs | 77 - .../OpenIddictValidationExtensions.cs | 88 - .../OpenIddictValidationHandler.cs | 33 - .../OpenIddictValidationHelpers.cs | 80 - .../OpenIddictValidationOptions.cs | 36 - .../OpenIddictValidationProvider.cs | 184 -- .../Properties/AssemblyInfo.cs | 9 - src/OpenIddict/OpenIddict.csproj | 10 +- .../OpenIddict.Abstractions.Tests.csproj | 15 +- .../OpenIddict.Core.Tests.csproj | 15 +- .../OpenIddict.EntityFramework.Tests.csproj | 14 +- ...penIddictEntityFrameworkExtensionsTests.cs | 1 - ...penIddict.EntityFrameworkCore.Tests.csproj | 12 +- ...ddictEntityFrameworkCoreExtensionsTests.cs | 1 - .../OpenIddict.MongoDb.Tests.csproj | 15 +- .../OpenIddictMongoDbExtensionsTests.cs | 1 - ...OpenIddictApplicationStoreResolverTests.cs | 1 - ...enIddictAuthorizationStoreResolverTests.cs | 1 - .../OpenIddictScopeStoreResolverTests.cs | 1 - .../OpenIddictTokenStoreResolverTests.cs | 1 - .../OpenIddict.Mvc.Tests.csproj | 27 - .../OpenIddictMvcBuilderTests.cs | 56 - .../OpenIddictMvcConfigurationTests.cs | 83 - .../OpenIddictMvcExtensionsTests.cs | 59 - .../OpenIddictMvcModelBinderProviderTests.cs | 57 - .../OpenIddictMvcModelBinderTests.cs | 230 -- .../OpenIddict.NHibernate.Tests.csproj | 15 +- .../OpenIddictNHibernateExtensionsTests.cs | 1 - test/OpenIddict.Server.Tests/Certificate.pfx | Bin 2482 -> 0 bytes .../OpenIddict.Server.Tests.csproj | 43 - .../OpenIddictServerBuilderTests.cs | 942 ------ .../OpenIddictServerConfigurationTests.cs | 458 --- .../OpenIddictServerEventDispatcherTests.cs | 92 - .../OpenIddictServerEventHandlerTests.cs | 60 - .../OpenIddictServerExtensionsTests.cs | 249 -- ...ddictServerProviderTests.Authentication.cs | 1060 ------- ...OpenIddictServerProviderTests.Discovery.cs | 240 -- .../OpenIddictServerProviderTests.Exchange.cs | 2174 -------------- ...IddictServerProviderTests.Introspection.cs | 789 ----- ...penIddictServerProviderTests.Revocation.cs | 529 ---- ...IddictServerProviderTests.Serialization.cs | 2610 ----------------- .../OpenIddictServerProviderTests.Session.cs | 289 -- .../OpenIddictServerProviderTests.Userinfo.cs | 35 - .../OpenIddictServerProviderTests.cs | 1767 ----------- .../OpenIddict.Validation.Tests.csproj | 29 - .../OpenIddictValidationBuilderTests.cs | 259 -- .../OpenIddictValidationConfigurationTests.cs | 168 -- ...penIddictValidationEventDispatcherTests.cs | 92 - .../OpenIddictValidationEventHandlerTests.cs | 60 - .../OpenIddictValidationExtensionsTests.cs | 211 -- .../OpenIddictValidationProviderTests.cs | 738 ----- 350 files changed, 30487 insertions(+), 20146 deletions(-) create mode 100644 Build.cmd create mode 100644 Directory.Build.props create mode 100644 Directory.Build.targets delete mode 100644 build.cmd delete mode 100644 build.ps1 delete mode 100644 build/common.props delete mode 100644 build/dependencies.props delete mode 100644 build/packages.props delete mode 100644 build/repo.props delete mode 100644 build/version.props create mode 100644 eng/Signing.props create mode 100644 eng/Version.Details.xml create mode 100644 eng/Versions.props create mode 100644 eng/common/CIBuild.cmd create mode 100644 eng/common/CheckSymbols.ps1 create mode 100644 eng/common/PSScriptAnalyzerSettings.psd1 create mode 100644 eng/common/PublishToPackageFeed.proj create mode 100644 eng/common/PublishToSymbolServers.proj create mode 100644 eng/common/README.md create mode 100644 eng/common/SigningValidation.proj create mode 100644 eng/common/SourceLinkValidation.ps1 create mode 100644 eng/common/build.ps1 create mode 100755 eng/common/build.sh create mode 100755 eng/common/cibuild.sh create mode 100644 eng/common/cross/android/arm/toolchain.cmake create mode 100644 eng/common/cross/android/arm64/toolchain.cmake create mode 100644 eng/common/cross/arm/sources.list.bionic create mode 100644 eng/common/cross/arm/sources.list.jessie create mode 100644 eng/common/cross/arm/sources.list.trusty create mode 100644 eng/common/cross/arm/sources.list.xenial create mode 100644 eng/common/cross/arm/sources.list.zesty create mode 100644 eng/common/cross/arm/trusty-lttng-2.4.patch create mode 100644 eng/common/cross/arm/trusty.patch create mode 100644 eng/common/cross/arm64/sources.list.bionic create mode 100644 eng/common/cross/arm64/sources.list.buster create mode 100644 eng/common/cross/arm64/sources.list.stretch create mode 100644 eng/common/cross/arm64/sources.list.trusty create mode 100644 eng/common/cross/arm64/sources.list.xenial create mode 100644 eng/common/cross/arm64/sources.list.zesty create mode 100644 eng/common/cross/armel/sources.list.jessie create mode 100755 eng/common/cross/armel/tizen-build-rootfs.sh create mode 100755 eng/common/cross/armel/tizen-fetch.sh create mode 100644 eng/common/cross/armel/tizen/tizen-dotnet.ks create mode 100644 eng/common/cross/armel/tizen/tizen.patch create mode 100755 eng/common/cross/build-android-rootfs.sh create mode 100755 eng/common/cross/build-rootfs.sh create mode 100644 eng/common/cross/toolchain.cmake create mode 100644 eng/common/cross/x86/sources.list.bionic create mode 100644 eng/common/cross/x86/sources.list.trusty create mode 100644 eng/common/cross/x86/sources.list.xenial create mode 100644 eng/common/darc-init.ps1 create mode 100755 eng/common/darc-init.sh create mode 100644 eng/common/dotnet-install.cmd create mode 100644 eng/common/dotnet-install.ps1 create mode 100755 eng/common/dotnet-install.sh create mode 100644 eng/common/generate-graph-files.ps1 create mode 100644 eng/common/helixpublish.proj create mode 100644 eng/common/init-tools-native.cmd create mode 100644 eng/common/init-tools-native.ps1 create mode 100755 eng/common/init-tools-native.sh create mode 100644 eng/common/internal-feed-operations.ps1 create mode 100755 eng/common/internal-feed-operations.sh create mode 100644 eng/common/internal/Directory.Build.props create mode 100644 eng/common/internal/Tools.csproj create mode 100644 eng/common/msbuild.ps1 create mode 100755 eng/common/msbuild.sh create mode 100644 eng/common/native/CommonLibrary.psm1 create mode 100755 eng/common/native/common-library.sh create mode 100755 eng/common/native/install-cmake-test.sh create mode 100755 eng/common/native/install-cmake.sh create mode 100644 eng/common/native/install-tool.ps1 create mode 100644 eng/common/performance/perfhelixpublish.proj create mode 100644 eng/common/performance/performance-setup.ps1 create mode 100755 eng/common/performance/performance-setup.sh create mode 100644 eng/common/pipeline-logging-functions.ps1 create mode 100755 eng/common/pipeline-logging-functions.sh create mode 100644 eng/common/post-build/darc-gather-drop.ps1 create mode 100644 eng/common/post-build/dotnetsymbol-init.ps1 create mode 100644 eng/common/post-build/nuget-validation.ps1 create mode 100644 eng/common/post-build/post-build-utils.ps1 create mode 100644 eng/common/post-build/promote-build.ps1 create mode 100644 eng/common/post-build/setup-maestro-vars.ps1 create mode 100644 eng/common/post-build/sourcelink-cli-init.ps1 create mode 100644 eng/common/post-build/sourcelink-validation.ps1 create mode 100644 eng/common/post-build/symbols-validation.ps1 create mode 100644 eng/common/post-build/trigger-subscriptions.ps1 create mode 100644 eng/common/sdk-task.ps1 create mode 100644 eng/common/sdl/NuGet.config create mode 100644 eng/common/sdl/execute-all-sdl-tools.ps1 create mode 100644 eng/common/sdl/extract-artifact-packages.ps1 create mode 100644 eng/common/sdl/init-sdl.ps1 create mode 100644 eng/common/sdl/packages.config create mode 100644 eng/common/sdl/push-gdn.ps1 create mode 100644 eng/common/sdl/run-sdl.ps1 create mode 100644 eng/common/templates/job/execute-sdl.yml create mode 100644 eng/common/templates/job/generate-graph-files.yml create mode 100644 eng/common/templates/job/job.yml create mode 100644 eng/common/templates/job/performance.yml create mode 100644 eng/common/templates/job/publish-build-assets.yml create mode 100644 eng/common/templates/jobs/jobs.yml create mode 100644 eng/common/templates/phases/base.yml create mode 100644 eng/common/templates/phases/publish-build-assets.yml create mode 100644 eng/common/templates/post-build/channels/internal-servicing.yml create mode 100644 eng/common/templates/post-build/channels/netcore-dev-5.yml create mode 100644 eng/common/templates/post-build/channels/netcore-tools-latest.yml create mode 100644 eng/common/templates/post-build/channels/public-dev-release.yml create mode 100644 eng/common/templates/post-build/channels/public-release.yml create mode 100644 eng/common/templates/post-build/channels/public-validation-release.yml create mode 100644 eng/common/templates/post-build/common-variables.yml create mode 100644 eng/common/templates/post-build/darc-gather-drop.yml create mode 100644 eng/common/templates/post-build/post-build.yml create mode 100644 eng/common/templates/post-build/promote-build.yml create mode 100644 eng/common/templates/post-build/setup-maestro-vars.yml create mode 100644 eng/common/templates/post-build/trigger-subscription.yml create mode 100644 eng/common/templates/steps/build-reason.yml create mode 100644 eng/common/templates/steps/perf-send-to-helix.yml create mode 100644 eng/common/templates/steps/run-on-unix.yml create mode 100644 eng/common/templates/steps/run-on-windows.yml create mode 100644 eng/common/templates/steps/run-script-ifequalelse.yml create mode 100644 eng/common/templates/steps/send-to-helix.yml create mode 100644 eng/common/templates/steps/telemetry-end.yml create mode 100644 eng/common/templates/steps/telemetry-start.yml create mode 100644 eng/common/tools.ps1 create mode 100755 eng/common/tools.sh rename {build => eng}/key.snk (100%) create mode 100644 global.json delete mode 100644 korebuild-lock.txt delete mode 100644 korebuild.json delete mode 100644 run.cmd delete mode 100644 run.ps1 delete mode 100755 run.sh create mode 100644 src/OpenIddict.Abstractions/Primitives/OpenIddictConverter.cs create mode 100644 src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs create mode 100644 src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs create mode 100644 src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs create mode 100644 src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs create mode 100644 src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs create mode 100644 src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj delete mode 100644 src/OpenIddict.Mvc/OpenIddict.Mvc.csproj delete mode 100644 src/OpenIddict.Mvc/OpenIddictMvcBinder.cs delete mode 100644 src/OpenIddict.Mvc/OpenIddictMvcBinderProvider.cs delete mode 100644 src/OpenIddict.Mvc/OpenIddictMvcConfiguration.cs delete mode 100644 src/OpenIddict.Mvc/OpenIddictMvcExtensions.cs delete mode 100644 src/OpenIddict.Mvc/OpenIddictMvcOptions.cs create mode 100644 src/OpenIddict.Owin/OpenIddict.Owin.csproj create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddict.Server.AspNetCore.csproj create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs rename src/{OpenIddict.Server/OpenIddictServerDefaults.cs => OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreDefaults.cs} (71%) create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreFeature.cs create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Discovery.cs create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Serialization.cs create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHelpers.cs create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs create mode 100644 src/OpenIddict.Server.DataProtection/OpenIddict.Server.DataProtection.csproj rename src/{OpenIddict.Mvc/OpenIddictMvcBuilder.cs => OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs} (54%) create mode 100644 src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConfiguration.cs create mode 100644 src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs create mode 100644 src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlerFilters.cs create mode 100644 src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Serialization.cs create mode 100644 src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs create mode 100644 src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddict.Server.Owin.csproj create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinDefaults.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Discovery.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Serialization.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinHelpers.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddlewareFactory.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs delete mode 100644 src/OpenIddict.Server/IOpenIddictServerEventDispatcher.cs rename src/OpenIddict.Server/{IOpenIddictServerEventHandler.cs => IOpenIddictServerHandler.cs} (57%) rename src/OpenIddict.Server/{IOpenIddictServerEvent.cs => IOpenIddictServerHandlerFilter.cs} (50%) create mode 100644 src/OpenIddict.Server/IOpenIddictServerProvider.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerEndpointType.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerEvent.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerEventDispatcher.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerEventHandler.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerEventState.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerEvents.Session.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerHandlerDescriptor.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerHandlers.Serialization.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerHandlers.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerHelpers.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerProvider.Authentication.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerProvider.Discovery.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerProvider.Exchange.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerProvider.Helpers.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerProvider.Introspection.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerProvider.Revocation.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerProvider.Serialization.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerProvider.Session.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerProvider.Userinfo.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerTransaction.cs delete mode 100644 src/OpenIddict.Server/Properties/AssemblyInfo.cs delete mode 100644 src/OpenIddict.Validation/IOpenIddictValidationEvent.cs delete mode 100644 src/OpenIddict.Validation/IOpenIddictValidationEventDispatcher.cs delete mode 100644 src/OpenIddict.Validation/IOpenIddictValidationEventHandler.cs delete mode 100644 src/OpenIddict.Validation/OpenIddict.Validation.csproj delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationBuilder.cs delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationDefaults.cs delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationEvent.cs delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationEventDispatcher.cs delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationEventHandler.cs delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationEventState.cs delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationEvents.cs delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationExtensions.cs delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationHandler.cs delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationHelpers.cs delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationOptions.cs delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationProvider.cs delete mode 100644 src/OpenIddict.Validation/Properties/AssemblyInfo.cs delete mode 100644 test/OpenIddict.Mvc.Tests/OpenIddict.Mvc.Tests.csproj delete mode 100644 test/OpenIddict.Mvc.Tests/OpenIddictMvcBuilderTests.cs delete mode 100644 test/OpenIddict.Mvc.Tests/OpenIddictMvcConfigurationTests.cs delete mode 100644 test/OpenIddict.Mvc.Tests/OpenIddictMvcExtensionsTests.cs delete mode 100644 test/OpenIddict.Mvc.Tests/OpenIddictMvcModelBinderProviderTests.cs delete mode 100644 test/OpenIddict.Mvc.Tests/OpenIddictMvcModelBinderTests.cs delete mode 100644 test/OpenIddict.Server.Tests/Certificate.pfx delete mode 100644 test/OpenIddict.Server.Tests/OpenIddict.Server.Tests.csproj delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerConfigurationTests.cs delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerEventDispatcherTests.cs delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerEventHandlerTests.cs delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Authentication.cs delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Discovery.cs delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Exchange.cs delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Introspection.cs delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Revocation.cs delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Serialization.cs delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Session.cs delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Userinfo.cs delete mode 100644 test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.cs delete mode 100755 test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj delete mode 100644 test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs delete mode 100644 test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs delete mode 100644 test/OpenIddict.Validation.Tests/OpenIddictValidationEventDispatcherTests.cs delete mode 100644 test/OpenIddict.Validation.Tests/OpenIddictValidationEventHandlerTests.cs delete mode 100644 test/OpenIddict.Validation.Tests/OpenIddictValidationExtensionsTests.cs delete mode 100644 test/OpenIddict.Validation.Tests/OpenIddictValidationProviderTests.cs diff --git a/.travis.yml b/.travis.yml index 7f87ae830..5f207f5e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,4 +22,4 @@ osx_image: xcode8.2 before_install: - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi script: - - ./build.sh \ No newline at end of file + - ./eng/common/cibuild.sh -configuration Release -prepareMachine \ No newline at end of file diff --git a/Build.cmd b/Build.cmd new file mode 100644 index 000000000..4afad0471 --- /dev/null +++ b/Build.cmd @@ -0,0 +1,3 @@ +@echo off +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0eng\common\Build.ps1""" -restore -build %*" +exit /b %ErrorLevel% diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..0d2bbcb47 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,49 @@ + + + + + + preview + $(NoWarn);CS1591 + true + true + $(MSBuildThisFileDirectory)\eng\key.snk + true + true + + + + Kévin Chalet + OpenIddict + © Kévin Chalet. All rights reserved. + https://avatars3.githubusercontent.com/u/13908567?s=64 + https://github.com/openiddict/openiddict-core + Apache-2.0 + authentication;jwt;openidconnect;openiddict;security + git + git://github.com/openiddict/openiddict-core + + + + $([MSBuild]::MakeRelative($(RepoRoot), $(MSBuildProjectDirectory))) + false + + + + true + true + true + true + true + + + + + + + + + + + + diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 000000000..f90f13fc3 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,5 @@ + + + + + diff --git a/LICENSE.md b/LICENSE.md index d9a10c0d8..d64569567 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -174,3 +175,28 @@ of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NuGet.config b/NuGet.config index ae8aea9e0..6f84f0328 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,7 +1,9 @@ - - + + + + \ No newline at end of file diff --git a/OpenIddict.sln b/OpenIddict.sln index 17758e909..ddddeacc2 100644 --- a/OpenIddict.sln +++ b/OpenIddict.sln @@ -1,18 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.12 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28606.126 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{19E887E7-7B1C-47E3-8F0C-06B007B2CE05}" - ProjectSection(SolutionItems) = preProject - build\common.props = build\common.props - build\dependencies.props = build\dependencies.props - build\key.snk = build\key.snk - build\packages.props = build\packages.props - build\repo.props = build\repo.props - build\version.props = build\version.props - EndProjectSection -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D544447C-D701-46BB-9A5B-C76C612A596B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{F47D1283-0EE9-4728-8026-58405C29B786}" @@ -29,14 +19,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFrameworkC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Core", "src\OpenIddict.Core\OpenIddict.Core.csproj", "{E60CF8CA-6313-4359-BE43-AFCBB927EA30}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Mvc", "src\OpenIddict.Mvc\OpenIddict.Mvc.csproj", "{6EB5B6A9-4ED8-401D-A673-FD513F256AAE}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Core.Tests", "test\OpenIddict.Core.Tests\OpenIddict.Core.Tests.csproj", "{A892B3DE-1A02-48D6-993B-DD3DFCAC84C7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFrameworkCore.Tests", "test\OpenIddict.EntityFrameworkCore.Tests\OpenIddict.EntityFrameworkCore.Tests.csproj", "{7831F17A-DF0B-42EC-841B-065A9B5BD786}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Mvc.Tests", "test\OpenIddict.Mvc.Tests\OpenIddict.Mvc.Tests.csproj", "{8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFramework.Models", "src\OpenIddict.EntityFramework.Models\OpenIddict.EntityFramework.Models.csproj", "{0102A6CC-41A6-4B34-B49E-65AFE95882BB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFramework", "src\OpenIddict.EntityFramework\OpenIddict.EntityFramework.csproj", "{BF42CC6C-0B56-4F66-9866-18B8393F3C06}" @@ -45,14 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFramework. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server", "src\OpenIddict.Server\OpenIddict.Server.csproj", "{21A7F241-CBE7-4F5C-9787-F2C50D135AEA}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server.Tests", "test\OpenIddict.Server.Tests\OpenIddict.Server.Tests.csproj", "{07B02B98-8A68-432D-A932-48E6D52B221A}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Abstractions", "src\OpenIddict.Abstractions\OpenIddict.Abstractions.csproj", "{886A16DA-C9CF-4979-9B38-D06DF8A714B6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Validation.Tests", "test\OpenIddict.Validation.Tests\OpenIddict.Validation.Tests.csproj", "{F470E734-F4B6-4355-AF32-53412B619E41}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Validation", "src\OpenIddict.Validation\OpenIddict.Validation.csproj", "{6AB8F9E7-47F8-4A40-837F-C8753362AF54}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFrameworkCore.Models", "src\OpenIddict.EntityFrameworkCore.Models\OpenIddict.EntityFrameworkCore.Models.csproj", "{B5371534-4C33-41FA-B3D3-7D70D632DB15}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.MongoDb", "src\OpenIddict.MongoDb\OpenIddict.MongoDb.csproj", "{BACF1DD4-8390-48D4-BD9B-DA1EC00C1F98}" @@ -71,7 +51,34 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.NHibernate", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.NHibernate.Models", "src\OpenIddict.NHibernate.Models\OpenIddict.NHibernate.Models.csproj", "{22882DA6-6A5F-4E48-8BDC-7248B1DE5D14}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.NHibernate.Tests", "test\OpenIddict.NHibernate.Tests\OpenIddict.NHibernate.Tests.csproj", "{B99BCBEC-9771-4C68-96E2-1A54E9BC432D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.NHibernate.Tests", "test\OpenIddict.NHibernate.Tests\OpenIddict.NHibernate.Tests.csproj", "{B99BCBEC-9771-4C68-96E2-1A54E9BC432D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server.AspNetCore", "src\OpenIddict.Server.AspNetCore\OpenIddict.Server.AspNetCore.csproj", "{9C86897B-DB77-4D85-AC74-A6768F97098C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server.Owin", "src\OpenIddict.Server.Owin\OpenIddict.Server.Owin.csproj", "{93FFD5DC-EC92-48AD-B638-613205D89BD7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.AspNetCore", "src\OpenIddict.AspNetCore\OpenIddict.AspNetCore.csproj", "{97A59757-A249-4FCF-B042-BF425E117706}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server.DataProtection", "src\OpenIddict.Server.DataProtection\OpenIddict.Server.DataProtection.csproj", "{1BD05607-C964-477C-A26A-73F01F7BB06E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Owin", "src\OpenIddict.Owin\OpenIddict.Owin.csproj", "{C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{6E8E862C-3F26-47D9-9C20-E3E87FD7CDFC}" + ProjectSection(SolutionItems) = preProject + eng\key.snk = eng\key.snk + eng\Signing.props = eng\Signing.props + eng\Version.Details.xml = eng\Version.Details.xml + eng\Versions.props = eng\Versions.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{F6F3C8E0-BBD7-41A5-9E20-385DC185DBC0}" + ProjectSection(SolutionItems) = preProject + .travis.yml = .travis.yml + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + global.json = global.json + NuGet.config = NuGet.config + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -99,10 +106,6 @@ Global {E60CF8CA-6313-4359-BE43-AFCBB927EA30}.Debug|Any CPU.Build.0 = Debug|Any CPU {E60CF8CA-6313-4359-BE43-AFCBB927EA30}.Release|Any CPU.ActiveCfg = Release|Any CPU {E60CF8CA-6313-4359-BE43-AFCBB927EA30}.Release|Any CPU.Build.0 = Release|Any CPU - {6EB5B6A9-4ED8-401D-A673-FD513F256AAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6EB5B6A9-4ED8-401D-A673-FD513F256AAE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6EB5B6A9-4ED8-401D-A673-FD513F256AAE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6EB5B6A9-4ED8-401D-A673-FD513F256AAE}.Release|Any CPU.Build.0 = Release|Any CPU {A892B3DE-1A02-48D6-993B-DD3DFCAC84C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A892B3DE-1A02-48D6-993B-DD3DFCAC84C7}.Debug|Any CPU.Build.0 = Debug|Any CPU {A892B3DE-1A02-48D6-993B-DD3DFCAC84C7}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -111,10 +114,6 @@ Global {7831F17A-DF0B-42EC-841B-065A9B5BD786}.Debug|Any CPU.Build.0 = Debug|Any CPU {7831F17A-DF0B-42EC-841B-065A9B5BD786}.Release|Any CPU.ActiveCfg = Release|Any CPU {7831F17A-DF0B-42EC-841B-065A9B5BD786}.Release|Any CPU.Build.0 = Release|Any CPU - {8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA}.Release|Any CPU.Build.0 = Release|Any CPU {0102A6CC-41A6-4B34-B49E-65AFE95882BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0102A6CC-41A6-4B34-B49E-65AFE95882BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {0102A6CC-41A6-4B34-B49E-65AFE95882BB}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -131,22 +130,10 @@ Global {21A7F241-CBE7-4F5C-9787-F2C50D135AEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {21A7F241-CBE7-4F5C-9787-F2C50D135AEA}.Release|Any CPU.ActiveCfg = Release|Any CPU {21A7F241-CBE7-4F5C-9787-F2C50D135AEA}.Release|Any CPU.Build.0 = Release|Any CPU - {07B02B98-8A68-432D-A932-48E6D52B221A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07B02B98-8A68-432D-A932-48E6D52B221A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07B02B98-8A68-432D-A932-48E6D52B221A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07B02B98-8A68-432D-A932-48E6D52B221A}.Release|Any CPU.Build.0 = Release|Any CPU {886A16DA-C9CF-4979-9B38-D06DF8A714B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {886A16DA-C9CF-4979-9B38-D06DF8A714B6}.Debug|Any CPU.Build.0 = Debug|Any CPU {886A16DA-C9CF-4979-9B38-D06DF8A714B6}.Release|Any CPU.ActiveCfg = Release|Any CPU {886A16DA-C9CF-4979-9B38-D06DF8A714B6}.Release|Any CPU.Build.0 = Release|Any CPU - {F470E734-F4B6-4355-AF32-53412B619E41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F470E734-F4B6-4355-AF32-53412B619E41}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F470E734-F4B6-4355-AF32-53412B619E41}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F470E734-F4B6-4355-AF32-53412B619E41}.Release|Any CPU.Build.0 = Release|Any CPU - {6AB8F9E7-47F8-4A40-837F-C8753362AF54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6AB8F9E7-47F8-4A40-837F-C8753362AF54}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6AB8F9E7-47F8-4A40-837F-C8753362AF54}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6AB8F9E7-47F8-4A40-837F-C8753362AF54}.Release|Any CPU.Build.0 = Release|Any CPU {B5371534-4C33-41FA-B3D3-7D70D632DB15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B5371534-4C33-41FA-B3D3-7D70D632DB15}.Debug|Any CPU.Build.0 = Debug|Any CPU {B5371534-4C33-41FA-B3D3-7D70D632DB15}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -183,6 +170,26 @@ Global {B99BCBEC-9771-4C68-96E2-1A54E9BC432D}.Debug|Any CPU.Build.0 = Debug|Any CPU {B99BCBEC-9771-4C68-96E2-1A54E9BC432D}.Release|Any CPU.ActiveCfg = Release|Any CPU {B99BCBEC-9771-4C68-96E2-1A54E9BC432D}.Release|Any CPU.Build.0 = Release|Any CPU + {9C86897B-DB77-4D85-AC74-A6768F97098C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C86897B-DB77-4D85-AC74-A6768F97098C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C86897B-DB77-4D85-AC74-A6768F97098C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C86897B-DB77-4D85-AC74-A6768F97098C}.Release|Any CPU.Build.0 = Release|Any CPU + {93FFD5DC-EC92-48AD-B638-613205D89BD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93FFD5DC-EC92-48AD-B638-613205D89BD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93FFD5DC-EC92-48AD-B638-613205D89BD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93FFD5DC-EC92-48AD-B638-613205D89BD7}.Release|Any CPU.Build.0 = Release|Any CPU + {97A59757-A249-4FCF-B042-BF425E117706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97A59757-A249-4FCF-B042-BF425E117706}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97A59757-A249-4FCF-B042-BF425E117706}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97A59757-A249-4FCF-B042-BF425E117706}.Release|Any CPU.Build.0 = Release|Any CPU + {1BD05607-C964-477C-A26A-73F01F7BB06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BD05607-C964-477C-A26A-73F01F7BB06E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BD05607-C964-477C-A26A-73F01F7BB06E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BD05607-C964-477C-A26A-73F01F7BB06E}.Release|Any CPU.Build.0 = Release|Any CPU + {C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -193,18 +200,13 @@ Global {7CBEAFD2-E3D0-4424-9B78-E87AB52327A6} = {F47D1283-0EE9-4728-8026-58405C29B786} {D2450929-ED0E-420D-B475-327924F9701C} = {D544447C-D701-46BB-9A5B-C76C612A596B} {E60CF8CA-6313-4359-BE43-AFCBB927EA30} = {D544447C-D701-46BB-9A5B-C76C612A596B} - {6EB5B6A9-4ED8-401D-A673-FD513F256AAE} = {D544447C-D701-46BB-9A5B-C76C612A596B} {A892B3DE-1A02-48D6-993B-DD3DFCAC84C7} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {7831F17A-DF0B-42EC-841B-065A9B5BD786} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} - {8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {0102A6CC-41A6-4B34-B49E-65AFE95882BB} = {D544447C-D701-46BB-9A5B-C76C612A596B} {BF42CC6C-0B56-4F66-9866-18B8393F3C06} = {D544447C-D701-46BB-9A5B-C76C612A596B} {96325E37-9897-43AC-8408-7B17F58E8788} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {21A7F241-CBE7-4F5C-9787-F2C50D135AEA} = {D544447C-D701-46BB-9A5B-C76C612A596B} - {07B02B98-8A68-432D-A932-48E6D52B221A} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {886A16DA-C9CF-4979-9B38-D06DF8A714B6} = {D544447C-D701-46BB-9A5B-C76C612A596B} - {F470E734-F4B6-4355-AF32-53412B619E41} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} - {6AB8F9E7-47F8-4A40-837F-C8753362AF54} = {D544447C-D701-46BB-9A5B-C76C612A596B} {B5371534-4C33-41FA-B3D3-7D70D632DB15} = {D544447C-D701-46BB-9A5B-C76C612A596B} {BACF1DD4-8390-48D4-BD9B-DA1EC00C1F98} = {D544447C-D701-46BB-9A5B-C76C612A596B} {14C55FB6-9626-4BDE-8961-3BE91DDD6418} = {D544447C-D701-46BB-9A5B-C76C612A596B} @@ -214,6 +216,11 @@ Global {17BFF448-F11F-40D6-B658-BD81B306D2CA} = {D544447C-D701-46BB-9A5B-C76C612A596B} {22882DA6-6A5F-4E48-8BDC-7248B1DE5D14} = {D544447C-D701-46BB-9A5B-C76C612A596B} {B99BCBEC-9771-4C68-96E2-1A54E9BC432D} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} + {9C86897B-DB77-4D85-AC74-A6768F97098C} = {D544447C-D701-46BB-9A5B-C76C612A596B} + {93FFD5DC-EC92-48AD-B638-613205D89BD7} = {D544447C-D701-46BB-9A5B-C76C612A596B} + {97A59757-A249-4FCF-B042-BF425E117706} = {D544447C-D701-46BB-9A5B-C76C612A596B} + {1BD05607-C964-477C-A26A-73F01F7BB06E} = {D544447C-D701-46BB-9A5B-C76C612A596B} + {C3DCEB4E-0980-4C96-8D5E-A4D1970AD4A8} = {D544447C-D701-46BB-9A5B-C76C612A596B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616} diff --git a/README.md b/README.md index a2656983c..a3a27f21f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ [![Build status](https://ci.appveyor.com/api/projects/status/46ofo2eusje0hcw2?svg=true)](https://ci.appveyor.com/project/openiddict/openiddict-core) [![Build status](https://travis-ci.org/openiddict/openiddict-core.svg)](https://travis-ci.org/openiddict/openiddict-core) +> **Warning: this branch contains the OpenIddict 3.0 source code, which is still a work in progress. The 3.0 packages are still experimental and no official binaries are available yet**. +**These packages currently lack many features and haven't been heavily tested: don't use them in any production application**. + ### What's OpenIddict? OpenIddict aims at providing a **simple and easy-to-use solution** to implement an **OpenID Connect server in any ASP.NET Core 1.x or 2.x application**. @@ -40,11 +43,6 @@ with the power to control who can access your API and the information that is ex - [Refresh flow sample](https://github.com/openiddict/openiddict-samples/tree/dev/samples/RefreshFlow) > **Samples for ASP.NET Core 1.x can be found [in the master branch of the samples repository](https://github.com/openiddict/openiddict-samples/tree/master)**. - -## Migrating to OpenIddict RC3? - -**Before updating your application, read [the migration guide](https://openiddict.github.io/openiddict-documentation/guide/migration.html)**. - -------------- ## Getting started @@ -58,8 +56,8 @@ To use OpenIddict, you need to: - **Update your `.csproj` file** to reference the `OpenIddict` packages: ```xml - - + + ``` - **Configure the OpenIddict services** in `Startup.ConfigureServices`: diff --git a/build.cmd b/build.cmd deleted file mode 100644 index c0050bda1..000000000 --- a/build.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@ECHO OFF -PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' default-build %*; exit $LASTEXITCODE" diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index 5bf0e2c11..000000000 --- a/build.ps1 +++ /dev/null @@ -1,67 +0,0 @@ -$ErrorActionPreference = "Stop" - -function DownloadWithRetry([string] $url, [string] $downloadLocation, [int] $retries) -{ - while($true) - { - try - { - Invoke-WebRequest $url -OutFile $downloadLocation - break - } - catch - { - $exceptionMessage = $_.Exception.Message - Write-Host "Failed to download '$url': $exceptionMessage" - if ($retries -gt 0) { - $retries-- - Write-Host "Waiting 10 seconds before retrying. Retries left: $retries" - Start-Sleep -Seconds 10 - - } - else - { - $exception = $_.Exception - throw $exception - } - } - } -} - -cd $PSScriptRoot - -$repoFolder = $PSScriptRoot -$env:REPO_FOLDER = $repoFolder - -$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" -if ($env:KOREBUILD_ZIP) -{ - $koreBuildZip=$env:KOREBUILD_ZIP -} - -$buildFolder = ".build" -$buildFile="$buildFolder\KoreBuild.ps1" - -if (!(Test-Path $buildFolder)) { - Write-Host "Downloading KoreBuild from $koreBuildZip" - - $tempFolder=$env:TEMP + "\KoreBuild-" + [guid]::NewGuid() - New-Item -Path "$tempFolder" -Type directory | Out-Null - - $localZipFile="$tempFolder\korebuild.zip" - - DownloadWithRetry -url $koreBuildZip -downloadLocation $localZipFile -retries 6 - - Add-Type -AssemblyName System.IO.Compression.FileSystem - [System.IO.Compression.ZipFile]::ExtractToDirectory($localZipFile, $tempFolder) - - New-Item -Path "$buildFolder" -Type directory | Out-Null - copy-item "$tempFolder\**\build\*" $buildFolder -Recurse - - # Cleanup - if (Test-Path $tempFolder) { - Remove-Item -Recurse -Force $tempFolder - } -} - -&"$buildFile" @args diff --git a/build.sh b/build.sh index 98a4b2276..8477d5af8 100755 --- a/build.sh +++ b/build.sh @@ -1,8 +1,16 @@ #!/usr/bin/env bash -set -euo pipefail -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source="${BASH_SOURCE[0]}" -# Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs) -chmod +x "$DIR/run.sh"; sync -"$DIR/run.sh" default-build "$@" +# resolve $SOURCE until the file is no longer a symlink +while [[ -h $source ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done + +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" +"$scriptroot/eng/common/build.sh" --build --restore $@ diff --git a/build/common.props b/build/common.props deleted file mode 100644 index 55b585593..000000000 --- a/build/common.props +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - latest - $(NoWarn);CS1591 - true - true - full - pdbonly - portable - $(MSBuildThisFileDirectory)key.snk - true - true - - - - - <_GenerateBindingRedirectsIntermediateAppConfig>$(IntermediateOutputPath)$(TargetFileName).config - - - - diff --git a/build/dependencies.props b/build/dependencies.props deleted file mode 100644 index 94e21b110..000000000 --- a/build/dependencies.props +++ /dev/null @@ -1,26 +0,0 @@ - - - - 2.0.0 - 2.0.0 - 2.0.0 - 4.4.0 - 3.0.2 - 4.4.0 - 6.1.3 - 2018.2.1 - 10.0.2 - 1.0.1 - 1.5.0 - 2.7.0 - 4.7.63 - 2.0.0 - 2.0.0 - 5.2.2 - 2.0.0 - 4.4.0 - 15.3.0 - 2.3.1 - - - diff --git a/build/packages.props b/build/packages.props deleted file mode 100644 index cc33384b4..000000000 --- a/build/packages.props +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - true - true - true - - - - OpenIddict - https://avatars3.githubusercontent.com/u/13908567?s=64 - https://github.com/openiddict/openiddict-core - http://www.apache.org/licenses/LICENSE-2.0.html - git - git://github.com/openiddict/openiddict-core - - - diff --git a/build/repo.props b/build/repo.props deleted file mode 100644 index b8e3b5595..000000000 --- a/build/repo.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/build/version.props b/build/version.props deleted file mode 100644 index d8e514553..000000000 --- a/build/version.props +++ /dev/null @@ -1,9 +0,0 @@ - - - - 3.0.0 - alpha1 - $(VersionSuffix)-$(BuildNumber) - - - diff --git a/eng/Signing.props b/eng/Signing.props new file mode 100644 index 000000000..4ab0a58f0 --- /dev/null +++ b/eng/Signing.props @@ -0,0 +1,13 @@ + + + + true + + + + + + + + + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml new file mode 100644 index 000000000..6a634e28f --- /dev/null +++ b/eng/Version.Details.xml @@ -0,0 +1,19 @@ + + + + + + + + + https://github.com/dotnet/arcade + af34d3d032aaaa0ed6756de4e9dd50a94f7b40f2 + + + + https://github.com/dotnet/arcade + af34d3d032aaaa0ed6756de4e9dd50a94f7b40f2 + + + + diff --git a/eng/Versions.props b/eng/Versions.props new file mode 100644 index 000000000..5fd5f835e --- /dev/null +++ b/eng/Versions.props @@ -0,0 +1,29 @@ + + + + 3.0.0 + alpha1 + + + + 3.0.0-preview8.19405.7 + 1.0.0-preview8.19405.3 + 4.4.0 + 3.0.2 + 4.4.0 + 6.3.0-preview8-19405-04 + 3.0.0-preview8.19405.11 + 3.0.0-preview8.19405.4 + 2018.3.0 + 12.0.2 + 1.0.1 + 6.2.0-preview-60806030202 + 1.5.0 + 2.7.0 + 4.7.63 + 5.2.2 + 4.0.0 + 4.5.3 + + + diff --git a/eng/common/CIBuild.cmd b/eng/common/CIBuild.cmd new file mode 100644 index 000000000..56c2f25ac --- /dev/null +++ b/eng/common/CIBuild.cmd @@ -0,0 +1,2 @@ +@echo off +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0Build.ps1""" -restore -build -test -sign -pack -publish -ci %*" \ No newline at end of file diff --git a/eng/common/CheckSymbols.ps1 b/eng/common/CheckSymbols.ps1 new file mode 100644 index 000000000..b8d84607b --- /dev/null +++ b/eng/common/CheckSymbols.ps1 @@ -0,0 +1,158 @@ +param( + [Parameter(Mandatory=$true)][string] $InputPath, # Full path to directory where NuGet packages to be checked are stored + [Parameter(Mandatory=$true)][string] $ExtractPath, # Full path to directory where the packages will be extracted during validation + [Parameter(Mandatory=$true)][string] $SymbolToolPath # Full path to directory where dotnet symbol-tool was installed +) + +Add-Type -AssemblyName System.IO.Compression.FileSystem + +function FirstMatchingSymbolDescriptionOrDefault { + param( + [string] $FullPath, # Full path to the module that has to be checked + [string] $TargetServerParam, # Parameter to pass to `Symbol Tool` indicating the server to lookup for symbols + [string] $SymbolsPath + ) + + $FileName = [System.IO.Path]::GetFileName($FullPath) + $Extension = [System.IO.Path]::GetExtension($FullPath) + + # Those below are potential symbol files that the `dotnet symbol` might + # return. Which one will be returned depend on the type of file we are + # checking and which type of file was uploaded. + + # The file itself is returned + $SymbolPath = $SymbolsPath + "\" + $FileName + + # PDB file for the module + $PdbPath = $SymbolPath.Replace($Extension, ".pdb") + + # PDB file for R2R module (created by crossgen) + $NGenPdb = $SymbolPath.Replace($Extension, ".ni.pdb") + + # DBG file for a .so library + $SODbg = $SymbolPath.Replace($Extension, ".so.dbg") + + # DWARF file for a .dylib + $DylibDwarf = $SymbolPath.Replace($Extension, ".dylib.dwarf") + + .\dotnet-symbol.exe --symbols --modules --windows-pdbs $TargetServerParam $FullPath -o $SymbolsPath | Out-Null + + if (Test-Path $PdbPath) { + return "PDB" + } + elseif (Test-Path $NGenPdb) { + return "NGen PDB" + } + elseif (Test-Path $SODbg) { + return "DBG for SO" + } + elseif (Test-Path $DylibDwarf) { + return "Dwarf for Dylib" + } + elseif (Test-Path $SymbolPath) { + return "Module" + } + else { + return $null + } +} + +function CountMissingSymbols { + param( + [string] $PackagePath # Path to a NuGet package + ) + + # Ensure input file exist + if (!(Test-Path $PackagePath)) { + throw "Input file does not exist: $PackagePath" + } + + # Extensions for which we'll look for symbols + $RelevantExtensions = @(".dll", ".exe", ".so", ".dylib") + + # How many files are missing symbol information + $MissingSymbols = 0 + + $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) + $PackageGuid = New-Guid + $ExtractPath = Join-Path -Path $ExtractPath -ChildPath $PackageGuid + $SymbolsPath = Join-Path -Path $ExtractPath -ChildPath "Symbols" + + [System.IO.Compression.ZipFile]::ExtractToDirectory($PackagePath, $ExtractPath) + + # Makes easier to reference `symbol tool` + Push-Location $SymbolToolPath + + Get-ChildItem -Recurse $ExtractPath | + Where-Object {$RelevantExtensions -contains $_.Extension} | + ForEach-Object { + if ($_.FullName -Match "\\ref\\") { + Write-Host "`t Ignoring reference assembly file" $_.FullName + return + } + + $SymbolsOnMSDL = FirstMatchingSymbolDescriptionOrDefault $_.FullName "--microsoft-symbol-server" $SymbolsPath + $SymbolsOnSymWeb = FirstMatchingSymbolDescriptionOrDefault $_.FullName "--internal-server" $SymbolsPath + + Write-Host -NoNewLine "`t Checking file" $_.FullName "... " + + if ($SymbolsOnMSDL -ne $null -and $SymbolsOnSymWeb -ne $null) { + Write-Host "Symbols found on MSDL (" $SymbolsOnMSDL ") and SymWeb (" $SymbolsOnSymWeb ")" + } + else { + $MissingSymbols++ + + if ($SymbolsOnMSDL -eq $null -and $SymbolsOnSymWeb -eq $null) { + Write-Host "No symbols found on MSDL or SymWeb!" + } + else { + if ($SymbolsOnMSDL -eq $null) { + Write-Host "No symbols found on MSDL!" + } + else { + Write-Host "No symbols found on SymWeb!" + } + } + } + } + + Pop-Location + + return $MissingSymbols +} + +function CheckSymbolsAvailable { + if (Test-Path $ExtractPath) { + Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue + } + + Get-ChildItem "$InputPath\*.nupkg" | + ForEach-Object { + $FileName = $_.Name + + # These packages from Arcade-Services include some native libraries that + # our current symbol uploader can't handle. Below is a workaround until + # we get issue: https://github.com/dotnet/arcade/issues/2457 sorted. + if ($FileName -Match "Microsoft\.DotNet\.Darc\.") { + Write-Host "Ignoring Arcade-services file: $FileName" + Write-Host + return + } + elseif ($FileName -Match "Microsoft\.DotNet\.Maestro\.Tasks\.") { + Write-Host "Ignoring Arcade-services file: $FileName" + Write-Host + return + } + + Write-Host "Validating $FileName " + $Status = CountMissingSymbols "$InputPath\$FileName" + + if ($Status -ne 0) { + Write-Error "Missing symbols for $Status modules in the package $FileName" + } + + Write-Host + } +} + +CheckSymbolsAvailable diff --git a/eng/common/PSScriptAnalyzerSettings.psd1 b/eng/common/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 000000000..4c1ea7c98 --- /dev/null +++ b/eng/common/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,11 @@ +@{ + IncludeRules=@('PSAvoidUsingCmdletAliases', + 'PSAvoidUsingWMICmdlet', + 'PSAvoidUsingPositionalParameters', + 'PSAvoidUsingInvokeExpression', + 'PSUseDeclaredVarsMoreThanAssignments', + 'PSUseCmdletCorrectly', + 'PSStandardDSCFunctionsInResource', + 'PSUseIdenticalMandatoryParametersForDSC', + 'PSUseIdenticalParametersForDSC') +} \ No newline at end of file diff --git a/eng/common/PublishToPackageFeed.proj b/eng/common/PublishToPackageFeed.proj new file mode 100644 index 000000000..a1b133372 --- /dev/null +++ b/eng/common/PublishToPackageFeed.proj @@ -0,0 +1,83 @@ + + + + + + netcoreapp2.1 + + + + + + + + + + + + + + + + + + + + + + + + + https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json + https://dotnetfeed.blob.core.windows.net/arcade-validation/index.json + https://dotnetfeed.blob.core.windows.net/aspnet-aspnetcore/index.json + https://dotnetfeed.blob.core.windows.net/aspnet-aspnetcore-tooling/index.json + https://dotnetfeed.blob.core.windows.net/aspnet-entityframeworkcore/index.json + https://dotnetfeed.blob.core.windows.net/aspnet-extensions/index.json + https://dotnetfeed.blob.core.windows.net/dotnet-coreclr/index.json + https://dotnetfeed.blob.core.windows.net/dotnet-sdk/index.json + https://dotnetfeed.blob.core.windows.net/dotnet-tools-internal/index.json + https://dotnetfeed.blob.core.windows.net/dotnet-toolset/index.json + https://dotnetfeed.blob.core.windows.net/dotnet-windowsdesktop/index.json + https://dotnetfeed.blob.core.windows.net/nuget-nugetclient/index.json + https://dotnetfeed.blob.core.windows.net/aspnet-entityframework6/index.json + https://dotnetfeed.blob.core.windows.net/aspnet-blazor/index.json + + + + + + + + + + + + diff --git a/eng/common/PublishToSymbolServers.proj b/eng/common/PublishToSymbolServers.proj new file mode 100644 index 000000000..5d55e312b --- /dev/null +++ b/eng/common/PublishToSymbolServers.proj @@ -0,0 +1,82 @@ + + + + + + netcoreapp2.1 + + + + + + + + + + + + + + + + 3650 + true + false + + + + + + + + + + + + + + + + + diff --git a/eng/common/README.md b/eng/common/README.md new file mode 100644 index 000000000..ff49c3715 --- /dev/null +++ b/eng/common/README.md @@ -0,0 +1,28 @@ +# Don't touch this folder + + uuuuuuuuuuuuuuuuuuuu + u" uuuuuuuuuuuuuuuuuu "u + u" u$$$$$$$$$$$$$$$$$$$$u "u + u" u$$$$$$$$$$$$$$$$$$$$$$$$u "u + u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u + u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u + u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u + $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ + $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ + $ $$$" ... "$... ...$" ... "$$$ ... "$$$ $ + $ $$$u `"$$$$$$$ $$$ $$$$$ $$ $$$ $$$ $ + $ $$$$$$uu "$$$$ $$$ $$$$$ $$ """ u$$$ $ + $ $$$""$$$ $$$$ $$$u "$$$" u$$ $$$$$$$$ $ + $ $$$$....,$$$$$..$$$$$....,$$$$..$$$$$$$$ $ + $ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ + "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u" + "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u" + "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u" + "u "$$$$$$$$$$$$$$$$$$$$$$$$" u" + "u "$$$$$$$$$$$$$$$$$$$$" u" + "u """""""""""""""""" u" + """""""""""""""""""" + +!!! Changes made in this directory are subject to being overwritten by automation !!! + +The files in this directory are shared by all Arcade repos and managed by automation. If you need to make changes to these files, open an issue or submit a pull request to https://github.com/dotnet/arcade first. diff --git a/eng/common/SigningValidation.proj b/eng/common/SigningValidation.proj new file mode 100644 index 000000000..3d0ac80af --- /dev/null +++ b/eng/common/SigningValidation.proj @@ -0,0 +1,83 @@ + + + + + + netcoreapp2.1 + + + + + + + + $(NuGetPackageRoot)Microsoft.DotNet.SignCheck\$(SignCheckVersion)\tools\Microsoft.DotNet.SignCheck.exe + + $(PackageBasePath) + signcheck.log + signcheck.errors.log + signcheck.exclusions.txt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/common/SourceLinkValidation.ps1 b/eng/common/SourceLinkValidation.ps1 new file mode 100644 index 000000000..cb2d28cb9 --- /dev/null +++ b/eng/common/SourceLinkValidation.ps1 @@ -0,0 +1,184 @@ +param( + [Parameter(Mandatory=$true)][string] $InputPath, # Full path to directory where Symbols.NuGet packages to be checked are stored + [Parameter(Mandatory=$true)][string] $ExtractPath, # Full path to directory where the packages will be extracted during validation + [Parameter(Mandatory=$true)][string] $SourceLinkToolPath, # Full path to directory where dotnet SourceLink CLI was installed + [Parameter(Mandatory=$true)][string] $GHRepoName, # GitHub name of the repo including the Org. E.g., dotnet/arcade + [Parameter(Mandatory=$true)][string] $GHCommit # GitHub commit SHA used to build the packages +) + +# Cache/HashMap (File -> Exist flag) used to consult whether a file exist +# in the repository at a specific commit point. This is populated by inserting +# all files present in the repo at a specific commit point. +$global:RepoFiles = @{} + +$ValidatePackage = { + param( + [string] $PackagePath # Full path to a Symbols.NuGet package + ) + + # Ensure input file exist + if (!(Test-Path $PackagePath)) { + throw "Input file does not exist: $PackagePath" + } + + # Extensions for which we'll look for SourceLink information + # For now we'll only care about Portable & Embedded PDBs + $RelevantExtensions = @(".dll", ".exe", ".pdb") + + Write-Host -NoNewLine "Validating" ([System.IO.Path]::GetFileName($PackagePath)) "... " + + $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) + $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageId + $FailedFiles = 0 + + Add-Type -AssemblyName System.IO.Compression.FileSystem + + [System.IO.Directory]::CreateDirectory($ExtractPath); + + $zip = [System.IO.Compression.ZipFile]::OpenRead($PackagePath) + + $zip.Entries | + Where-Object {$RelevantExtensions -contains [System.IO.Path]::GetExtension($_.Name)} | + ForEach-Object { + $FileName = $_.FullName + $Extension = [System.IO.Path]::GetExtension($_.Name) + $FakeName = -Join((New-Guid), $Extension) + $TargetFile = Join-Path -Path $ExtractPath -ChildPath $FakeName + + # We ignore resource DLLs + if ($FileName.EndsWith(".resources.dll")) { + return + } + + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $TargetFile, $true) + + $ValidateFile = { + param( + [string] $FullPath, # Full path to the module that has to be checked + [string] $RealPath, + [ref] $FailedFiles + ) + + # Makes easier to reference `sourcelink cli` + Push-Location $using:SourceLinkToolPath + + $SourceLinkInfos = .\sourcelink.exe print-urls $FullPath | Out-String + + if ($LASTEXITCODE -eq 0 -and -not ([string]::IsNullOrEmpty($SourceLinkInfos))) { + $NumFailedLinks = 0 + + # We only care about Http addresses + $Matches = (Select-String '(http[s]?)(:\/\/)([^\s,]+)' -Input $SourceLinkInfos -AllMatches).Matches + + if ($Matches.Count -ne 0) { + $Matches.Value | + ForEach-Object { + $Link = $_ + $CommitUrl = -Join("https://raw.githubusercontent.com/", $using:GHRepoName, "/", $using:GHCommit, "/") + $FilePath = $Link.Replace($CommitUrl, "") + $Status = 200 + $Cache = $using:RepoFiles + + if ( !($Cache.ContainsKey($FilePath)) ) { + try { + $Uri = $Link -as [System.URI] + + # Only GitHub links are valid + if ($Uri.AbsoluteURI -ne $null -and $Uri.Host -match "github") { + $Status = (Invoke-WebRequest -Uri $Link -UseBasicParsing -Method HEAD -TimeoutSec 5).StatusCode + } + else { + $Status = 0 + } + } + catch { + $Status = 0 + } + } + + if ($Status -ne 200) { + if ($NumFailedLinks -eq 0) { + if ($FailedFiles.Value -eq 0) { + Write-Host + } + + Write-Host "`tFile $RealPath has broken links:" + } + + Write-Host "`t`tFailed to retrieve $Link" + + $NumFailedLinks++ + } + } + } + + if ($NumFailedLinks -ne 0) { + $FailedFiles.value++ + $global:LASTEXITCODE = 1 + } + } + + Pop-Location + } + + &$ValidateFile $TargetFile $FileName ([ref]$FailedFiles) + } + + $zip.Dispose() + + if ($FailedFiles -eq 0) { + Write-Host "Passed." + } +} + +function ValidateSourceLinkLinks { + if (!($GHRepoName -Match "^[^\s\/]+/[^\s\/]+$")) { + Write-Host "GHRepoName should be in the format /" + $global:LASTEXITCODE = 1 + return + } + + if (!($GHCommit -Match "^[0-9a-fA-F]{40}$")) { + Write-Host "GHCommit should be a 40 chars hexadecimal string" + $global:LASTEXITCODE = 1 + return + } + + $RepoTreeURL = -Join("https://api.github.com/repos/", $GHRepoName, "/git/trees/", $GHCommit, "?recursive=1") + $CodeExtensions = @(".cs", ".vb", ".fs", ".fsi", ".fsx", ".fsscript") + + try { + # Retrieve the list of files in the repo at that particular commit point and store them in the RepoFiles hash + $Data = Invoke-WebRequest $RepoTreeURL | ConvertFrom-Json | Select-Object -ExpandProperty tree + + foreach ($file in $Data) { + $Extension = [System.IO.Path]::GetExtension($file.path) + + if ($CodeExtensions.Contains($Extension)) { + $RepoFiles[$file.path] = 1 + } + } + } + catch { + Write-Host "Problems downloading the list of files from the repo. Url used: $RepoTreeURL" + $global:LASTEXITCODE = 1 + return + } + + if (Test-Path $ExtractPath) { + Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue + } + + # Process each NuGet package in parallel + $Jobs = @() + Get-ChildItem "$InputPath\*.symbols.nupkg" | + ForEach-Object { + $Jobs += Start-Job -ScriptBlock $ValidatePackage -ArgumentList $_.FullName + } + + foreach ($Job in $Jobs) { + Wait-Job -Id $Job.Id | Receive-Job + } +} + +Measure-Command { ValidateSourceLinkLinks } diff --git a/eng/common/build.ps1 b/eng/common/build.ps1 new file mode 100644 index 000000000..feb58d141 --- /dev/null +++ b/eng/common/build.ps1 @@ -0,0 +1,141 @@ +[CmdletBinding(PositionalBinding=$false)] +Param( + [string][Alias('c')]$configuration = "Debug", + [string]$platform = $null, + [string] $projects, + [string][Alias('v')]$verbosity = "minimal", + [string] $msbuildEngine = $null, + [bool] $warnAsError = $true, + [bool] $nodeReuse = $true, + [switch][Alias('r')]$restore, + [switch] $deployDeps, + [switch][Alias('b')]$build, + [switch] $rebuild, + [switch] $deploy, + [switch][Alias('t')]$test, + [switch] $integrationTest, + [switch] $performanceTest, + [switch] $sign, + [switch] $pack, + [switch] $publish, + [switch][Alias('bl')]$binaryLog, + [switch] $ci, + [switch] $prepareMachine, + [switch] $help, + [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties +) + +. $PSScriptRoot\tools.ps1 + +function Print-Usage() { + Write-Host "Common settings:" + Write-Host " -configuration Build configuration: 'Debug' or 'Release' (short: -c)" + Write-Host " -platform Platform configuration: 'x86', 'x64' or any valid Platform value to pass to msbuild" + Write-Host " -verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic] (short: -v)" + Write-Host " -binaryLog Output binary log (short: -bl)" + Write-Host " -help Print help and exit" + Write-Host "" + + Write-Host "Actions:" + Write-Host " -restore Restore dependencies (short: -r)" + Write-Host " -build Build solution (short: -b)" + Write-Host " -rebuild Rebuild solution" + Write-Host " -deploy Deploy built VSIXes" + Write-Host " -deployDeps Deploy dependencies (e.g. VSIXes for integration tests)" + Write-Host " -test Run all unit tests in the solution (short: -t)" + Write-Host " -integrationTest Run all integration tests in the solution" + Write-Host " -performanceTest Run all performance tests in the solution" + Write-Host " -pack Package build outputs into NuGet packages and Willow components" + Write-Host " -sign Sign build outputs" + Write-Host " -publish Publish artifacts (e.g. symbols)" + Write-Host "" + + Write-Host "Advanced settings:" + Write-Host " -projects Semi-colon delimited list of sln/proj's to build. Globbing is supported (*.sln)" + Write-Host " -ci Set when running on CI server" + Write-Host " -prepareMachine Prepare machine for CI run, clean up processes after build" + Write-Host " -warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" + Write-Host " -msbuildEngine Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." + Write-Host "" + + Write-Host "Command line arguments not listed above are passed thru to msbuild." + Write-Host "The above arguments can be shortened as much as to be unambiguous (e.g. -co for configuration, -t for test, etc.)." +} + +function InitializeCustomToolset { + if (-not $restore) { + return + } + + $script = Join-Path $EngRoot "restore-toolset.ps1" + + if (Test-Path $script) { + . $script + } +} + +function Build { + $toolsetBuildProj = InitializeToolset + InitializeCustomToolset + + $bl = if ($binaryLog) { "/bl:" + (Join-Path $LogDir "Build.binlog") } else { "" } + $platformArg = if ($platform) { "/p:Platform=$platform" } else { "" } + + if ($projects) { + # Re-assign properties to a new variable because PowerShell doesn't let us append properties directly for unclear reasons. + # Explicitly set the type as string[] because otherwise PowerShell would make this char[] if $properties is empty. + [string[]] $msbuildArgs = $properties + $msbuildArgs += "/p:Projects=$projects" + $properties = $msbuildArgs + } + + MSBuild $toolsetBuildProj ` + $bl ` + $platformArg ` + /p:Configuration=$configuration ` + /p:RepoRoot=$RepoRoot ` + /p:Restore=$restore ` + /p:DeployDeps=$deployDeps ` + /p:Build=$build ` + /p:Rebuild=$rebuild ` + /p:Deploy=$deploy ` + /p:Test=$test ` + /p:Pack=$pack ` + /p:IntegrationTest=$integrationTest ` + /p:PerformanceTest=$performanceTest ` + /p:Sign=$sign ` + /p:Publish=$publish ` + @properties +} + +try { + if ($help -or (($null -ne $properties) -and ($properties.Contains("/help") -or $properties.Contains("/?")))) { + Print-Usage + exit 0 + } + + if ($ci) { + $binaryLog = $true + $nodeReuse = $false + } + + # Import custom tools configuration, if present in the repo. + # Note: Import in global scope so that the script set top-level variables without qualification. + $configureToolsetScript = Join-Path $EngRoot "configure-toolset.ps1" + if (Test-Path $configureToolsetScript) { + . $configureToolsetScript + } + + if (($restore) -and ($null -eq $env:DisableNativeToolsetInstalls)) { + InitializeNativeTools + } + + Build +} +catch { + Write-Host $_.ScriptStackTrace + Write-PipelineTelemetryError -Category "InitializeToolset" -Message $_ + ExitWithExitCode 1 +} + +ExitWithExitCode 0 diff --git a/eng/common/build.sh b/eng/common/build.sh new file mode 100755 index 000000000..6236fc4d3 --- /dev/null +++ b/eng/common/build.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash + +# Stop script if unbound variable found (use ${var:-} if intentional) +set -u + +# Stop script if command returns non-zero exit code. +# Prevents hidden errors caused by missing error code propagation. +set -e + +usage() +{ + echo "Common settings:" + echo " --configuration Build configuration: 'Debug' or 'Release' (short: -c)" + echo " --verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic] (short: -v)" + echo " --binaryLog Create MSBuild binary log (short: -bl)" + echo " --help Print help and exit (short: -h)" + echo "" + + echo "Actions:" + echo " --restore Restore dependencies (short: -r)" + echo " --build Build solution (short: -b)" + echo " --rebuild Rebuild solution" + echo " --test Run all unit tests in the solution (short: -t)" + echo " --integrationTest Run all integration tests in the solution" + echo " --performanceTest Run all performance tests in the solution" + echo " --pack Package build outputs into NuGet packages and Willow components" + echo " --sign Sign build outputs" + echo " --publish Publish artifacts (e.g. symbols)" + echo "" + + echo "Advanced settings:" + echo " --projects Project or solution file(s) to build" + echo " --ci Set when running on CI server" + echo " --prepareMachine Prepare machine for CI run, clean up processes after build" + echo " --nodeReuse Sets nodereuse msbuild parameter ('true' or 'false')" + echo " --warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" + echo "" + echo "Command line arguments not listed above are passed thru to msbuild." + echo "Arguments can also be passed in with a single hyphen." +} + +source="${BASH_SOURCE[0]}" + +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +restore=false +build=false +rebuild=false +test=false +integration_test=false +performance_test=false +pack=false +publish=false +sign=false +public=false +ci=false + +warn_as_error=true +node_reuse=true +binary_log=false +pipelines_log=false + +projects='' +configuration='Debug' +prepare_machine=false +verbosity='minimal' + +properties='' + +while [[ $# > 0 ]]; do + opt="$(echo "${1/#--/-}" | awk '{print tolower($0)}')" + case "$opt" in + -help|-h) + usage + exit 0 + ;; + -configuration|-c) + configuration=$2 + shift + ;; + -verbosity|-v) + verbosity=$2 + shift + ;; + -binarylog|-bl) + binary_log=true + ;; + -pipelineslog|-pl) + pipelines_log=true + ;; + -restore|-r) + restore=true + ;; + -build|-b) + build=true + ;; + -rebuild) + rebuild=true + ;; + -pack) + pack=true + ;; + -test|-t) + test=true + ;; + -integrationtest) + integration_test=true + ;; + -performancetest) + performance_test=true + ;; + -sign) + sign=true + ;; + -publish) + publish=true + ;; + -preparemachine) + prepare_machine=true + ;; + -projects) + projects=$2 + shift + ;; + -ci) + ci=true + ;; + -warnaserror) + warn_as_error=$2 + shift + ;; + -nodereuse) + node_reuse=$2 + shift + ;; + *) + properties="$properties $1" + ;; + esac + + shift +done + +if [[ "$ci" == true ]]; then + pipelines_log=true + binary_log=true + node_reuse=false +fi + +. "$scriptroot/tools.sh" + +function InitializeCustomToolset { + local script="$eng_root/restore-toolset.sh" + + if [[ -a "$script" ]]; then + . "$script" + fi +} + +function Build { + InitializeToolset + InitializeCustomToolset + + if [[ ! -z "$projects" ]]; then + properties="$properties /p:Projects=$projects" + fi + + local bl="" + if [[ "$binary_log" == true ]]; then + bl="/bl:\"$log_dir/Build.binlog\"" + fi + + MSBuild $_InitializeToolset \ + $bl \ + /p:Configuration=$configuration \ + /p:RepoRoot="$repo_root" \ + /p:Restore=$restore \ + /p:Build=$build \ + /p:Rebuild=$rebuild \ + /p:Test=$test \ + /p:Pack=$pack \ + /p:IntegrationTest=$integration_test \ + /p:PerformanceTest=$performance_test \ + /p:Sign=$sign \ + /p:Publish=$publish \ + $properties + + ExitWithExitCode 0 +} + +# Import custom tools configuration, if present in the repo. +configure_toolset_script="$eng_root/configure-toolset.sh" +if [[ -a "$configure_toolset_script" ]]; then + . "$configure_toolset_script" +fi + +# TODO: https://github.com/dotnet/arcade/issues/1468 +# Temporary workaround to avoid breaking change. +# Remove once repos are updated. +if [[ -n "${useInstalledDotNetCli:-}" ]]; then + use_installed_dotnet_cli="$useInstalledDotNetCli" +fi + +if [[ "$restore" == true && -z ${DisableNativeToolsetInstalls:-} ]]; then + InitializeNativeTools +fi + +Build diff --git a/eng/common/cibuild.sh b/eng/common/cibuild.sh new file mode 100755 index 000000000..1a02c0dec --- /dev/null +++ b/eng/common/cibuild.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" + +# resolve $SOURCE until the file is no longer a symlink +while [[ -h $source ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + + # if $source was a relative symlink, we need to resolve it relative to the path where + # the symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. "$scriptroot/build.sh" --restore --build --test --pack --publish --ci $@ \ No newline at end of file diff --git a/eng/common/cross/android/arm/toolchain.cmake b/eng/common/cross/android/arm/toolchain.cmake new file mode 100644 index 000000000..a7e1c7350 --- /dev/null +++ b/eng/common/cross/android/arm/toolchain.cmake @@ -0,0 +1,41 @@ +set(CROSS_NDK_TOOLCHAIN $ENV{ROOTFS_DIR}/../) +set(CROSS_ROOTFS ${CROSS_NDK_TOOLCHAIN}/sysroot) +set(CLR_CMAKE_PLATFORM_ANDROID "Android") + +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_VERSION 1) +set(CMAKE_SYSTEM_PROCESSOR arm) + +## Specify the toolchain +set(TOOLCHAIN "arm-linux-androideabi") +set(CMAKE_PREFIX_PATH ${CROSS_NDK_TOOLCHAIN}) +set(TOOLCHAIN_PREFIX ${TOOLCHAIN}-) + +find_program(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}clang) +find_program(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}clang++) +find_program(CMAKE_ASM_COMPILER ${TOOLCHAIN_PREFIX}clang) +find_program(CMAKE_AR ${TOOLCHAIN_PREFIX}ar) +find_program(CMAKE_LD ${TOOLCHAIN_PREFIX}ar) +find_program(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy) +find_program(CMAKE_OBJDUMP ${TOOLCHAIN_PREFIX}objdump) + +add_compile_options(--sysroot=${CROSS_ROOTFS}) +add_compile_options(-fPIE) +add_compile_options(-mfloat-abi=soft) +include_directories(SYSTEM ${CROSS_NDK_TOOLCHAIN}/include/c++/4.9.x/) +include_directories(SYSTEM ${CROSS_NDK_TOOLCHAIN}/include/c++/4.9.x/arm-linux-androideabi/) + +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} -B ${CROSS_ROOTFS}/usr/lib/gcc/${TOOLCHAIN}") +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} -L${CROSS_ROOTFS}/lib/${TOOLCHAIN}") +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} --sysroot=${CROSS_ROOTFS}") +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} -fPIE -pie") + +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${CROSS_LINK_FLAGS}" CACHE STRING "" FORCE) +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${CROSS_LINK_FLAGS}" CACHE STRING "" FORCE) +set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${CROSS_LINK_FLAGS}" CACHE STRING "" FORCE) + +set(CMAKE_FIND_ROOT_PATH "${CROSS_ROOTFS}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/eng/common/cross/android/arm64/toolchain.cmake b/eng/common/cross/android/arm64/toolchain.cmake new file mode 100644 index 000000000..29415899c --- /dev/null +++ b/eng/common/cross/android/arm64/toolchain.cmake @@ -0,0 +1,42 @@ +set(CROSS_NDK_TOOLCHAIN $ENV{ROOTFS_DIR}/../) +set(CROSS_ROOTFS ${CROSS_NDK_TOOLCHAIN}/sysroot) +set(CLR_CMAKE_PLATFORM_ANDROID "Android") + +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_VERSION 1) +set(CMAKE_SYSTEM_PROCESSOR aarch64) + +## Specify the toolchain +set(TOOLCHAIN "aarch64-linux-android") +set(CMAKE_PREFIX_PATH ${CROSS_NDK_TOOLCHAIN}) +set(TOOLCHAIN_PREFIX ${TOOLCHAIN}-) + +find_program(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}clang) +find_program(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}clang++) +find_program(CMAKE_ASM_COMPILER ${TOOLCHAIN_PREFIX}clang) +find_program(CMAKE_AR ${TOOLCHAIN_PREFIX}ar) +find_program(CMAKE_LD ${TOOLCHAIN_PREFIX}ar) +find_program(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy) +find_program(CMAKE_OBJDUMP ${TOOLCHAIN_PREFIX}objdump) + +add_compile_options(--sysroot=${CROSS_ROOTFS}) +add_compile_options(-fPIE) + +## Needed for Android or bionic specific conditionals +add_compile_options(-D__ANDROID__) +add_compile_options(-D__BIONIC__) + +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} -B ${CROSS_ROOTFS}/usr/lib/gcc/${TOOLCHAIN}") +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} -L${CROSS_ROOTFS}/lib/${TOOLCHAIN}") +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} --sysroot=${CROSS_ROOTFS}") +set(CROSS_LINK_FLAGS "${CROSS_LINK_FLAGS} -fPIE -pie") + +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${CROSS_LINK_FLAGS}" CACHE STRING "" FORCE) +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${CROSS_LINK_FLAGS}" CACHE STRING "" FORCE) +set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${CROSS_LINK_FLAGS}" CACHE STRING "" FORCE) + +set(CMAKE_FIND_ROOT_PATH "${CROSS_ROOTFS}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/eng/common/cross/arm/sources.list.bionic b/eng/common/cross/arm/sources.list.bionic new file mode 100644 index 000000000..210955740 --- /dev/null +++ b/eng/common/cross/arm/sources.list.bionic @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse diff --git a/eng/common/cross/arm/sources.list.jessie b/eng/common/cross/arm/sources.list.jessie new file mode 100644 index 000000000..4d142ac9b --- /dev/null +++ b/eng/common/cross/arm/sources.list.jessie @@ -0,0 +1,3 @@ +# Debian (sid) # UNSTABLE +deb http://ftp.debian.org/debian/ sid main contrib non-free +deb-src http://ftp.debian.org/debian/ sid main contrib non-free diff --git a/eng/common/cross/arm/sources.list.trusty b/eng/common/cross/arm/sources.list.trusty new file mode 100644 index 000000000..07d8f88d8 --- /dev/null +++ b/eng/common/cross/arm/sources.list.trusty @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ trusty main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ trusty-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ trusty-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ trusty-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty-security main restricted universe multiverse \ No newline at end of file diff --git a/eng/common/cross/arm/sources.list.xenial b/eng/common/cross/arm/sources.list.xenial new file mode 100644 index 000000000..eacd86b7d --- /dev/null +++ b/eng/common/cross/arm/sources.list.xenial @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse \ No newline at end of file diff --git a/eng/common/cross/arm/sources.list.zesty b/eng/common/cross/arm/sources.list.zesty new file mode 100644 index 000000000..ea2c14a78 --- /dev/null +++ b/eng/common/cross/arm/sources.list.zesty @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ zesty main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-security main restricted universe multiverse diff --git a/eng/common/cross/arm/trusty-lttng-2.4.patch b/eng/common/cross/arm/trusty-lttng-2.4.patch new file mode 100644 index 000000000..8e4dd7ae7 --- /dev/null +++ b/eng/common/cross/arm/trusty-lttng-2.4.patch @@ -0,0 +1,71 @@ +From e72c9d7ead60e3317bd6d1fade995c07021c947b Mon Sep 17 00:00:00 2001 +From: Mathieu Desnoyers +Date: Thu, 7 May 2015 13:25:04 -0400 +Subject: [PATCH] Fix: building probe providers with C++ compiler + +Robert Daniels wrote: +> > I'm attempting to use lttng userspace tracing with a C++ application +> > on an ARM platform. I'm using GCC 4.8.4 on Linux 3.14 with the 2.6 +> > release of lttng. I've compiled lttng-modules, lttng-ust, and +> > lttng-tools and have been able to get a simple test working with C +> > code. When I attempt to run the hello.cxx test on my target it will +> > segfault. +> +> +> I spent a little time digging into this issue and finally discovered the +> cause of my segfault with ARM C++ tracepoints. +> +> There is a struct called 'lttng_event' in ust-events.h which contains an +> empty union 'u'. This was the cause of my issue. Under C, this empty union +> compiles to a zero byte member while under C++ it compiles to a one byte +> member, and in my case was four-byte aligned which caused my C++ code to +> have the 'cds_list_head node' offset incorrectly by four bytes. This lead +> to an incorrect linked list structure which caused my issue. +> +> Since this union is empty, I simply removed it from the struct and everything +> worked correctly. +> +> I don't know the history or purpose behind this empty union so I'd like to +> know if this is a safe fix. If it is I can submit a patch with the union +> removed. + +That's a very nice catch! + +We do not support building tracepoint probe provider with +g++ yet, as stated in lttng-ust(3): + +"- Note for C++ support: although an application instrumented with + tracepoints can be compiled with g++, tracepoint probes should be + compiled with gcc (only tested with gcc so far)." + +However, if it works fine with this fix, then I'm tempted to take it, +especially because removing the empty union does not appear to affect +the layout of struct lttng_event as seen from liblttng-ust, which must +be compiled with a C compiler, and from probe providers compiled with +a C compiler. So all we are changing is the layout of a probe provider +compiled with a C++ compiler, which is anyway buggy at the moment, +because it is not compatible with the layout expected by liblttng-ust +compiled with a C compiler. + +Reported-by: Robert Daniels +Signed-off-by: Mathieu Desnoyers +--- + include/lttng/ust-events.h | 2 -- + 1 file changed, 2 deletions(-) + +diff --git a/usr/include/lttng/ust-events.h b/usr/include/lttng/ust-events.h +index 328a875..3d7a274 100644 +--- a/usr/include/lttng/ust-events.h ++++ b/usr/include/lttng/ust-events.h +@@ -407,8 +407,6 @@ struct lttng_event { + void *_deprecated1; + struct lttng_ctx *ctx; + enum lttng_ust_instrumentation instrumentation; +- union { +- } u; + struct cds_list_head node; /* Event list in session */ + struct cds_list_head _deprecated2; + void *_deprecated3; +-- +2.7.4 + diff --git a/eng/common/cross/arm/trusty.patch b/eng/common/cross/arm/trusty.patch new file mode 100644 index 000000000..2f2972f8e --- /dev/null +++ b/eng/common/cross/arm/trusty.patch @@ -0,0 +1,97 @@ +diff -u -r a/usr/include/urcu/uatomic/generic.h b/usr/include/urcu/uatomic/generic.h +--- a/usr/include/urcu/uatomic/generic.h 2014-03-28 06:04:42.000000000 +0900 ++++ b/usr/include/urcu/uatomic/generic.h 2017-02-13 10:35:21.189927116 +0900 +@@ -65,17 +65,17 @@ + switch (len) { + #ifdef UATOMIC_HAS_ATOMIC_BYTE + case 1: +- return __sync_val_compare_and_swap_1(addr, old, _new); ++ return __sync_val_compare_and_swap_1((uint8_t *) addr, old, _new); + #endif + #ifdef UATOMIC_HAS_ATOMIC_SHORT + case 2: +- return __sync_val_compare_and_swap_2(addr, old, _new); ++ return __sync_val_compare_and_swap_2((uint16_t *) addr, old, _new); + #endif + case 4: +- return __sync_val_compare_and_swap_4(addr, old, _new); ++ return __sync_val_compare_and_swap_4((uint32_t *) addr, old, _new); + #if (CAA_BITS_PER_LONG == 64) + case 8: +- return __sync_val_compare_and_swap_8(addr, old, _new); ++ return __sync_val_compare_and_swap_8((uint64_t *) addr, old, _new); + #endif + } + _uatomic_link_error(); +@@ -100,20 +100,20 @@ + switch (len) { + #ifdef UATOMIC_HAS_ATOMIC_BYTE + case 1: +- __sync_and_and_fetch_1(addr, val); ++ __sync_and_and_fetch_1((uint8_t *) addr, val); + return; + #endif + #ifdef UATOMIC_HAS_ATOMIC_SHORT + case 2: +- __sync_and_and_fetch_2(addr, val); ++ __sync_and_and_fetch_2((uint16_t *) addr, val); + return; + #endif + case 4: +- __sync_and_and_fetch_4(addr, val); ++ __sync_and_and_fetch_4((uint32_t *) addr, val); + return; + #if (CAA_BITS_PER_LONG == 64) + case 8: +- __sync_and_and_fetch_8(addr, val); ++ __sync_and_and_fetch_8((uint64_t *) addr, val); + return; + #endif + } +@@ -139,20 +139,20 @@ + switch (len) { + #ifdef UATOMIC_HAS_ATOMIC_BYTE + case 1: +- __sync_or_and_fetch_1(addr, val); ++ __sync_or_and_fetch_1((uint8_t *) addr, val); + return; + #endif + #ifdef UATOMIC_HAS_ATOMIC_SHORT + case 2: +- __sync_or_and_fetch_2(addr, val); ++ __sync_or_and_fetch_2((uint16_t *) addr, val); + return; + #endif + case 4: +- __sync_or_and_fetch_4(addr, val); ++ __sync_or_and_fetch_4((uint32_t *) addr, val); + return; + #if (CAA_BITS_PER_LONG == 64) + case 8: +- __sync_or_and_fetch_8(addr, val); ++ __sync_or_and_fetch_8((uint64_t *) addr, val); + return; + #endif + } +@@ -180,17 +180,17 @@ + switch (len) { + #ifdef UATOMIC_HAS_ATOMIC_BYTE + case 1: +- return __sync_add_and_fetch_1(addr, val); ++ return __sync_add_and_fetch_1((uint8_t *) addr, val); + #endif + #ifdef UATOMIC_HAS_ATOMIC_SHORT + case 2: +- return __sync_add_and_fetch_2(addr, val); ++ return __sync_add_and_fetch_2((uint16_t *) addr, val); + #endif + case 4: +- return __sync_add_and_fetch_4(addr, val); ++ return __sync_add_and_fetch_4((uint32_t *) addr, val); + #if (CAA_BITS_PER_LONG == 64) + case 8: +- return __sync_add_and_fetch_8(addr, val); ++ return __sync_add_and_fetch_8((uint64_t *) addr, val); + #endif + } + _uatomic_link_error(); diff --git a/eng/common/cross/arm64/sources.list.bionic b/eng/common/cross/arm64/sources.list.bionic new file mode 100644 index 000000000..210955740 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.bionic @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse diff --git a/eng/common/cross/arm64/sources.list.buster b/eng/common/cross/arm64/sources.list.buster new file mode 100644 index 000000000..7194ac64a --- /dev/null +++ b/eng/common/cross/arm64/sources.list.buster @@ -0,0 +1,11 @@ +deb http://deb.debian.org/debian buster main +deb-src http://deb.debian.org/debian buster main + +deb http://deb.debian.org/debian-security/ buster/updates main +deb-src http://deb.debian.org/debian-security/ buster/updates main + +deb http://deb.debian.org/debian buster-updates main +deb-src http://deb.debian.org/debian buster-updates main + +deb http://deb.debian.org/debian buster-backports main contrib non-free +deb-src http://deb.debian.org/debian buster-backports main contrib non-free diff --git a/eng/common/cross/arm64/sources.list.stretch b/eng/common/cross/arm64/sources.list.stretch new file mode 100644 index 000000000..0e1215774 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.stretch @@ -0,0 +1,12 @@ +deb http://deb.debian.org/debian stretch main +deb-src http://deb.debian.org/debian stretch main + +deb http://deb.debian.org/debian-security/ stretch/updates main +deb-src http://deb.debian.org/debian-security/ stretch/updates main + +deb http://deb.debian.org/debian stretch-updates main +deb-src http://deb.debian.org/debian stretch-updates main + +deb http://deb.debian.org/debian stretch-backports main contrib non-free +deb-src http://deb.debian.org/debian stretch-backports main contrib non-free + diff --git a/eng/common/cross/arm64/sources.list.trusty b/eng/common/cross/arm64/sources.list.trusty new file mode 100644 index 000000000..07d8f88d8 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.trusty @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ trusty main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ trusty-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ trusty-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ trusty-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ trusty-security main restricted universe multiverse \ No newline at end of file diff --git a/eng/common/cross/arm64/sources.list.xenial b/eng/common/cross/arm64/sources.list.xenial new file mode 100644 index 000000000..eacd86b7d --- /dev/null +++ b/eng/common/cross/arm64/sources.list.xenial @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse \ No newline at end of file diff --git a/eng/common/cross/arm64/sources.list.zesty b/eng/common/cross/arm64/sources.list.zesty new file mode 100644 index 000000000..ea2c14a78 --- /dev/null +++ b/eng/common/cross/arm64/sources.list.zesty @@ -0,0 +1,11 @@ +deb http://ports.ubuntu.com/ubuntu-ports/ zesty main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-updates main restricted universe +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-updates main restricted universe + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-backports main restricted +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-backports main restricted + +deb http://ports.ubuntu.com/ubuntu-ports/ zesty-security main restricted universe multiverse +deb-src http://ports.ubuntu.com/ubuntu-ports/ zesty-security main restricted universe multiverse diff --git a/eng/common/cross/armel/sources.list.jessie b/eng/common/cross/armel/sources.list.jessie new file mode 100644 index 000000000..3d9c3059d --- /dev/null +++ b/eng/common/cross/armel/sources.list.jessie @@ -0,0 +1,3 @@ +# Debian (jessie) # Stable +deb http://ftp.debian.org/debian/ jessie main contrib non-free +deb-src http://ftp.debian.org/debian/ jessie main contrib non-free diff --git a/eng/common/cross/armel/tizen-build-rootfs.sh b/eng/common/cross/armel/tizen-build-rootfs.sh new file mode 100755 index 000000000..87c48e78f --- /dev/null +++ b/eng/common/cross/armel/tizen-build-rootfs.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e + +__ARM_SOFTFP_CrossDir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +__TIZEN_CROSSDIR="$__ARM_SOFTFP_CrossDir/tizen" + +if [[ -z "$ROOTFS_DIR" ]]; then + echo "ROOTFS_DIR is not defined." + exit 1; +fi + +# Clean-up (TODO-Cleanup: We may already delete $ROOTFS_DIR at ./cross/build-rootfs.sh.) +# hk0110 +if [ -d "$ROOTFS_DIR" ]; then + umount $ROOTFS_DIR/* + rm -rf $ROOTFS_DIR +fi + +TIZEN_TMP_DIR=$ROOTFS_DIR/tizen_tmp +mkdir -p $TIZEN_TMP_DIR + +# Download files +echo ">>Start downloading files" +VERBOSE=1 $__ARM_SOFTFP_CrossDir/tizen-fetch.sh $TIZEN_TMP_DIR +echo "<>Start constructing Tizen rootfs" +TIZEN_RPM_FILES=`ls $TIZEN_TMP_DIR/*.rpm` +cd $ROOTFS_DIR +for f in $TIZEN_RPM_FILES; do + rpm2cpio $f | cpio -idm --quiet +done +echo "<>Start configuring Tizen rootfs" +rm ./usr/lib/libunwind.so +ln -s libunwind.so.8 ./usr/lib/libunwind.so +ln -sfn asm-arm ./usr/include/asm +patch -p1 < $__TIZEN_CROSSDIR/tizen.patch +echo "</dev/null; then + VERBOSE=0 +fi + +Log() +{ + if [ $VERBOSE -ge $1 ]; then + echo ${@:2} + fi +} + +Inform() +{ + Log 1 -e "\x1B[0;34m$@\x1B[m" +} + +Debug() +{ + Log 2 -e "\x1B[0;32m$@\x1B[m" +} + +Error() +{ + >&2 Log 0 -e "\x1B[0;31m$@\x1B[m" +} + +Fetch() +{ + URL=$1 + FILE=$2 + PROGRESS=$3 + if [ $VERBOSE -ge 1 ] && [ $PROGRESS ]; then + CURL_OPT="--progress-bar" + else + CURL_OPT="--silent" + fi + curl $CURL_OPT $URL > $FILE +} + +hash curl 2> /dev/null || { Error "Require 'curl' Aborting."; exit 1; } +hash xmllint 2> /dev/null || { Error "Require 'xmllint' Aborting."; exit 1; } +hash sha256sum 2> /dev/null || { Error "Require 'sha256sum' Aborting."; exit 1; } + +TMPDIR=$1 +if [ ! -d $TMPDIR ]; then + TMPDIR=./tizen_tmp + Debug "Create temporary directory : $TMPDIR" + mkdir -p $TMPDIR +fi + +TIZEN_URL=http://download.tizen.org/releases/milestone/tizen +BUILD_XML=build.xml +REPOMD_XML=repomd.xml +PRIMARY_XML=primary.xml +TARGET_URL="http://__not_initialized" + +Xpath_get() +{ + XPATH_RESULT='' + XPATH=$1 + XML_FILE=$2 + RESULT=$(xmllint --xpath $XPATH $XML_FILE) + if [[ -z ${RESULT// } ]]; then + Error "Can not find target from $XML_FILE" + Debug "Xpath = $XPATH" + exit 1 + fi + XPATH_RESULT=$RESULT +} + +fetch_tizen_pkgs_init() +{ + TARGET=$1 + PROFILE=$2 + Debug "Initialize TARGET=$TARGET, PROFILE=$PROFILE" + + TMP_PKG_DIR=$TMPDIR/tizen_${PROFILE}_pkgs + if [ -d $TMP_PKG_DIR ]; then rm -rf $TMP_PKG_DIR; fi + mkdir -p $TMP_PKG_DIR + + PKG_URL=$TIZEN_URL/$PROFILE/latest + + BUILD_XML_URL=$PKG_URL/$BUILD_XML + TMP_BUILD=$TMP_PKG_DIR/$BUILD_XML + TMP_REPOMD=$TMP_PKG_DIR/$REPOMD_XML + TMP_PRIMARY=$TMP_PKG_DIR/$PRIMARY_XML + TMP_PRIMARYGZ=${TMP_PRIMARY}.gz + + Fetch $BUILD_XML_URL $TMP_BUILD + + Debug "fetch $BUILD_XML_URL to $TMP_BUILD" + + TARGET_XPATH="//build/buildtargets/buildtarget[@name=\"$TARGET\"]/repo[@type=\"binary\"]/text()" + Xpath_get $TARGET_XPATH $TMP_BUILD + TARGET_PATH=$XPATH_RESULT + TARGET_URL=$PKG_URL/$TARGET_PATH + + REPOMD_URL=$TARGET_URL/repodata/repomd.xml + PRIMARY_XPATH='string(//*[local-name()="data"][@type="primary"]/*[local-name()="location"]/@href)' + + Fetch $REPOMD_URL $TMP_REPOMD + + Debug "fetch $REPOMD_URL to $TMP_REPOMD" + + Xpath_get $PRIMARY_XPATH $TMP_REPOMD + PRIMARY_XML_PATH=$XPATH_RESULT + PRIMARY_URL=$TARGET_URL/$PRIMARY_XML_PATH + + Fetch $PRIMARY_URL $TMP_PRIMARYGZ + + Debug "fetch $PRIMARY_URL to $TMP_PRIMARYGZ" + + gunzip $TMP_PRIMARYGZ + + Debug "unzip $TMP_PRIMARYGZ to $TMP_PRIMARY" +} + +fetch_tizen_pkgs() +{ + ARCH=$1 + PACKAGE_XPATH_TPL='string(//*[local-name()="metadata"]/*[local-name()="package"][*[local-name()="name"][text()="_PKG_"]][*[local-name()="arch"][text()="_ARCH_"]]/*[local-name()="location"]/@href)' + + PACKAGE_CHECKSUM_XPATH_TPL='string(//*[local-name()="metadata"]/*[local-name()="package"][*[local-name()="name"][text()="_PKG_"]][*[local-name()="arch"][text()="_ARCH_"]]/*[local-name()="checksum"]/text())' + + for pkg in ${@:2} + do + Inform "Fetching... $pkg" + XPATH=${PACKAGE_XPATH_TPL/_PKG_/$pkg} + XPATH=${XPATH/_ARCH_/$ARCH} + Xpath_get $XPATH $TMP_PRIMARY + PKG_PATH=$XPATH_RESULT + + XPATH=${PACKAGE_CHECKSUM_XPATH_TPL/_PKG_/$pkg} + XPATH=${XPATH/_ARCH_/$ARCH} + Xpath_get $XPATH $TMP_PRIMARY + CHECKSUM=$XPATH_RESULT + + PKG_URL=$TARGET_URL/$PKG_PATH + PKG_FILE=$(basename $PKG_PATH) + PKG_PATH=$TMPDIR/$PKG_FILE + + Debug "Download $PKG_URL to $PKG_PATH" + Fetch $PKG_URL $PKG_PATH true + + echo "$CHECKSUM $PKG_PATH" | sha256sum -c - > /dev/null + if [ $? -ne 0 ]; then + Error "Fail to fetch $PKG_URL to $PKG_PATH" + Debug "Checksum = $CHECKSUM" + exit 1 + fi + done +} + +Inform "Initialize arm base" +fetch_tizen_pkgs_init standard base +Inform "fetch common packages" +fetch_tizen_pkgs armv7l gcc glibc glibc-devel libicu libicu-devel libatomic +fetch_tizen_pkgs noarch linux-glibc-devel +Inform "fetch coreclr packages" +fetch_tizen_pkgs armv7l lldb lldb-devel libgcc libstdc++ libstdc++-devel libunwind libunwind-devel lttng-ust-devel lttng-ust userspace-rcu-devel userspace-rcu +Inform "fetch corefx packages" +fetch_tizen_pkgs armv7l libcom_err libcom_err-devel zlib zlib-devel libopenssl libopenssl-devel krb5 krb5-devel libcurl libcurl-devel + +Inform "Initialize standard unified" +fetch_tizen_pkgs_init standard unified +Inform "fetch corefx packages" +fetch_tizen_pkgs armv7l gssdp gssdp-devel tizen-release + diff --git a/eng/common/cross/armel/tizen/tizen-dotnet.ks b/eng/common/cross/armel/tizen/tizen-dotnet.ks new file mode 100644 index 000000000..506d455bd --- /dev/null +++ b/eng/common/cross/armel/tizen/tizen-dotnet.ks @@ -0,0 +1,50 @@ +lang en_US.UTF-8 +keyboard us +timezone --utc Asia/Seoul + +part / --fstype="ext4" --size=3500 --ondisk=mmcblk0 --label rootfs --fsoptions=defaults,noatime + +rootpw tizen +desktop --autologinuser=root +user --name root --groups audio,video --password 'tizen' + +repo --name=standard --baseurl=http://download.tizen.org/releases/milestone/tizen/unified/latest/repos/standard/packages/ --ssl_verify=no +repo --name=base --baseurl=http://download.tizen.org/releases/milestone/tizen/base/latest/repos/standard/packages/ --ssl_verify=no + +%packages +tar +gzip + +sed +grep +gawk +perl + +binutils +findutils +util-linux +lttng-ust +userspace-rcu +procps-ng +tzdata +ca-certificates + + +### Core FX +libicu +libunwind +iputils +zlib +krb5 +libcurl +libopenssl + +%end + +%post + +### Update /tmp privilege +chmod 777 /tmp +#################################### + +%end diff --git a/eng/common/cross/armel/tizen/tizen.patch b/eng/common/cross/armel/tizen/tizen.patch new file mode 100644 index 000000000..d223427c9 --- /dev/null +++ b/eng/common/cross/armel/tizen/tizen.patch @@ -0,0 +1,18 @@ +diff -u -r a/usr/lib/libc.so b/usr/lib/libc.so +--- a/usr/lib/libc.so 2016-12-30 23:00:08.284951863 +0900 ++++ b/usr/lib/libc.so 2016-12-30 23:00:32.140951815 +0900 +@@ -2,4 +2,4 @@ + Use the shared library, but some functions are only in + the static library, so try that secondarily. */ + OUTPUT_FORMAT(elf32-littlearm) +-GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a AS_NEEDED ( /lib/ld-linux.so.3 ) ) ++GROUP ( libc.so.6 libc_nonshared.a AS_NEEDED ( ld-linux.so.3 ) ) +diff -u -r a/usr/lib/libpthread.so b/usr/lib/libpthread.so +--- a/usr/lib/libpthread.so 2016-12-30 23:00:19.408951841 +0900 ++++ b/usr/lib/libpthread.so 2016-12-30 23:00:39.068951801 +0900 +@@ -2,4 +2,4 @@ + Use the shared library, but some functions are only in + the static library, so try that secondarily. */ + OUTPUT_FORMAT(elf32-littlearm) +-GROUP ( /lib/libpthread.so.0 /usr/lib/libpthread_nonshared.a ) ++GROUP ( libpthread.so.0 libpthread_nonshared.a ) diff --git a/eng/common/cross/build-android-rootfs.sh b/eng/common/cross/build-android-rootfs.sh new file mode 100755 index 000000000..adceda877 --- /dev/null +++ b/eng/common/cross/build-android-rootfs.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +set -e +__NDK_Version=r14 + +usage() +{ + echo "Creates a toolchain and sysroot used for cross-compiling for Android." + echo. + echo "Usage: $0 [BuildArch] [ApiLevel]" + echo. + echo "BuildArch is the target architecture of Android. Currently only arm64 is supported." + echo "ApiLevel is the target Android API level. API levels usually match to Android releases. See https://source.android.com/source/build-numbers.html" + echo. + echo "By default, the toolchain and sysroot will be generated in cross/android-rootfs/toolchain/[BuildArch]. You can change this behavior" + echo "by setting the TOOLCHAIN_DIR environment variable" + echo. + echo "By default, the NDK will be downloaded into the cross/android-rootfs/android-ndk-$__NDK_Version directory. If you already have an NDK installation," + echo "you can set the NDK_DIR environment variable to have this script use that installation of the NDK." + echo "By default, this script will generate a file, android_platform, in the root of the ROOTFS_DIR directory that contains the RID for the supported and tested Android build: android.21-arm64. This file is to replace '/etc/os-release', which is not available for Android." + exit 1 +} + +__ApiLevel=21 # The minimum platform for arm64 is API level 21 +__BuildArch=arm64 +__AndroidArch=aarch64 +__AndroidToolchain=aarch64-linux-android + +for i in "$@" + do + lowerI="$(echo $i | awk '{print tolower($0)}')" + case $lowerI in + -?|-h|--help) + usage + exit 1 + ;; + arm64) + __BuildArch=arm64 + __AndroidArch=aarch64 + __AndroidToolchain=aarch64-linux-android + ;; + arm) + __BuildArch=arm + __AndroidArch=arm + __AndroidToolchain=arm-linux-androideabi + ;; + *[0-9]) + __ApiLevel=$i + ;; + *) + __UnprocessedBuildArgs="$__UnprocessedBuildArgs $i" + ;; + esac +done + +# Obtain the location of the bash script to figure out where the root of the repo is. +__CrossDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +__Android_Cross_Dir="$__CrossDir/android-rootfs" +__NDK_Dir="$__Android_Cross_Dir/android-ndk-$__NDK_Version" +__libunwind_Dir="$__Android_Cross_Dir/libunwind" +__lldb_Dir="$__Android_Cross_Dir/lldb" +__ToolchainDir="$__Android_Cross_Dir/toolchain/$__BuildArch" + +if [[ -n "$TOOLCHAIN_DIR" ]]; then + __ToolchainDir=$TOOLCHAIN_DIR +fi + +if [[ -n "$NDK_DIR" ]]; then + __NDK_Dir=$NDK_DIR +fi + +echo "Target API level: $__ApiLevel" +echo "Target architecture: $__BuildArch" +echo "NDK location: $__NDK_Dir" +echo "Target Toolchain location: $__ToolchainDir" + +# Download the NDK if required +if [ ! -d $__NDK_Dir ]; then + echo Downloading the NDK into $__NDK_Dir + mkdir -p $__NDK_Dir + wget -nv -nc --show-progress https://dl.google.com/android/repository/android-ndk-$__NDK_Version-linux-x86_64.zip -O $__Android_Cross_Dir/android-ndk-$__NDK_Version-linux-x86_64.zip + unzip -q $__Android_Cross_Dir/android-ndk-$__NDK_Version-linux-x86_64.zip -d $__Android_Cross_Dir +fi + +if [ ! -d $__lldb_Dir ]; then + mkdir -p $__lldb_Dir + echo Downloading LLDB into $__lldb_Dir + wget -nv -nc --show-progress https://dl.google.com/android/repository/lldb-2.3.3614996-linux-x86_64.zip -O $__Android_Cross_Dir/lldb-2.3.3614996-linux-x86_64.zip + unzip -q $__Android_Cross_Dir/lldb-2.3.3614996-linux-x86_64.zip -d $__lldb_Dir +fi + +# Create the RootFS for both arm64 as well as aarch +rm -rf $__Android_Cross_Dir/toolchain + +echo Generating the $__BuildArch toolchain +$__NDK_Dir/build/tools/make_standalone_toolchain.py --arch $__BuildArch --api $__ApiLevel --install-dir $__ToolchainDir + +# Install the required packages into the toolchain +# TODO: Add logic to get latest pkg version instead of specific version number +rm -rf $__Android_Cross_Dir/deb/ +rm -rf $__Android_Cross_Dir/tmp + +mkdir -p $__Android_Cross_Dir/deb/ +mkdir -p $__Android_Cross_Dir/tmp/$arch/ +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libicu_60.2_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libicu_60.2_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libicu-dev_60.2_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libicu-dev_60.2_$__AndroidArch.deb + +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libandroid-glob-dev_0.4_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libandroid-glob-dev_0.4_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libandroid-glob_0.4_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libandroid-glob_0.4_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libandroid-support-dev_22_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libandroid-support-dev_22_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libandroid-support_22_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libandroid-support_22_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/liblzma-dev_5.2.3_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/liblzma-dev_5.2.3_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/liblzma_5.2.3_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/liblzma_5.2.3_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libunwind-dev_1.2.20170304_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libunwind-dev_1.2.20170304_$__AndroidArch.deb +wget -nv -nc http://termux.net/dists/stable/main/binary-$__AndroidArch/libunwind_1.2.20170304_$__AndroidArch.deb -O $__Android_Cross_Dir/deb/libunwind_1.2.20170304_$__AndroidArch.deb + +echo Unpacking Termux packages +dpkg -x $__Android_Cross_Dir/deb/libicu_60.2_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libicu-dev_60.2_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libandroid-glob-dev_0.4_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libandroid-glob_0.4_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libandroid-support-dev_22_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libandroid-support_22_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/liblzma-dev_5.2.3_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/liblzma_5.2.3_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libunwind-dev_1.2.20170304_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ +dpkg -x $__Android_Cross_Dir/deb/libunwind_1.2.20170304_$__AndroidArch.deb $__Android_Cross_Dir/tmp/$__AndroidArch/ + +cp -R $__Android_Cross_Dir/tmp/$__AndroidArch/data/data/com.termux/files/usr/* $__ToolchainDir/sysroot/usr/ + +# Generate platform file for build.sh script to assign to __DistroRid +echo "Generating platform file..." + +echo "RID=android.21-arm64" > $__ToolchainDir/sysroot/android_platform +echo Now run: +echo CONFIG_DIR=\`realpath cross/android/$__BuildArch\` ROOTFS_DIR=\`realpath $__ToolchainDir/sysroot\` ./build.sh cross $__BuildArch skipgenerateversion skipnuget cmakeargs -DENABLE_LLDBPLUGIN=0 + diff --git a/eng/common/cross/build-rootfs.sh b/eng/common/cross/build-rootfs.sh new file mode 100755 index 000000000..d7d5d7d5f --- /dev/null +++ b/eng/common/cross/build-rootfs.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash + +usage() +{ + echo "Usage: $0 [BuildArch] [LinuxCodeName] [lldbx.y] [--skipunmount] --rootfsdir ]" + echo "BuildArch can be: arm(default), armel, arm64, x86" + echo "LinuxCodeName - optional, Code name for Linux, can be: trusty, xenial(default), zesty, bionic, alpine. If BuildArch is armel, LinuxCodeName is jessie(default) or tizen." + echo "lldbx.y - optional, LLDB version, can be: lldb3.9(default), lldb4.0, lldb5.0, lldb6.0 no-lldb. Ignored for alpine" + echo "--skipunmount - optional, will skip the unmount of rootfs folder." + exit 1 +} + +__LinuxCodeName=xenial +__CrossDir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +__InitialDir=$PWD +__BuildArch=arm +__UbuntuArch=armhf +__UbuntuRepo="http://ports.ubuntu.com/" +__LLDB_Package="liblldb-3.9-dev" +__SkipUnmount=0 + +# base development support +__UbuntuPackages="build-essential" + +__AlpinePackages="alpine-base" +__AlpinePackages+=" build-base" +__AlpinePackages+=" linux-headers" +__AlpinePackages+=" lldb-dev" +__AlpinePackages+=" llvm-dev" + +# symlinks fixer +__UbuntuPackages+=" symlinks" + +# CoreCLR and CoreFX dependencies +__UbuntuPackages+=" libicu-dev" +__UbuntuPackages+=" liblttng-ust-dev" +__UbuntuPackages+=" libunwind8-dev" + +__AlpinePackages+=" gettext-dev" +__AlpinePackages+=" icu-dev" +__AlpinePackages+=" libunwind-dev" +__AlpinePackages+=" lttng-ust-dev" + +# CoreFX dependencies +__UbuntuPackages+=" libcurl4-openssl-dev" +__UbuntuPackages+=" libkrb5-dev" +__UbuntuPackages+=" libssl-dev" +__UbuntuPackages+=" zlib1g-dev" + +__AlpinePackages+=" curl-dev" +__AlpinePackages+=" krb5-dev" +__AlpinePackages+=" openssl-dev" +__AlpinePackages+=" zlib-dev" + +__UnprocessedBuildArgs= +while :; do + if [ $# -le 0 ]; then + break + fi + + lowerI="$(echo $1 | awk '{print tolower($0)}')" + case $lowerI in + -?|-h|--help) + usage + exit 1 + ;; + arm) + __BuildArch=arm + __UbuntuArch=armhf + __AlpineArch=armhf + __QEMUArch=arm + ;; + arm64) + __BuildArch=arm64 + __UbuntuArch=arm64 + __AlpineArch=aarch64 + __QEMUArch=aarch64 + ;; + armel) + __BuildArch=armel + __UbuntuArch=armel + __UbuntuRepo="http://ftp.debian.org/debian/" + __LinuxCodeName=jessie + ;; + x86) + __BuildArch=x86 + __UbuntuArch=i386 + __UbuntuRepo="http://archive.ubuntu.com/ubuntu/" + ;; + lldb3.6) + __LLDB_Package="lldb-3.6-dev" + ;; + lldb3.8) + __LLDB_Package="lldb-3.8-dev" + ;; + lldb3.9) + __LLDB_Package="liblldb-3.9-dev" + ;; + lldb4.0) + __LLDB_Package="liblldb-4.0-dev" + ;; + lldb5.0) + __LLDB_Package="liblldb-5.0-dev" + ;; + lldb6.0) + __LLDB_Package="liblldb-6.0-dev" + ;; + no-lldb) + unset __LLDB_Package + ;; + trusty) # Ubuntu 14.04 + if [ "$__LinuxCodeName" != "jessie" ]; then + __LinuxCodeName=trusty + fi + ;; + xenial) # Ubuntu 16.04 + if [ "$__LinuxCodeName" != "jessie" ]; then + __LinuxCodeName=xenial + fi + ;; + zesty) # Ubuntu 17.04 + if [ "$__LinuxCodeName" != "jessie" ]; then + __LinuxCodeName=zesty + fi + ;; + bionic) # Ubuntu 18.04 + if [ "$__LinuxCodeName" != "jessie" ]; then + __LinuxCodeName=bionic + fi + ;; + jessie) # Debian 8 + __LinuxCodeName=jessie + __UbuntuRepo="http://ftp.debian.org/debian/" + ;; + stretch) # Debian 9 + __LinuxCodeName=stretch + __UbuntuRepo="http://ftp.debian.org/debian/" + __LLDB_Package="liblldb-6.0-dev" + ;; + buster) # Debian 10 + __LinuxCodeName=buster + __UbuntuRepo="http://ftp.debian.org/debian/" + __LLDB_Package="liblldb-6.0-dev" + ;; + tizen) + if [ "$__BuildArch" != "armel" ]; then + echo "Tizen is available only for armel." + usage; + exit 1; + fi + __LinuxCodeName= + __UbuntuRepo= + __Tizen=tizen + ;; + alpine) + __LinuxCodeName=alpine + __UbuntuRepo= + ;; + --skipunmount) + __SkipUnmount=1 + ;; + --rootfsdir|-rootfsdir) + shift + __RootfsDir=$1 + ;; + *) + __UnprocessedBuildArgs="$__UnprocessedBuildArgs $1" + ;; + esac + + shift +done + +if [ "$__BuildArch" == "armel" ]; then + __LLDB_Package="lldb-3.5-dev" +fi +__UbuntuPackages+=" ${__LLDB_Package:-}" + +if [ -z "$__RootfsDir" ] && [ ! -z "$ROOTFS_DIR" ]; then + __RootfsDir=$ROOTFS_DIR +fi + +if [ -z "$__RootfsDir" ]; then + __RootfsDir="$__CrossDir/../../../.tools/rootfs/$__BuildArch" +fi + +if [ -d "$__RootfsDir" ]; then + if [ $__SkipUnmount == 0 ]; then + umount $__RootfsDir/* + fi + rm -rf $__RootfsDir +fi + +if [[ "$__LinuxCodeName" == "alpine" ]]; then + __ApkToolsVersion=2.9.1 + __AlpineVersion=3.7 + __ApkToolsDir=$(mktemp -d) + wget https://github.com/alpinelinux/apk-tools/releases/download/v$__ApkToolsVersion/apk-tools-$__ApkToolsVersion-x86_64-linux.tar.gz -P $__ApkToolsDir + tar -xf $__ApkToolsDir/apk-tools-$__ApkToolsVersion-x86_64-linux.tar.gz -C $__ApkToolsDir + mkdir -p $__RootfsDir/usr/bin + cp -v /usr/bin/qemu-$__QEMUArch-static $__RootfsDir/usr/bin + $__ApkToolsDir/apk-tools-$__ApkToolsVersion/apk \ + -X http://dl-cdn.alpinelinux.org/alpine/v$__AlpineVersion/main \ + -X http://dl-cdn.alpinelinux.org/alpine/v$__AlpineVersion/community \ + -X http://dl-cdn.alpinelinux.org/alpine/edge/testing \ + -X http://dl-cdn.alpinelinux.org/alpine/edge/main \ + -U --allow-untrusted --root $__RootfsDir --arch $__AlpineArch --initdb \ + add $__AlpinePackages + rm -r $__ApkToolsDir +elif [[ -n $__LinuxCodeName ]]; then + qemu-debootstrap --arch $__UbuntuArch $__LinuxCodeName $__RootfsDir $__UbuntuRepo + cp $__CrossDir/$__BuildArch/sources.list.$__LinuxCodeName $__RootfsDir/etc/apt/sources.list + chroot $__RootfsDir apt-get update + chroot $__RootfsDir apt-get -f -y install + chroot $__RootfsDir apt-get -y install $__UbuntuPackages + chroot $__RootfsDir symlinks -cr /usr + + if [ $__SkipUnmount == 0 ]; then + umount $__RootfsDir/* + fi + + if [[ "$__BuildArch" == "arm" && "$__LinuxCodeName" == "trusty" ]]; then + pushd $__RootfsDir + patch -p1 < $__CrossDir/$__BuildArch/trusty.patch + patch -p1 < $__CrossDir/$__BuildArch/trusty-lttng-2.4.patch + popd + fi +elif [ "$__Tizen" == "tizen" ]; then + ROOTFS_DIR=$__RootfsDir $__CrossDir/$__BuildArch/tizen-build-rootfs.sh +else + echo "Unsupported target platform." + usage; + exit 1 +fi diff --git a/eng/common/cross/toolchain.cmake b/eng/common/cross/toolchain.cmake new file mode 100644 index 000000000..071d41124 --- /dev/null +++ b/eng/common/cross/toolchain.cmake @@ -0,0 +1,138 @@ +set(CROSS_ROOTFS $ENV{ROOTFS_DIR}) + +set(TARGET_ARCH_NAME $ENV{TARGET_BUILD_ARCH}) +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_VERSION 1) + +if(TARGET_ARCH_NAME STREQUAL "armel") + set(CMAKE_SYSTEM_PROCESSOR armv7l) + set(TOOLCHAIN "arm-linux-gnueabi") + if("$ENV{__DistroRid}" MATCHES "tizen.*") + set(TIZEN_TOOLCHAIN "armv7l-tizen-linux-gnueabi/6.2.1") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "arm") + set(CMAKE_SYSTEM_PROCESSOR armv7l) + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/armv6-alpine-linux-musleabihf) + set(TOOLCHAIN "armv6-alpine-linux-musleabihf") + else() + set(TOOLCHAIN "arm-linux-gnueabihf") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "arm64") + set(CMAKE_SYSTEM_PROCESSOR aarch64) + if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/aarch64-alpine-linux-musl) + set(TOOLCHAIN "aarch64-alpine-linux-musl") + else() + set(TOOLCHAIN "aarch64-linux-gnu") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "x86") + set(CMAKE_SYSTEM_PROCESSOR i686) + set(TOOLCHAIN "i686-linux-gnu") +else() + message(FATAL_ERROR "Arch is ${TARGET_ARCH_NAME}. Only armel, arm, arm64 and x86 are supported!") +endif() + +# Specify include paths +if(TARGET_ARCH_NAME STREQUAL "armel") + if(DEFINED TIZEN_TOOLCHAIN) + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}/include/c++/) + include_directories(SYSTEM ${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}/include/c++/armv7l-tizen-linux-gnueabi) + endif() +endif() + +# add_compile_param - adds only new options without duplicates. +# arg0 - list with result options, arg1 - list with new options. +# arg2 - optional argument, quick summary string for optional using CACHE FORCE mode. +macro(add_compile_param) + if(NOT ${ARGC} MATCHES "^(2|3)$") + message(FATAL_ERROR "Wrong using add_compile_param! Two or three parameters must be given! See add_compile_param description.") + endif() + foreach(OPTION ${ARGV1}) + if(NOT ${ARGV0} MATCHES "${OPTION}($| )") + set(${ARGV0} "${${ARGV0}} ${OPTION}") + if(${ARGC} EQUAL "3") # CACHE FORCE mode + set(${ARGV0} "${${ARGV0}}" CACHE STRING "${ARGV2}" FORCE) + endif() + endif() + endforeach() +endmacro() + +# Specify link flags +add_compile_param(CROSS_LINK_FLAGS "--sysroot=${CROSS_ROOTFS}") +add_compile_param(CROSS_LINK_FLAGS "--gcc-toolchain=${CROSS_ROOTFS}/usr") +add_compile_param(CROSS_LINK_FLAGS "--target=${TOOLCHAIN}") +add_compile_param(CROSS_LINK_FLAGS "-fuse-ld=gold") + +if(TARGET_ARCH_NAME STREQUAL "armel") + if(DEFINED TIZEN_TOOLCHAIN) # For Tizen only + add_compile_param(CROSS_LINK_FLAGS "-B${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}") + add_compile_param(CROSS_LINK_FLAGS "-L${CROSS_ROOTFS}/lib") + add_compile_param(CROSS_LINK_FLAGS "-L${CROSS_ROOTFS}/usr/lib") + add_compile_param(CROSS_LINK_FLAGS "-L${CROSS_ROOTFS}/usr/lib/gcc/${TIZEN_TOOLCHAIN}") + endif() +elseif(TARGET_ARCH_NAME STREQUAL "x86") + add_compile_param(CROSS_LINK_FLAGS "-m32") +endif() + +add_compile_param(CMAKE_EXE_LINKER_FLAGS "${CROSS_LINK_FLAGS}" "TOOLCHAIN_EXE_LINKER_FLAGS") +add_compile_param(CMAKE_SHARED_LINKER_FLAGS "${CROSS_LINK_FLAGS}" "TOOLCHAIN_EXE_LINKER_FLAGS") +add_compile_param(CMAKE_MODULE_LINKER_FLAGS "${CROSS_LINK_FLAGS}" "TOOLCHAIN_EXE_LINKER_FLAGS") + +# Specify compile options +add_compile_options("--sysroot=${CROSS_ROOTFS}") +add_compile_options("--target=${TOOLCHAIN}") +add_compile_options("--gcc-toolchain=${CROSS_ROOTFS}/usr") + +if(TARGET_ARCH_NAME MATCHES "^(arm|armel|arm64)$") + set(CMAKE_C_COMPILER_TARGET ${TOOLCHAIN}) + set(CMAKE_CXX_COMPILER_TARGET ${TOOLCHAIN}) + set(CMAKE_ASM_COMPILER_TARGET ${TOOLCHAIN}) +endif() + +if(TARGET_ARCH_NAME MATCHES "^(arm|armel)$") + add_compile_options(-mthumb) + add_compile_options(-mfpu=vfpv3) + if(TARGET_ARCH_NAME STREQUAL "armel") + add_compile_options(-mfloat-abi=softfp) + if(DEFINED TIZEN_TOOLCHAIN) + add_compile_options(-Wno-deprecated-declarations) # compile-time option + add_compile_options(-D__extern_always_inline=inline) # compile-time option + endif() + endif() +elseif(TARGET_ARCH_NAME STREQUAL "x86") + add_compile_options(-m32) + add_compile_options(-Wno-error=unused-command-line-argument) +endif() + +# Set LLDB include and library paths +if(TARGET_ARCH_NAME MATCHES "^(arm|armel|x86)$") + if(TARGET_ARCH_NAME STREQUAL "x86") + set(LLVM_CROSS_DIR "$ENV{LLVM_CROSS_HOME}") + else() # arm/armel case + set(LLVM_CROSS_DIR "$ENV{LLVM_ARM_HOME}") + endif() + if(LLVM_CROSS_DIR) + set(WITH_LLDB_LIBS "${LLVM_CROSS_DIR}/lib/" CACHE STRING "") + set(WITH_LLDB_INCLUDES "${LLVM_CROSS_DIR}/include" CACHE STRING "") + set(LLDB_H "${WITH_LLDB_INCLUDES}" CACHE STRING "") + set(LLDB "${LLVM_CROSS_DIR}/lib/liblldb.so" CACHE STRING "") + else() + if(TARGET_ARCH_NAME STREQUAL "x86") + set(WITH_LLDB_LIBS "${CROSS_ROOTFS}/usr/lib/i386-linux-gnu" CACHE STRING "") + set(CHECK_LLVM_DIR "${CROSS_ROOTFS}/usr/lib/llvm-3.8/include") + if(EXISTS "${CHECK_LLVM_DIR}" AND IS_DIRECTORY "${CHECK_LLVM_DIR}") + set(WITH_LLDB_INCLUDES "${CHECK_LLVM_DIR}") + else() + set(WITH_LLDB_INCLUDES "${CROSS_ROOTFS}/usr/lib/llvm-3.6/include") + endif() + else() # arm/armel case + set(WITH_LLDB_LIBS "${CROSS_ROOTFS}/usr/lib/${TOOLCHAIN}" CACHE STRING "") + set(WITH_LLDB_INCLUDES "${CROSS_ROOTFS}/usr/lib/llvm-3.6/include" CACHE STRING "") + endif() + endif() +endif() + +set(CMAKE_FIND_ROOT_PATH "${CROSS_ROOTFS}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/eng/common/cross/x86/sources.list.bionic b/eng/common/cross/x86/sources.list.bionic new file mode 100644 index 000000000..a71ccadcf --- /dev/null +++ b/eng/common/cross/x86/sources.list.bionic @@ -0,0 +1,11 @@ +deb http://archive.ubuntu.com/ubuntu/ bionic main restricted universe +deb-src http://archive.ubuntu.com/ubuntu/ bionic main restricted universe + +deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted universe +deb-src http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted universe + +deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted +deb-src http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted + +deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted universe multiverse +deb-src http://archive.ubuntu.com/ubuntu/ bionic-security main restricted universe multiverse diff --git a/eng/common/cross/x86/sources.list.trusty b/eng/common/cross/x86/sources.list.trusty new file mode 100644 index 000000000..9b3085436 --- /dev/null +++ b/eng/common/cross/x86/sources.list.trusty @@ -0,0 +1,11 @@ +deb http://archive.ubuntu.com/ubuntu/ trusty main restricted universe +deb-src http://archive.ubuntu.com/ubuntu/ trusty main restricted universe + +deb http://archive.ubuntu.com/ubuntu/ trusty-updates main restricted universe +deb-src http://archive.ubuntu.com/ubuntu/ trusty-updates main restricted universe + +deb http://archive.ubuntu.com/ubuntu/ trusty-backports main restricted +deb-src http://archive.ubuntu.com/ubuntu/ trusty-backports main restricted + +deb http://archive.ubuntu.com/ubuntu/ trusty-security main restricted universe multiverse +deb-src http://archive.ubuntu.com/ubuntu/ trusty-security main restricted universe multiverse diff --git a/eng/common/cross/x86/sources.list.xenial b/eng/common/cross/x86/sources.list.xenial new file mode 100644 index 000000000..ad9c5a014 --- /dev/null +++ b/eng/common/cross/x86/sources.list.xenial @@ -0,0 +1,11 @@ +deb http://archive.ubuntu.com/ubuntu/ xenial main restricted universe +deb-src http://archive.ubuntu.com/ubuntu/ xenial main restricted universe + +deb http://archive.ubuntu.com/ubuntu/ xenial-updates main restricted universe +deb-src http://archive.ubuntu.com/ubuntu/ xenial-updates main restricted universe + +deb http://archive.ubuntu.com/ubuntu/ xenial-backports main restricted +deb-src http://archive.ubuntu.com/ubuntu/ xenial-backports main restricted + +deb http://archive.ubuntu.com/ubuntu/ xenial-security main restricted universe multiverse +deb-src http://archive.ubuntu.com/ubuntu/ xenial-security main restricted universe multiverse diff --git a/eng/common/darc-init.ps1 b/eng/common/darc-init.ps1 new file mode 100644 index 000000000..8854d979f --- /dev/null +++ b/eng/common/darc-init.ps1 @@ -0,0 +1,33 @@ +param ( + $darcVersion = $null, + $versionEndpoint = "https://maestro-prod.westus2.cloudapp.azure.com/api/assets/darc-version?api-version=2019-01-16" +) + +$verbosity = "m" +. $PSScriptRoot\tools.ps1 + +function InstallDarcCli ($darcVersion) { + $darcCliPackageName = "microsoft.dotnet.darc" + + $dotnetRoot = InitializeDotNetCli -install:$true + $dotnet = "$dotnetRoot\dotnet.exe" + $toolList = & "$dotnet" tool list -g + + if ($toolList -like "*$darcCliPackageName*") { + & "$dotnet" tool uninstall $darcCliPackageName -g + } + + # If the user didn't explicitly specify the darc version, + # query the Maestro API for the correct version of darc to install. + if (-not $darcVersion) { + $darcVersion = $(Invoke-WebRequest -Uri $versionEndpoint -UseBasicParsing).Content + } + + $arcadeServicesSource = 'https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json' + + Write-Host "Installing Darc CLI version $darcVersion..." + Write-Host "You may need to restart your command window if this is the first dotnet tool you have installed." + & "$dotnet" tool install $darcCliPackageName --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity -g +} + +InstallDarcCli $darcVersion diff --git a/eng/common/darc-init.sh b/eng/common/darc-init.sh new file mode 100755 index 000000000..abdd0bc05 --- /dev/null +++ b/eng/common/darc-init.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +darcVersion='' +versionEndpoint="https://maestro-prod.westus2.cloudapp.azure.com/api/assets/darc-version?api-version=2019-01-16" + +while [[ $# > 0 ]]; do + opt="$(echo "$1" | awk '{print tolower($0)}')" + case "$opt" in + --darcversion) + darcVersion=$2 + shift + ;; + --versionendpoint) + versionEndpoint=$2 + shift + ;; + *) + echo "Invalid argument: $1" + usage + exit 1 + ;; + esac + + shift +done + +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" +verbosity=m + +. "$scriptroot/tools.sh" + +if [ -z "$darcVersion" ]; then + darcVersion=$(curl -X GET "$versionEndpoint" -H "accept: text/plain") +fi + +function InstallDarcCli { + local darc_cli_package_name="microsoft.dotnet.darc" + + InitializeDotNetCli + local dotnet_root=$_InitializeDotNetCli + + local uninstall_command=`$dotnet_root/dotnet tool uninstall $darc_cli_package_name -g` + local tool_list=$($dotnet_root/dotnet tool list -g) + if [[ $tool_list = *$darc_cli_package_name* ]]; then + echo $($dotnet_root/dotnet tool uninstall $darc_cli_package_name -g) + fi + + local arcadeServicesSource="https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json" + + echo "Installing Darc CLI version $darcVersion..." + echo "You may need to restart your command shell if this is the first dotnet tool you have installed." + echo $($dotnet_root/dotnet tool install $darc_cli_package_name --version $darcVersion --add-source "$arcadeServicesSource" -v $verbosity -g) +} + +InstallDarcCli diff --git a/eng/common/dotnet-install.cmd b/eng/common/dotnet-install.cmd new file mode 100644 index 000000000..b1c2642e7 --- /dev/null +++ b/eng/common/dotnet-install.cmd @@ -0,0 +1,2 @@ +@echo off +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0dotnet-install.ps1""" %*" \ No newline at end of file diff --git a/eng/common/dotnet-install.ps1 b/eng/common/dotnet-install.ps1 new file mode 100644 index 000000000..0b629b830 --- /dev/null +++ b/eng/common/dotnet-install.ps1 @@ -0,0 +1,27 @@ +[CmdletBinding(PositionalBinding=$false)] +Param( + [string] $verbosity = "minimal", + [string] $architecture = "", + [string] $version = "Latest", + [string] $runtime = "dotnet" +) + +. $PSScriptRoot\tools.ps1 + +$dotnetRoot = Join-Path $RepoRoot ".dotnet" + +$installdir = $dotnetRoot +try { + if ($architecture -and $architecture.Trim() -eq "x86") { + $installdir = Join-Path $installdir "x86" + } + InstallDotNet $installdir $version $architecture $runtime $true +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} + +ExitWithExitCode 0 diff --git a/eng/common/dotnet-install.sh b/eng/common/dotnet-install.sh new file mode 100755 index 000000000..c3072c958 --- /dev/null +++ b/eng/common/dotnet-install.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +version='Latest' +architecture='' +runtime='dotnet' +while [[ $# > 0 ]]; do + opt="$(echo "$1" | awk '{print tolower($0)}')" + case "$opt" in + -version|-v) + shift + version="$1" + ;; + -architecture|-a) + shift + architecture="$1" + ;; + -runtime|-r) + shift + runtime="$1" + ;; + *) + echo "Invalid argument: $1" + usage + exit 1 + ;; + esac + shift +done + +. "$scriptroot/tools.sh" +dotnetRoot="$repo_root/.dotnet" +InstallDotNet $dotnetRoot $version "$architecture" $runtime true || { + local exit_code=$? + echo "dotnet-install.sh failed (exit code '$exit_code')." >&2 + ExitWithExitCode $exit_code +} + +ExitWithExitCode 0 diff --git a/eng/common/generate-graph-files.ps1 b/eng/common/generate-graph-files.ps1 new file mode 100644 index 000000000..b056e4c1a --- /dev/null +++ b/eng/common/generate-graph-files.ps1 @@ -0,0 +1,87 @@ +Param( + [Parameter(Mandatory=$true)][string] $barToken, # Token generated at https://maestro-prod.westus2.cloudapp.azure.com/Account/Tokens + [Parameter(Mandatory=$true)][string] $gitHubPat, # GitHub personal access token from https://github.com/settings/tokens (no auth scopes needed) + [Parameter(Mandatory=$true)][string] $azdoPat, # Azure Dev Ops tokens from https://dev.azure.com/dnceng/_details/security/tokens (code read scope needed) + [Parameter(Mandatory=$true)][string] $outputFolder, # Where the graphviz.txt file will be created + [string] $darcVersion = '1.1.0-beta.19175.6', # darc's version + [string] $graphvizVersion = '2.38', # GraphViz version + [switch] $includeToolset # Whether the graph should include toolset dependencies or not. i.e. arcade, optimization. For more about + # toolset dependencies see https://github.com/dotnet/arcade/blob/master/Documentation/Darc.md#toolset-vs-product-dependencies +) + +$ErrorActionPreference = "Stop" +. $PSScriptRoot\tools.ps1 + +Import-Module -Name (Join-Path $PSScriptRoot "native\CommonLibrary.psm1") + +function CheckExitCode ([string]$stage) +{ + $exitCode = $LASTEXITCODE + if ($exitCode -ne 0) { + Write-Host "Something failed in stage: '$stage'. Check for errors above. Exiting now..." + ExitWithExitCode $exitCode + } +} + +try { + Push-Location $PSScriptRoot + + Write-Host "Installing darc..." + . .\darc-init.ps1 -darcVersion $darcVersion + CheckExitCode "Running darc-init" + + $engCommonBaseDir = Join-Path $PSScriptRoot "native\" + $graphvizInstallDir = CommonLibrary\Get-NativeInstallDirectory + $nativeToolBaseUri = "https://netcorenativeassets.blob.core.windows.net/resource-packages/external" + $installBin = Join-Path $graphvizInstallDir "bin" + + Write-Host "Installing dot..." + .\native\install-tool.ps1 -ToolName graphviz -InstallPath $installBin -BaseUri $nativeToolBaseUri -CommonLibraryDirectory $engCommonBaseDir -Version $graphvizVersion -Verbose + + $darcExe = "$env:USERPROFILE\.dotnet\tools" + $darcExe = Resolve-Path "$darcExe\darc.exe" + + Create-Directory $outputFolder + + # Generate 3 graph descriptions: + # 1. Flat with coherency information + # 2. Graphviz (dot) file + # 3. Standard dependency graph + $graphVizFilePath = "$outputFolder\graphviz.txt" + $graphVizImageFilePath = "$outputFolder\graph.png" + $normalGraphFilePath = "$outputFolder\graph-full.txt" + $flatGraphFilePath = "$outputFolder\graph-flat.txt" + $baseOptions = @( "--github-pat", "$gitHubPat", "--azdev-pat", "$azdoPat", "--password", "$barToken" ) + + if ($includeToolset) { + Write-Host "Toolsets will be included in the graph..." + $baseOptions += @( "--include-toolset" ) + } + + Write-Host "Generating standard dependency graph..." + & "$darcExe" get-dependency-graph @baseOptions --output-file $normalGraphFilePath + CheckExitCode "Generating normal dependency graph" + + Write-Host "Generating flat dependency graph and graphviz file..." + & "$darcExe" get-dependency-graph @baseOptions --flat --coherency --graphviz $graphVizFilePath --output-file $flatGraphFilePath + CheckExitCode "Generating flat and graphviz dependency graph" + + Write-Host "Generating graph image $graphVizFilePath" + $dotFilePath = Join-Path $installBin "graphviz\$graphvizVersion\release\bin\dot.exe" + & "$dotFilePath" -Tpng -o"$graphVizImageFilePath" "$graphVizFilePath" + CheckExitCode "Generating graphviz image" + + Write-Host "'$graphVizFilePath', '$flatGraphFilePath', '$normalGraphFilePath' and '$graphVizImageFilePath' created!" +} +catch { + if (!$includeToolset) { + Write-Host "This might be a toolset repo which includes only toolset dependencies. " -NoNewline -ForegroundColor Yellow + Write-Host "Since -includeToolset is not set there is no graph to create. Include -includeToolset and try again..." -ForegroundColor Yellow + } + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} finally { + Pop-Location +} \ No newline at end of file diff --git a/eng/common/helixpublish.proj b/eng/common/helixpublish.proj new file mode 100644 index 000000000..d7f185856 --- /dev/null +++ b/eng/common/helixpublish.proj @@ -0,0 +1,26 @@ + + + + msbuild + + + + + %(Identity) + + + + + + $(WorkItemDirectory) + $(WorkItemCommand) + $(WorkItemTimeout) + + + + + + + + + diff --git a/eng/common/init-tools-native.cmd b/eng/common/init-tools-native.cmd new file mode 100644 index 000000000..438cd548c --- /dev/null +++ b/eng/common/init-tools-native.cmd @@ -0,0 +1,3 @@ +@echo off +powershell -NoProfile -NoLogo -ExecutionPolicy ByPass -command "& """%~dp0init-tools-native.ps1""" %*" +exit /b %ErrorLevel% \ No newline at end of file diff --git a/eng/common/init-tools-native.ps1 b/eng/common/init-tools-native.ps1 new file mode 100644 index 000000000..8cf18bcfe --- /dev/null +++ b/eng/common/init-tools-native.ps1 @@ -0,0 +1,147 @@ +<# +.SYNOPSIS +Entry point script for installing native tools + +.DESCRIPTION +Reads $RepoRoot\global.json file to determine native assets to install +and executes installers for those tools + +.PARAMETER BaseUri +Base file directory or Url from which to acquire tool archives + +.PARAMETER InstallDirectory +Directory to install native toolset. This is a command-line override for the default +Install directory precedence order: +- InstallDirectory command-line override +- NETCOREENG_INSTALL_DIRECTORY environment variable +- (default) %USERPROFILE%/.netcoreeng/native + +.PARAMETER Clean +Switch specifying to not install anything, but cleanup native asset folders + +.PARAMETER Force +Clean and then install tools + +.PARAMETER DownloadRetries +Total number of retry attempts + +.PARAMETER RetryWaitTimeInSeconds +Wait time between retry attempts in seconds + +.PARAMETER GlobalJsonFile +File path to global.json file + +.NOTES +#> +[CmdletBinding(PositionalBinding=$false)] +Param ( + [string] $BaseUri = "https://netcorenativeassets.blob.core.windows.net/resource-packages/external", + [string] $InstallDirectory, + [switch] $Clean = $False, + [switch] $Force = $False, + [int] $DownloadRetries = 5, + [int] $RetryWaitTimeInSeconds = 30, + [string] $GlobalJsonFile +) + +if (!$GlobalJsonFile) { + $GlobalJsonFile = Join-Path (Get-Item $PSScriptRoot).Parent.Parent.FullName "global.json" +} + +Set-StrictMode -version 2.0 +$ErrorActionPreference="Stop" + +Import-Module -Name (Join-Path $PSScriptRoot "native\CommonLibrary.psm1") + +try { + # Define verbose switch if undefined + $Verbose = $VerbosePreference -Eq "Continue" + + $EngCommonBaseDir = Join-Path $PSScriptRoot "native\" + $NativeBaseDir = $InstallDirectory + if (!$NativeBaseDir) { + $NativeBaseDir = CommonLibrary\Get-NativeInstallDirectory + } + $Env:CommonLibrary_NativeInstallDir = $NativeBaseDir + $InstallBin = Join-Path $NativeBaseDir "bin" + $InstallerPath = Join-Path $EngCommonBaseDir "install-tool.ps1" + + # Process tools list + Write-Host "Processing $GlobalJsonFile" + If (-Not (Test-Path $GlobalJsonFile)) { + Write-Host "Unable to find '$GlobalJsonFile'" + exit 0 + } + $NativeTools = Get-Content($GlobalJsonFile) -Raw | + ConvertFrom-Json | + Select-Object -Expand "native-tools" -ErrorAction SilentlyContinue + if ($NativeTools) { + $NativeTools.PSObject.Properties | ForEach-Object { + $ToolName = $_.Name + $ToolVersion = $_.Value + $LocalInstallerArguments = @{ ToolName = "$ToolName" } + $LocalInstallerArguments += @{ InstallPath = "$InstallBin" } + $LocalInstallerArguments += @{ BaseUri = "$BaseUri" } + $LocalInstallerArguments += @{ CommonLibraryDirectory = "$EngCommonBaseDir" } + $LocalInstallerArguments += @{ Version = "$ToolVersion" } + + if ($Verbose) { + $LocalInstallerArguments += @{ Verbose = $True } + } + if (Get-Variable 'Force' -ErrorAction 'SilentlyContinue') { + if($Force) { + $LocalInstallerArguments += @{ Force = $True } + } + } + if ($Clean) { + $LocalInstallerArguments += @{ Clean = $True } + } + + Write-Verbose "Installing $ToolName version $ToolVersion" + Write-Verbose "Executing '$InstallerPath $($LocalInstallerArguments.Keys.ForEach({"-$_ '$($LocalInstallerArguments.$_)'"}) -join ' ')'" + & $InstallerPath @LocalInstallerArguments + if ($LASTEXITCODE -Ne "0") { + $errMsg = "$ToolName installation failed" + if ((Get-Variable 'DoNotAbortNativeToolsInstallationOnFailure' -ErrorAction 'SilentlyContinue') -and $DoNotAbortNativeToolsInstallationOnFailure) { + $showNativeToolsWarning = $true + if ((Get-Variable 'DoNotDisplayNativeToolsInstallationWarnings' -ErrorAction 'SilentlyContinue') -and $DoNotDisplayNativeToolsInstallationWarnings) { + $showNativeToolsWarning = $false + } + if ($showNativeToolsWarning) { + Write-Warning $errMsg + } + $toolInstallationFailure = $true + } else { + Write-Error $errMsg + exit 1 + } + } + } + + if ((Get-Variable 'toolInstallationFailure' -ErrorAction 'SilentlyContinue') -and $toolInstallationFailure) { + exit 1 + } + } + else { + Write-Host "No native tools defined in global.json" + exit 0 + } + + if ($Clean) { + exit 0 + } + if (Test-Path $InstallBin) { + Write-Host "Native tools are available from" (Convert-Path -Path $InstallBin) + Write-Host "##vso[task.prependpath]$(Convert-Path -Path $InstallBin)" + } + else { + Write-Error "Native tools install directory does not exist, installation failed" + exit 1 + } + exit 0 +} +catch { + Write-Host $_ + Write-Host $_.Exception + exit 1 +} diff --git a/eng/common/init-tools-native.sh b/eng/common/init-tools-native.sh new file mode 100755 index 000000000..4dafaaca1 --- /dev/null +++ b/eng/common/init-tools-native.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +base_uri='https://netcorenativeassets.blob.core.windows.net/resource-packages/external' +install_directory='' +clean=false +force=false +download_retries=5 +retry_wait_time_seconds=30 +global_json_file="$(dirname "$(dirname "${scriptroot}")")/global.json" +declare -A native_assets + +. $scriptroot/native/common-library.sh + +while (($# > 0)); do + lowerI="$(echo $1 | awk '{print tolower($0)}')" + case $lowerI in + --baseuri) + base_uri=$2 + shift 2 + ;; + --installdirectory) + install_directory=$2 + shift 2 + ;; + --clean) + clean=true + shift 1 + ;; + --force) + force=true + shift 1 + ;; + --downloadretries) + download_retries=$2 + shift 2 + ;; + --retrywaittimeseconds) + retry_wait_time_seconds=$2 + shift 2 + ;; + --help) + echo "Common settings:" + echo " --installdirectory Directory to install native toolset." + echo " This is a command-line override for the default" + echo " Install directory precedence order:" + echo " - InstallDirectory command-line override" + echo " - NETCOREENG_INSTALL_DIRECTORY environment variable" + echo " - (default) %USERPROFILE%/.netcoreeng/native" + echo "" + echo " --clean Switch specifying not to install anything, but cleanup native asset folders" + echo " --force Clean and then install tools" + echo " --help Print help and exit" + echo "" + echo "Advanced settings:" + echo " --baseuri Base URI for where to download native tools from" + echo " --downloadretries Number of times a download should be attempted" + echo " --retrywaittimeseconds Wait time between download attempts" + echo "" + exit 0 + ;; + esac +done + +function ReadGlobalJsonNativeTools { + # Get the native-tools section from the global.json. + local native_tools_section=$(cat $global_json_file | awk '/"native-tools"/,/}/') + # Only extract the contents of the object. + local native_tools_list=$(echo $native_tools_section | awk -F"[{}]" '{print $2}') + native_tools_list=${native_tools_list//[\" ]/} + native_tools_list=$( echo "$native_tools_list" | sed 's/\s//g' | sed 's/,/\n/g' ) + + local old_IFS=$IFS + while read -r line; do + # Lines are of the form: 'tool:version' + IFS=: + while read -r key value; do + native_assets[$key]=$value + done <<< "$line" + done <<< "$native_tools_list" + IFS=$old_IFS + + return 0; +} + +native_base_dir=$install_directory +if [[ -z $install_directory ]]; then + native_base_dir=$(GetNativeInstallDirectory) +fi + +install_bin="${native_base_dir}/bin" + +ReadGlobalJsonNativeTools + +if [[ ${#native_assets[@]} -eq 0 ]]; then + echo "No native tools defined in global.json" + exit 0; +else + native_installer_dir="$scriptroot/native" + for tool in "${!native_assets[@]}" + do + tool_version=${native_assets[$tool]} + installer_name="install-$tool.sh" + installer_command="$native_installer_dir/$installer_name" + installer_command+=" --baseuri $base_uri" + installer_command+=" --installpath $install_bin" + installer_command+=" --version $tool_version" + echo $installer_command + + if [[ $force = true ]]; then + installer_command+=" --force" + fi + + if [[ $clean = true ]]; then + installer_command+=" --clean" + fi + + $installer_command + + if [[ $? != 0 ]]; then + echo "Execution Failed" >&2 + exit 1 + fi + done +fi + +if [[ $clean = true ]]; then + exit 0 +fi + +if [[ -d $install_bin ]]; then + echo "Native tools are available from $install_bin" + echo "##vso[task.prependpath]$install_bin" +else + echo "Native tools install directory does not exist, installation failed" >&2 + exit 1 +fi + +exit 0 diff --git a/eng/common/internal-feed-operations.ps1 b/eng/common/internal-feed-operations.ps1 new file mode 100644 index 000000000..8b8bafd6a --- /dev/null +++ b/eng/common/internal-feed-operations.ps1 @@ -0,0 +1,135 @@ +param( + [Parameter(Mandatory=$true)][string] $Operation, + [string] $AuthToken, + [string] $CommitSha, + [string] $RepoName, + [switch] $IsFeedPrivate +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 + +. $PSScriptRoot\tools.ps1 + +# Sets VSS_NUGET_EXTERNAL_FEED_ENDPOINTS based on the "darc-int-*" feeds defined in NuGet.config. This is needed +# in build agents by CredProvider to authenticate the restore requests to internal feeds as specified in +# https://github.com/microsoft/artifacts-credprovider/blob/0f53327cd12fd893d8627d7b08a2171bf5852a41/README.md#environment-variables. This should ONLY be called from identified +# internal builds +function SetupCredProvider { + param( + [string] $AuthToken + ) + + # Install the Cred Provider NuGet plugin + Write-Host "Setting up Cred Provider NuGet plugin in the agent..." + Write-Host "Getting 'installcredprovider.ps1' from 'https://github.com/microsoft/artifacts-credprovider'..." + + $url = 'https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.ps1' + + Write-Host "Writing the contents of 'installcredprovider.ps1' locally..." + Invoke-WebRequest $url -OutFile installcredprovider.ps1 + + Write-Host "Installing plugin..." + .\installcredprovider.ps1 -Force + + Write-Host "Deleting local copy of 'installcredprovider.ps1'..." + Remove-Item .\installcredprovider.ps1 + + if (-Not("$env:USERPROFILE\.nuget\plugins\netcore")) { + Write-Host "CredProvider plugin was not installed correctly!" + ExitWithExitCode 1 + } + else { + Write-Host "CredProvider plugin was installed correctly!" + } + + # Then, we set the 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS' environment variable to restore from the stable + # feeds successfully + + $nugetConfigPath = "$RepoRoot\NuGet.config" + + if (-Not (Test-Path -Path $nugetConfigPath)) { + Write-Host "NuGet.config file not found in repo's root!" + ExitWithExitCode 1 + } + + $endpoints = New-Object System.Collections.ArrayList + $nugetConfigPackageSources = Select-Xml -Path $nugetConfigPath -XPath "//packageSources/add[contains(@key, 'darc-int-')]/@value" | foreach{$_.Node.Value} + + if (($nugetConfigPackageSources | Measure-Object).Count -gt 0 ) { + foreach ($stableRestoreResource in $nugetConfigPackageSources) { + $trimmedResource = ([string]$stableRestoreResource).Trim() + [void]$endpoints.Add(@{endpoint="$trimmedResource"; password="$AuthToken"}) + } + } + + if (($endpoints | Measure-Object).Count -gt 0) { + # Create the JSON object. It should look like '{"endpointCredentials": [{"endpoint":"http://example.index.json", "username":"optional", "password":"accesstoken"}]}' + $endpointCredentials = @{endpointCredentials=$endpoints} | ConvertTo-Json -Compress + + # Create the environment variables the AzDo way + Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data $endpointCredentials -Properties @{ + 'variable' = 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS' + 'issecret' = 'false' + } + + # We don't want sessions cached since we will be updating the endpoints quite frequently + Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data 'False' -Properties @{ + 'variable' = 'NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED' + 'issecret' = 'false' + } + } + else + { + Write-Host "No internal endpoints found in NuGet.config" + } +} + +#Workaround for https://github.com/microsoft/msbuild/issues/4430 +function InstallDotNetSdkAndRestoreArcade { + $dotnetTempDir = "$RepoRoot\dotnet" + $dotnetSdkVersion="2.1.507" # After experimentation we know this version works when restoring the SDK (compared to 3.0.*) + $dotnet = "$dotnetTempDir\dotnet.exe" + $restoreProjPath = "$PSScriptRoot\restore.proj" + + Write-Host "Installing dotnet SDK version $dotnetSdkVersion to restore Arcade SDK..." + InstallDotNetSdk "$dotnetTempDir" "$dotnetSdkVersion" + + '' | Out-File "$restoreProjPath" + + & $dotnet restore $restoreProjPath + + Write-Host "Arcade SDK restored!" + + if (Test-Path -Path $restoreProjPath) { + Remove-Item $restoreProjPath + } + + if (Test-Path -Path $dotnetTempDir) { + Remove-Item $dotnetTempDir -Recurse + } +} + +try { + Push-Location $PSScriptRoot + + if ($Operation -like "setup") { + SetupCredProvider $AuthToken + } + elseif ($Operation -like "install-restore") { + InstallDotNetSdkAndRestoreArcade + } + else { + Write-Host "Unknown operation '$Operation'!" + ExitWithExitCode 1 + } +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} +finally { + Pop-Location +} diff --git a/eng/common/internal-feed-operations.sh b/eng/common/internal-feed-operations.sh new file mode 100755 index 000000000..1ff654d2f --- /dev/null +++ b/eng/common/internal-feed-operations.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash + +set -e + +# Sets VSS_NUGET_EXTERNAL_FEED_ENDPOINTS based on the "darc-int-*" feeds defined in NuGet.config. This is needed +# in build agents by CredProvider to authenticate the restore requests to internal feeds as specified in +# https://github.com/microsoft/artifacts-credprovider/blob/0f53327cd12fd893d8627d7b08a2171bf5852a41/README.md#environment-variables. +# This should ONLY be called from identified internal builds +function SetupCredProvider { + local authToken=$1 + + # Install the Cred Provider NuGet plugin + echo "Setting up Cred Provider NuGet plugin in the agent..."... + echo "Getting 'installcredprovider.ps1' from 'https://github.com/microsoft/artifacts-credprovider'..." + + local url="https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh" + + echo "Writing the contents of 'installcredprovider.ps1' locally..." + local installcredproviderPath="installcredprovider.sh" + if command -v curl > /dev/null; then + curl $url > "$installcredproviderPath" + else + wget -q -O "$installcredproviderPath" "$url" + fi + + echo "Installing plugin..." + . "$installcredproviderPath" + + echo "Deleting local copy of 'installcredprovider.sh'..." + rm installcredprovider.sh + + if [ ! -d "$HOME/.nuget/plugins" ]; then + echo "CredProvider plugin was not installed correctly!" + ExitWithExitCode 1 + else + echo "CredProvider plugin was installed correctly!" + fi + + # Then, we set the 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS' environment variable to restore from the stable + # feeds successfully + + local nugetConfigPath="$repo_root/NuGet.config" + + if [ ! "$nugetConfigPath" ]; then + echo "NuGet.config file not found in repo's root!" + ExitWithExitCode 1 + fi + + local endpoints='[' + local nugetConfigPackageValues=`cat "$nugetConfigPath" | grep "key=\"darc-int-"` + local pattern="value=\"(.*)\"" + + for value in $nugetConfigPackageValues + do + if [[ $value =~ $pattern ]]; then + local endpoint="${BASH_REMATCH[1]}" + endpoints+="{\"endpoint\": \"$endpoint\", \"password\": \"$authToken\"}," + fi + done + + endpoints=${endpoints%?} + endpoints+=']' + + if [ ${#endpoints} -gt 2 ]; then + # Create the JSON object. It should look like '{"endpointCredentials": [{"endpoint":"http://example.index.json", "username":"optional", "password":"accesstoken"}]}' + local endpointCredentials="{\"endpointCredentials\": "$endpoints"}" + + echo "##vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$endpointCredentials" + echo "##vso[task.setvariable variable=NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED]False" + else + echo "No internal endpoints found in NuGet.config" + fi +} + +# Workaround for https://github.com/microsoft/msbuild/issues/4430 +function InstallDotNetSdkAndRestoreArcade { + local dotnetTempDir="$repo_root/dotnet" + local dotnetSdkVersion="2.1.507" # After experimentation we know this version works when restoring the SDK (compared to 3.0.*) + local restoreProjPath="$repo_root/eng/common/restore.proj" + + echo "Installing dotnet SDK version $dotnetSdkVersion to restore Arcade SDK..." + echo "" > "$restoreProjPath" + + InstallDotNetSdk "$dotnetTempDir" "$dotnetSdkVersion" + + local res=`$dotnetTempDir/dotnet restore $restoreProjPath` + echo "Arcade SDK restored!" + + # Cleanup + if [ "$restoreProjPath" ]; then + rm "$restoreProjPath" + fi + + if [ "$dotnetTempDir" ]; then + rm -r $dotnetTempDir + fi +} + +source="${BASH_SOURCE[0]}" +operation='' +authToken='' +repoName='' + +while [[ $# > 0 ]]; do + opt="$(echo "$1" | awk '{print tolower($0)}')" + case "$opt" in + --operation) + operation=$2 + shift + ;; + --authtoken) + authToken=$2 + shift + ;; + *) + echo "Invalid argument: $1" + usage + exit 1 + ;; + esac + + shift +done + +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. "$scriptroot/tools.sh" + +if [ "$operation" = "setup" ]; then + SetupCredProvider $authToken +elif [ "$operation" = "install-restore" ]; then + InstallDotNetSdkAndRestoreArcade +else + echo "Unknown operation '$operation'!" +fi diff --git a/eng/common/internal/Directory.Build.props b/eng/common/internal/Directory.Build.props new file mode 100644 index 000000000..e33179ef3 --- /dev/null +++ b/eng/common/internal/Directory.Build.props @@ -0,0 +1,4 @@ + + + + diff --git a/eng/common/internal/Tools.csproj b/eng/common/internal/Tools.csproj new file mode 100644 index 000000000..1a39a7ef3 --- /dev/null +++ b/eng/common/internal/Tools.csproj @@ -0,0 +1,27 @@ + + + + + net472 + false + + + + + + + + + + + https://devdiv.pkgs.visualstudio.com/_packaging/dotnet-core-internal-tooling/nuget/v3/index.json; + + + $(RestoreSources); + https://devdiv.pkgs.visualstudio.com/_packaging/VS/nuget/v3/index.json; + + + + + + diff --git a/eng/common/msbuild.ps1 b/eng/common/msbuild.ps1 new file mode 100644 index 000000000..b37fd3d5e --- /dev/null +++ b/eng/common/msbuild.ps1 @@ -0,0 +1,27 @@ +[CmdletBinding(PositionalBinding=$false)] +Param( + [string] $verbosity = "minimal", + [bool] $warnAsError = $true, + [bool] $nodeReuse = $true, + [switch] $ci, + [switch] $prepareMachine, + [Parameter(ValueFromRemainingArguments=$true)][String[]]$extraArgs +) + +. $PSScriptRoot\tools.ps1 + +try { + if ($ci) { + $nodeReuse = $false + } + + MSBuild @extraArgs +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} + +ExitWithExitCode 0 \ No newline at end of file diff --git a/eng/common/msbuild.sh b/eng/common/msbuild.sh new file mode 100755 index 000000000..8160cd5a5 --- /dev/null +++ b/eng/common/msbuild.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" + +# resolve $source until the file is no longer a symlink +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +verbosity='minimal' +warn_as_error=true +node_reuse=true +prepare_machine=false +extra_args='' + +while (($# > 0)); do + lowerI="$(echo $1 | awk '{print tolower($0)}')" + case $lowerI in + --verbosity) + verbosity=$2 + shift 2 + ;; + --warnaserror) + warn_as_error=$2 + shift 2 + ;; + --nodereuse) + node_reuse=$2 + shift 2 + ;; + --ci) + ci=true + shift 1 + ;; + --preparemachine) + prepare_machine=true + shift 1 + ;; + *) + extra_args="$extra_args $1" + shift 1 + ;; + esac +done + +. "$scriptroot/tools.sh" + +if [[ "$ci" == true ]]; then + node_reuse=false +fi + +MSBuild $extra_args +ExitWithExitCode 0 diff --git a/eng/common/native/CommonLibrary.psm1 b/eng/common/native/CommonLibrary.psm1 new file mode 100644 index 000000000..2a08d5246 --- /dev/null +++ b/eng/common/native/CommonLibrary.psm1 @@ -0,0 +1,387 @@ +<# +.SYNOPSIS +Helper module to install an archive to a directory + +.DESCRIPTION +Helper module to download and extract an archive to a specified directory + +.PARAMETER Uri +Uri of artifact to download + +.PARAMETER InstallDirectory +Directory to extract artifact contents to + +.PARAMETER Force +Force download / extraction if file or contents already exist. Default = False + +.PARAMETER DownloadRetries +Total number of retry attempts. Default = 5 + +.PARAMETER RetryWaitTimeInSeconds +Wait time between retry attempts in seconds. Default = 30 + +.NOTES +Returns False if download or extraction fail, True otherwise +#> +function DownloadAndExtract { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $Uri, + [Parameter(Mandatory=$True)] + [string] $InstallDirectory, + [switch] $Force = $False, + [int] $DownloadRetries = 5, + [int] $RetryWaitTimeInSeconds = 30 + ) + # Define verbose switch if undefined + $Verbose = $VerbosePreference -Eq "Continue" + + $TempToolPath = CommonLibrary\Get-TempPathFilename -Path $Uri + + # Download native tool + $DownloadStatus = CommonLibrary\Get-File -Uri $Uri ` + -Path $TempToolPath ` + -DownloadRetries $DownloadRetries ` + -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds ` + -Force:$Force ` + -Verbose:$Verbose + + if ($DownloadStatus -Eq $False) { + Write-Error "Download failed" + return $False + } + + # Extract native tool + $UnzipStatus = CommonLibrary\Expand-Zip -ZipPath $TempToolPath ` + -OutputDirectory $InstallDirectory ` + -Force:$Force ` + -Verbose:$Verbose + + if ($UnzipStatus -Eq $False) { + # Retry Download one more time with Force=true + $DownloadRetryStatus = CommonLibrary\Get-File -Uri $Uri ` + -Path $TempToolPath ` + -DownloadRetries 1 ` + -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds ` + -Force:$True ` + -Verbose:$Verbose + + if ($DownloadRetryStatus -Eq $False) { + Write-Error "Last attempt of download failed as well" + return $False + } + + # Retry unzip again one more time with Force=true + $UnzipRetryStatus = CommonLibrary\Expand-Zip -ZipPath $TempToolPath ` + -OutputDirectory $InstallDirectory ` + -Force:$True ` + -Verbose:$Verbose + if ($UnzipRetryStatus -Eq $False) + { + Write-Error "Last attempt of unzip failed as well" + # Clean up partial zips and extracts + if (Test-Path $TempToolPath) { + Remove-Item $TempToolPath -Force + } + if (Test-Path $InstallDirectory) { + Remove-Item $InstallDirectory -Force -Recurse + } + return $False + } + } + + return $True +} + +<# +.SYNOPSIS +Download a file, retry on failure + +.DESCRIPTION +Download specified file and retry if attempt fails + +.PARAMETER Uri +Uri of file to download. If Uri is a local path, the file will be copied instead of downloaded + +.PARAMETER Path +Path to download or copy uri file to + +.PARAMETER Force +Overwrite existing file if present. Default = False + +.PARAMETER DownloadRetries +Total number of retry attempts. Default = 5 + +.PARAMETER RetryWaitTimeInSeconds +Wait time between retry attempts in seconds Default = 30 + +#> +function Get-File { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $Uri, + [Parameter(Mandatory=$True)] + [string] $Path, + [int] $DownloadRetries = 5, + [int] $RetryWaitTimeInSeconds = 30, + [switch] $Force = $False + ) + $Attempt = 0 + + if ($Force) { + if (Test-Path $Path) { + Remove-Item $Path -Force + } + } + if (Test-Path $Path) { + Write-Host "File '$Path' already exists, skipping download" + return $True + } + + $DownloadDirectory = Split-Path -ErrorAction Ignore -Path "$Path" -Parent + if (-Not (Test-Path $DownloadDirectory)) { + New-Item -path $DownloadDirectory -force -itemType "Directory" | Out-Null + } + + if (Test-Path -IsValid -Path $Uri) { + Write-Verbose "'$Uri' is a file path, copying file to '$Path'" + Copy-Item -Path $Uri -Destination $Path + return $? + } + else { + Write-Verbose "Downloading $Uri" + while($Attempt -Lt $DownloadRetries) + { + try { + Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $Path + Write-Verbose "Downloaded to '$Path'" + return $True + } + catch { + $Attempt++ + if ($Attempt -Lt $DownloadRetries) { + $AttemptsLeft = $DownloadRetries - $Attempt + Write-Warning "Download failed, $AttemptsLeft attempts remaining, will retry in $RetryWaitTimeInSeconds seconds" + Start-Sleep -Seconds $RetryWaitTimeInSeconds + } + else { + Write-Error $_ + Write-Error $_.Exception + } + } + } + } + + return $False +} + +<# +.SYNOPSIS +Generate a shim for a native tool + +.DESCRIPTION +Creates a wrapper script (shim) that passes arguments forward to native tool assembly + +.PARAMETER ShimName +The name of the shim + +.PARAMETER ShimDirectory +The directory where shims are stored + +.PARAMETER ToolFilePath +Path to file that shim forwards to + +.PARAMETER Force +Replace shim if already present. Default = False + +.NOTES +Returns $True if generating shim succeeds, $False otherwise +#> +function New-ScriptShim { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $ShimName, + [Parameter(Mandatory=$True)] + [string] $ShimDirectory, + [Parameter(Mandatory=$True)] + [string] $ToolFilePath, + [Parameter(Mandatory=$True)] + [string] $BaseUri, + [switch] $Force + ) + try { + Write-Verbose "Generating '$ShimName' shim" + + if (-Not (Test-Path $ToolFilePath)){ + Write-Error "Specified tool file path '$ToolFilePath' does not exist" + return $False + } + + # WinShimmer is a small .NET Framework program that creates .exe shims to bootstrapped programs + # Many of the checks for installed programs expect a .exe extension for Windows tools, rather + # than a .bat or .cmd file. + # Source: https://github.com/dotnet/arcade/tree/master/src/WinShimmer + if (-Not (Test-Path "$ShimDirectory\WinShimmer\winshimmer.exe")) { + $InstallStatus = DownloadAndExtract -Uri "$BaseUri/windows/winshimmer/WinShimmer.zip" ` + -InstallDirectory $ShimDirectory\WinShimmer ` + -Force:$Force ` + -DownloadRetries 2 ` + -RetryWaitTimeInSeconds 5 ` + -Verbose:$Verbose + } + + if ((Test-Path (Join-Path $ShimDirectory "$ShimName.exe"))) { + Write-Host "$ShimName.exe already exists; replacing..." + Remove-Item (Join-Path $ShimDirectory "$ShimName.exe") + } + + & "$ShimDirectory\WinShimmer\winshimmer.exe" $ShimName $ToolFilePath $ShimDirectory + return $True + } + catch { + Write-Host $_ + Write-Host $_.Exception + return $False + } +} + +<# +.SYNOPSIS +Returns the machine architecture of the host machine + +.NOTES +Returns 'x64' on 64 bit machines + Returns 'x86' on 32 bit machines +#> +function Get-MachineArchitecture { + $ProcessorArchitecture = $Env:PROCESSOR_ARCHITECTURE + $ProcessorArchitectureW6432 = $Env:PROCESSOR_ARCHITEW6432 + if($ProcessorArchitecture -Eq "X86") + { + if(($ProcessorArchitectureW6432 -Eq "") -Or + ($ProcessorArchitectureW6432 -Eq "X86")) { + return "x86" + } + $ProcessorArchitecture = $ProcessorArchitectureW6432 + } + if (($ProcessorArchitecture -Eq "AMD64") -Or + ($ProcessorArchitecture -Eq "IA64") -Or + ($ProcessorArchitecture -Eq "ARM64")) { + return "x64" + } + return "x86" +} + +<# +.SYNOPSIS +Get the name of a temporary folder under the native install directory +#> +function Get-TempDirectory { + return Join-Path (Get-NativeInstallDirectory) "temp/" +} + +function Get-TempPathFilename { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $Path + ) + $TempDir = CommonLibrary\Get-TempDirectory + $TempFilename = Split-Path $Path -leaf + $TempPath = Join-Path $TempDir $TempFilename + return $TempPath +} + +<# +.SYNOPSIS +Returns the base directory to use for native tool installation + +.NOTES +Returns the value of the NETCOREENG_INSTALL_DIRECTORY if that environment variable +is set, or otherwise returns an install directory under the %USERPROFILE% +#> +function Get-NativeInstallDirectory { + $InstallDir = $Env:NETCOREENG_INSTALL_DIRECTORY + if (!$InstallDir) { + $InstallDir = Join-Path $Env:USERPROFILE ".netcoreeng/native/" + } + return $InstallDir +} + +<# +.SYNOPSIS +Unzip an archive + +.DESCRIPTION +Powershell module to unzip an archive to a specified directory + +.PARAMETER ZipPath (Required) +Path to archive to unzip + +.PARAMETER OutputDirectory (Required) +Output directory for archive contents + +.PARAMETER Force +Overwrite output directory contents if they already exist + +.NOTES +- Returns True and does not perform an extraction if output directory already exists but Overwrite is not True. +- Returns True if unzip operation is successful +- Returns False if Overwrite is True and it is unable to remove contents of OutputDirectory +- Returns False if unable to extract zip archive +#> +function Expand-Zip { + [CmdletBinding(PositionalBinding=$false)] + Param ( + [Parameter(Mandatory=$True)] + [string] $ZipPath, + [Parameter(Mandatory=$True)] + [string] $OutputDirectory, + [switch] $Force + ) + + Write-Verbose "Extracting '$ZipPath' to '$OutputDirectory'" + try { + if ((Test-Path $OutputDirectory) -And (-Not $Force)) { + Write-Host "Directory '$OutputDirectory' already exists, skipping extract" + return $True + } + if (Test-Path $OutputDirectory) { + Write-Verbose "'Force' is 'True', but '$OutputDirectory' exists, removing directory" + Remove-Item $OutputDirectory -Force -Recurse + if ($? -Eq $False) { + Write-Error "Unable to remove '$OutputDirectory'" + return $False + } + } + if (-Not (Test-Path $OutputDirectory)) { + New-Item -path $OutputDirectory -Force -itemType "Directory" | Out-Null + } + + Add-Type -assembly "system.io.compression.filesystem" + [io.compression.zipfile]::ExtractToDirectory("$ZipPath", "$OutputDirectory") + if ($? -Eq $False) { + Write-Error "Unable to extract '$ZipPath'" + return $False + } + } + catch { + Write-Host $_ + Write-Host $_.Exception + + return $False + } + return $True +} + +export-modulemember -function DownloadAndExtract +export-modulemember -function Expand-Zip +export-modulemember -function Get-File +export-modulemember -function Get-MachineArchitecture +export-modulemember -function Get-NativeInstallDirectory +export-modulemember -function Get-TempDirectory +export-modulemember -function Get-TempPathFilename +export-modulemember -function New-ScriptShim diff --git a/eng/common/native/common-library.sh b/eng/common/native/common-library.sh new file mode 100755 index 000000000..271bddfac --- /dev/null +++ b/eng/common/native/common-library.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash + +function GetNativeInstallDirectory { + local install_dir + + if [[ -z $NETCOREENG_INSTALL_DIRECTORY ]]; then + install_dir=$HOME/.netcoreeng/native/ + else + install_dir=$NETCOREENG_INSTALL_DIRECTORY + fi + + echo $install_dir + return 0 +} + +function GetTempDirectory { + + echo $(GetNativeInstallDirectory)temp/ + return 0 +} + +function ExpandZip { + local zip_path=$1 + local output_directory=$2 + local force=${3:-false} + + echo "Extracting $zip_path to $output_directory" + if [[ -d $output_directory ]] && [[ $force = false ]]; then + echo "Directory '$output_directory' already exists, skipping extract" + return 0 + fi + + if [[ -d $output_directory ]]; then + echo "'Force flag enabled, but '$output_directory' exists. Removing directory" + rm -rf $output_directory + if [[ $? != 0 ]]; then + echo Unable to remove '$output_directory'>&2 + return 1 + fi + fi + + echo "Creating directory: '$output_directory'" + mkdir -p $output_directory + + echo "Extracting archive" + tar -xf $zip_path -C $output_directory + if [[ $? != 0 ]]; then + echo "Unable to extract '$zip_path'" >&2 + return 1 + fi + + return 0 +} + +function GetCurrentOS { + local unameOut="$(uname -s)" + case $unameOut in + Linux*) echo "Linux";; + Darwin*) echo "MacOS";; + esac + return 0 +} + +function GetFile { + local uri=$1 + local path=$2 + local force=${3:-false} + local download_retries=${4:-5} + local retry_wait_time_seconds=${5:-30} + + if [[ -f $path ]]; then + if [[ $force = false ]]; then + echo "File '$path' already exists. Skipping download" + return 0 + else + rm -rf $path + fi + fi + + if [[ -f $uri ]]; then + echo "'$uri' is a file path, copying file to '$path'" + cp $uri $path + return $? + fi + + echo "Downloading $uri" + # Use curl if available, otherwise use wget + if command -v curl > /dev/null; then + curl "$uri" -sSL --retry $download_retries --retry-delay $retry_wait_time_seconds --create-dirs -o "$path" --fail + else + wget -q -O "$path" "$uri" --tries="$download_retries" + fi + + return $? +} + +function GetTempPathFileName { + local path=$1 + + local temp_dir=$(GetTempDirectory) + local temp_file_name=$(basename $path) + echo $temp_dir$temp_file_name + return 0 +} + +function DownloadAndExtract { + local uri=$1 + local installDir=$2 + local force=${3:-false} + local download_retries=${4:-5} + local retry_wait_time_seconds=${5:-30} + + local temp_tool_path=$(GetTempPathFileName $uri) + + echo "downloading to: $temp_tool_path" + + # Download file + GetFile "$uri" "$temp_tool_path" $force $download_retries $retry_wait_time_seconds + if [[ $? != 0 ]]; then + echo "Failed to download '$uri' to '$temp_tool_path'." >&2 + return 1 + fi + + # Extract File + echo "extracting from $temp_tool_path to $installDir" + ExpandZip "$temp_tool_path" "$installDir" $force $download_retries $retry_wait_time_seconds + if [[ $? != 0 ]]; then + echo "Failed to extract '$temp_tool_path' to '$installDir'." >&2 + return 1 + fi + + return 0 +} + +function NewScriptShim { + local shimpath=$1 + local tool_file_path=$2 + local force=${3:-false} + + echo "Generating '$shimpath' shim" + if [[ -f $shimpath ]]; then + if [[ $force = false ]]; then + echo "File '$shimpath' already exists." >&2 + return 1 + else + rm -rf $shimpath + fi + fi + + if [[ ! -f $tool_file_path ]]; then + echo "Specified tool file path:'$tool_file_path' does not exist" >&2 + return 1 + fi + + local shim_contents=$'#!/usr/bin/env bash\n' + shim_contents+="SHIMARGS="$'$1\n' + shim_contents+="$tool_file_path"$' $SHIMARGS\n' + + # Write shim file + echo "$shim_contents" > $shimpath + + chmod +x $shimpath + + echo "Finished generating shim '$shimpath'" + + return $? +} + diff --git a/eng/common/native/install-cmake-test.sh b/eng/common/native/install-cmake-test.sh new file mode 100755 index 000000000..53ddf4e68 --- /dev/null +++ b/eng/common/native/install-cmake-test.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. $scriptroot/common-library.sh + +base_uri= +install_path= +version= +clean=false +force=false +download_retries=5 +retry_wait_time_seconds=30 + +while (($# > 0)); do + lowerI="$(echo $1 | awk '{print tolower($0)}')" + case $lowerI in + --baseuri) + base_uri=$2 + shift 2 + ;; + --installpath) + install_path=$2 + shift 2 + ;; + --version) + version=$2 + shift 2 + ;; + --clean) + clean=true + shift 1 + ;; + --force) + force=true + shift 1 + ;; + --downloadretries) + download_retries=$2 + shift 2 + ;; + --retrywaittimeseconds) + retry_wait_time_seconds=$2 + shift 2 + ;; + --help) + echo "Common settings:" + echo " --baseuri Base file directory or Url wrom which to acquire tool archives" + echo " --installpath Base directory to install native tool to" + echo " --clean Don't install the tool, just clean up the current install of the tool" + echo " --force Force install of tools even if they previously exist" + echo " --help Print help and exit" + echo "" + echo "Advanced settings:" + echo " --downloadretries Total number of retry attempts" + echo " --retrywaittimeseconds Wait time between retry attempts in seconds" + echo "" + exit 0 + ;; + esac +done + +tool_name="cmake-test" +tool_os=$(GetCurrentOS) +tool_folder=$(echo $tool_os | awk '{print tolower($0)}') +tool_arch="x86_64" +tool_name_moniker="$tool_name-$version-$tool_os-$tool_arch" +tool_install_directory="$install_path/$tool_name/$version" +tool_file_path="$tool_install_directory/$tool_name_moniker/bin/$tool_name" +shim_path="$install_path/$tool_name.sh" +uri="${base_uri}/$tool_folder/$tool_name/$tool_name_moniker.tar.gz" + +# Clean up tool and installers +if [[ $clean = true ]]; then + echo "Cleaning $tool_install_directory" + if [[ -d $tool_install_directory ]]; then + rm -rf $tool_install_directory + fi + + echo "Cleaning $shim_path" + if [[ -f $shim_path ]]; then + rm -rf $shim_path + fi + + tool_temp_path=$(GetTempPathFileName $uri) + echo "Cleaning $tool_temp_path" + if [[ -f $tool_temp_path ]]; then + rm -rf $tool_temp_path + fi + + exit 0 +fi + +# Install tool +if [[ -f $tool_file_path ]] && [[ $force = false ]]; then + echo "$tool_name ($version) already exists, skipping install" + exit 0 +fi + +DownloadAndExtract $uri $tool_install_directory $force $download_retries $retry_wait_time_seconds + +if [[ $? != 0 ]]; then + echo "Installation failed" >&2 + exit 1 +fi + +# Generate Shim +# Always rewrite shims so that we are referencing the expected version +NewScriptShim $shim_path $tool_file_path true + +if [[ $? != 0 ]]; then + echo "Shim generation failed" >&2 + exit 1 +fi + +exit 0 \ No newline at end of file diff --git a/eng/common/native/install-cmake.sh b/eng/common/native/install-cmake.sh new file mode 100755 index 000000000..5f1a182fa --- /dev/null +++ b/eng/common/native/install-cmake.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. $scriptroot/common-library.sh + +base_uri= +install_path= +version= +clean=false +force=false +download_retries=5 +retry_wait_time_seconds=30 + +while (($# > 0)); do + lowerI="$(echo $1 | awk '{print tolower($0)}')" + case $lowerI in + --baseuri) + base_uri=$2 + shift 2 + ;; + --installpath) + install_path=$2 + shift 2 + ;; + --version) + version=$2 + shift 2 + ;; + --clean) + clean=true + shift 1 + ;; + --force) + force=true + shift 1 + ;; + --downloadretries) + download_retries=$2 + shift 2 + ;; + --retrywaittimeseconds) + retry_wait_time_seconds=$2 + shift 2 + ;; + --help) + echo "Common settings:" + echo " --baseuri Base file directory or Url wrom which to acquire tool archives" + echo " --installpath Base directory to install native tool to" + echo " --clean Don't install the tool, just clean up the current install of the tool" + echo " --force Force install of tools even if they previously exist" + echo " --help Print help and exit" + echo "" + echo "Advanced settings:" + echo " --downloadretries Total number of retry attempts" + echo " --retrywaittimeseconds Wait time between retry attempts in seconds" + echo "" + exit 0 + ;; + esac +done + +tool_name="cmake" +tool_os=$(GetCurrentOS) +tool_folder=$(echo $tool_os | awk '{print tolower($0)}') +tool_arch="x86_64" +tool_name_moniker="$tool_name-$version-$tool_os-$tool_arch" +tool_install_directory="$install_path/$tool_name/$version" +tool_file_path="$tool_install_directory/$tool_name_moniker/bin/$tool_name" +shim_path="$install_path/$tool_name.sh" +uri="${base_uri}/$tool_folder/$tool_name/$tool_name_moniker.tar.gz" + +# Clean up tool and installers +if [[ $clean = true ]]; then + echo "Cleaning $tool_install_directory" + if [[ -d $tool_install_directory ]]; then + rm -rf $tool_install_directory + fi + + echo "Cleaning $shim_path" + if [[ -f $shim_path ]]; then + rm -rf $shim_path + fi + + tool_temp_path=$(GetTempPathFileName $uri) + echo "Cleaning $tool_temp_path" + if [[ -f $tool_temp_path ]]; then + rm -rf $tool_temp_path + fi + + exit 0 +fi + +# Install tool +if [[ -f $tool_file_path ]] && [[ $force = false ]]; then + echo "$tool_name ($version) already exists, skipping install" + exit 0 +fi + +DownloadAndExtract $uri $tool_install_directory $force $download_retries $retry_wait_time_seconds + +if [[ $? != 0 ]]; then + echo "Installation failed" >&2 + exit 1 +fi + +# Generate Shim +# Always rewrite shims so that we are referencing the expected version +NewScriptShim $shim_path $tool_file_path true + +if [[ $? != 0 ]]; then + echo "Shim generation failed" >&2 + exit 1 +fi + +exit 0 \ No newline at end of file diff --git a/eng/common/native/install-tool.ps1 b/eng/common/native/install-tool.ps1 new file mode 100644 index 000000000..635ab3fd4 --- /dev/null +++ b/eng/common/native/install-tool.ps1 @@ -0,0 +1,130 @@ +<# +.SYNOPSIS +Install native tool + +.DESCRIPTION +Install cmake native tool from Azure blob storage + +.PARAMETER InstallPath +Base directory to install native tool to + +.PARAMETER BaseUri +Base file directory or Url from which to acquire tool archives + +.PARAMETER CommonLibraryDirectory +Path to folder containing common library modules + +.PARAMETER Force +Force install of tools even if they previously exist + +.PARAMETER Clean +Don't install the tool, just clean up the current install of the tool + +.PARAMETER DownloadRetries +Total number of retry attempts + +.PARAMETER RetryWaitTimeInSeconds +Wait time between retry attempts in seconds + +.NOTES +Returns 0 if install succeeds, 1 otherwise +#> +[CmdletBinding(PositionalBinding=$false)] +Param ( + [Parameter(Mandatory=$True)] + [string] $ToolName, + [Parameter(Mandatory=$True)] + [string] $InstallPath, + [Parameter(Mandatory=$True)] + [string] $BaseUri, + [Parameter(Mandatory=$True)] + [string] $Version, + [string] $CommonLibraryDirectory = $PSScriptRoot, + [switch] $Force = $False, + [switch] $Clean = $False, + [int] $DownloadRetries = 5, + [int] $RetryWaitTimeInSeconds = 30 +) + +# Import common library modules +Import-Module -Name (Join-Path $CommonLibraryDirectory "CommonLibrary.psm1") + +try { + # Define verbose switch if undefined + $Verbose = $VerbosePreference -Eq "Continue" + + $Arch = CommonLibrary\Get-MachineArchitecture + $ToolOs = "win64" + if($Arch -Eq "x32") { + $ToolOs = "win32" + } + $ToolNameMoniker = "$ToolName-$Version-$ToolOs-$Arch" + $ToolInstallDirectory = Join-Path $InstallPath "$ToolName\$Version\" + $Uri = "$BaseUri/windows/$ToolName/$ToolNameMoniker.zip" + $ShimPath = Join-Path $InstallPath "$ToolName.exe" + + if ($Clean) { + Write-Host "Cleaning $ToolInstallDirectory" + if (Test-Path $ToolInstallDirectory) { + Remove-Item $ToolInstallDirectory -Force -Recurse + } + Write-Host "Cleaning $ShimPath" + if (Test-Path $ShimPath) { + Remove-Item $ShimPath -Force + } + $ToolTempPath = CommonLibrary\Get-TempPathFilename -Path $Uri + Write-Host "Cleaning $ToolTempPath" + if (Test-Path $ToolTempPath) { + Remove-Item $ToolTempPath -Force + } + exit 0 + } + + # Install tool + if ((Test-Path $ToolInstallDirectory) -And (-Not $Force)) { + Write-Verbose "$ToolName ($Version) already exists, skipping install" + } + else { + $InstallStatus = CommonLibrary\DownloadAndExtract -Uri $Uri ` + -InstallDirectory $ToolInstallDirectory ` + -Force:$Force ` + -DownloadRetries $DownloadRetries ` + -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds ` + -Verbose:$Verbose + + if ($InstallStatus -Eq $False) { + Write-Error "Installation failed" + exit 1 + } + } + + $ToolFilePath = Get-ChildItem $ToolInstallDirectory -Recurse -Filter "$ToolName.exe" | % { $_.FullName } + if (@($ToolFilePath).Length -Gt 1) { + Write-Error "There are multiple copies of $ToolName in $($ToolInstallDirectory): `n$(@($ToolFilePath | out-string))" + exit 1 + } elseif (@($ToolFilePath).Length -Lt 1) { + Write-Error "$ToolName was not found in $ToolFilePath." + exit 1 + } + + # Generate shim + # Always rewrite shims so that we are referencing the expected version + $GenerateShimStatus = CommonLibrary\New-ScriptShim -ShimName $ToolName ` + -ShimDirectory $InstallPath ` + -ToolFilePath "$ToolFilePath" ` + -BaseUri $BaseUri ` + -Force:$Force ` + -Verbose:$Verbose + + if ($GenerateShimStatus -Eq $False) { + Write-Error "Generate shim failed" + return 1 + } + + exit 0 +} +catch { + Write-Host $_ + Write-Host $_.Exception + exit 1 +} diff --git a/eng/common/performance/perfhelixpublish.proj b/eng/common/performance/perfhelixpublish.proj new file mode 100644 index 000000000..05e5f0989 --- /dev/null +++ b/eng/common/performance/perfhelixpublish.proj @@ -0,0 +1,77 @@ + + + + %HELIX_CORRELATION_PAYLOAD%\performance\scripts\benchmarks_ci.py --csproj %HELIX_CORRELATION_PAYLOAD%\performance\$(TargetCsproj) + --dotnet-versions %DOTNET_VERSION% --cli-source-info args --cli-branch %PERFLAB_BRANCH% --cli-commit-sha %PERFLAB_HASH% --cli-repository https://github.com/%PERFLAB_REPO% --cli-source-timestamp %PERFLAB_BUILDTIMESTAMP% + py -3 + %HELIX_CORRELATION_PAYLOAD%\Core_Root\CoreRun.exe + $(HelixPreCommands);call %HELIX_CORRELATION_PAYLOAD%\performance\tools\machine-setup.cmd + %HELIX_CORRELATION_PAYLOAD%\artifacts\BenchmarkDotNet.Artifacts + + + + $HELIX_CORRELATION_PAYLOAD + $(BaseDirectory)/performance + + + + $HELIX_WORKITEM_PAYLOAD + $(BaseDirectory) + + + + $(PerformanceDirectory)/scripts/benchmarks_ci.py --csproj $(PerformanceDirectory)/$(TargetCsproj) + --dotnet-versions $DOTNET_VERSION --cli-source-info args --cli-branch $PERFLAB_BRANCH --cli-commit-sha $PERFLAB_HASH --cli-repository https://github.com/$PERFLAB_REPO --cli-source-timestamp $PERFLAB_BUILDTIMESTAMP + python3 + $(BaseDirectory)/Core_Root/corerun + $(HelixPreCommands);chmod +x $(PerformanceDirectory)/tools/machine-setup.sh;. $(PerformanceDirectory)/tools/machine-setup.sh + $(BaseDirectory)/artifacts/BenchmarkDotNet.Artifacts + + + + --corerun $(CoreRun) + + + + $(Python) $(WorkItemCommand) --incremental no --architecture $(Architecture) -f $(_Framework) $(PerfLabArguments) + + + + $(WorkItemCommand) $(CliArguments) + + + + + %(Identity) + + + + + 5 + + + + + + + + + + + + + $(WorkItemDirectory) + $(WorkItemCommand) --bdn-arguments="--anyCategories $(BDNCategories) $(ExtraBenchmarkDotNetArguments) $(CoreRunArgument) --artifacts $(ArtifactsDirectory) --partition-count $(PartitionCount) --partition-index %(HelixWorkItem.Index)" + 4:00 + + + + + $(WorkItemDirectory) + $(WorkItemCommand) --bdn-arguments="--anyCategories $(BDNCategories) $(ExtraBenchmarkDotNetArguments) $(CoreRunArgument) --artifacts $(ArtifactsDirectory)" + 4:00 + + + \ No newline at end of file diff --git a/eng/common/performance/performance-setup.ps1 b/eng/common/performance/performance-setup.ps1 new file mode 100644 index 000000000..7e5441f79 --- /dev/null +++ b/eng/common/performance/performance-setup.ps1 @@ -0,0 +1,91 @@ +Param( + [string] $SourceDirectory=$env:BUILD_SOURCESDIRECTORY, + [string] $CoreRootDirectory, + [string] $Architecture="x64", + [string] $Framework="netcoreapp3.0", + [string] $CompilationMode="Tiered", + [string] $Repository=$env:BUILD_REPOSITORY_NAME, + [string] $Branch=$env:BUILD_SOURCEBRANCH, + [string] $CommitSha=$env:BUILD_SOURCEVERSION, + [string] $BuildNumber=$env:BUILD_BUILDNUMBER, + [string] $RunCategories="coreclr corefx", + [string] $Csproj="src\benchmarks\micro\MicroBenchmarks.csproj", + [string] $Kind="micro", + [switch] $Internal, + [string] $Configurations="CompilationMode=$CompilationMode" +) + +$RunFromPerformanceRepo = ($Repository -eq "dotnet/performance") +$UseCoreRun = ($CoreRootDirectory -ne [string]::Empty) + +$PayloadDirectory = (Join-Path $SourceDirectory "Payload") +$PerformanceDirectory = (Join-Path $PayloadDirectory "performance") +$WorkItemDirectory = (Join-Path $SourceDirectory "workitem") +$ExtraBenchmarkDotNetArguments = "--iterationCount 1 --warmupCount 0 --invocationCount 1 --unrollFactor 1 --strategy ColdStart --stopOnFirstError true" +$Creator = $env:BUILD_DEFINITIONNAME +$PerfLabArguments = "" +$HelixSourcePrefix = "pr" + +$Queue = "Windows.10.Amd64.ClientRS4.DevEx.15.8.Open" + +if ($Framework.StartsWith("netcoreapp")) { + $Queue = "Windows.10.Amd64.ClientRS4.Open" +} + +if ($Internal) { + $Queue = "Windows.10.Amd64.ClientRS5.Perf" + $PerfLabArguments = "--upload-to-perflab-container" + $ExtraBenchmarkDotNetArguments = "" + $Creator = "" + $HelixSourcePrefix = "official" +} + +$CommonSetupArguments="--frameworks $Framework --queue $Queue --build-number $BuildNumber --build-configs $Configurations" +$SetupArguments = "--repository https://github.com/$Repository --branch $Branch --get-perf-hash --commit-sha $CommitSha $CommonSetupArguments" + +if ($RunFromPerformanceRepo) { + $SetupArguments = "--perf-hash $CommitSha $CommonSetupArguments" + + robocopy $SourceDirectory $PerformanceDirectory /E /XD $PayloadDirectory $SourceDirectory\artifacts $SourceDirectory\.git +} +else { + git clone --branch master --depth 1 --quiet https://github.com/dotnet/performance $PerformanceDirectory +} + +if ($UseCoreRun) { + $NewCoreRoot = (Join-Path $PayloadDirectory "Core_Root") + Move-Item -Path $CoreRootDirectory -Destination $NewCoreRoot +} + +$DocsDir = (Join-Path $PerformanceDirectory "docs") +robocopy $DocsDir $WorkItemDirectory + +# Set variables that we will need to have in future steps +$ci = $true + +. "$PSScriptRoot\..\pipeline-logging-functions.ps1" + +# Directories +Write-PipelineSetVariable -Name 'PayloadDirectory' -Value "$PayloadDirectory" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'PerformanceDirectory' -Value "$PerformanceDirectory" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'WorkItemDirectory' -Value "$WorkItemDirectory" -IsMultiJobVariable $false + +# Script Arguments +Write-PipelineSetVariable -Name 'Python' -Value "py -3" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'ExtraBenchmarkDotNetArguments' -Value "$ExtraBenchmarkDotNetArguments" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'SetupArguments' -Value "$SetupArguments" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'PerfLabArguments' -Value "$PerfLabArguments" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'BDNCategories' -Value "$RunCategories" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'TargetCsproj' -Value "$Csproj" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'Kind' -Value "$Kind" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'Architecture' -Value "$Architecture" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'UseCoreRun' -Value "$UseCoreRun" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'RunFromPerfRepo' -Value "$RunFromPerformanceRepo" -IsMultiJobVariable $false + +# Helix Arguments +Write-PipelineSetVariable -Name 'Creator' -Value "$Creator" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'Queue' -Value "$Queue" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name 'HelixSourcePrefix' -Value "$HelixSourcePrefix" -IsMultiJobVariable $false +Write-PipelineSetVariable -Name '_BuildConfig' -Value "$Architecture.$Kind.$Framework" -IsMultiJobVariable $false + +exit 0 \ No newline at end of file diff --git a/eng/common/performance/performance-setup.sh b/eng/common/performance/performance-setup.sh new file mode 100755 index 000000000..126da5f76 --- /dev/null +++ b/eng/common/performance/performance-setup.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash + +source_directory=$BUILD_SOURCESDIRECTORY +core_root_directory= +architecture=x64 +framework=netcoreapp3.0 +compilation_mode=tiered +repository=$BUILD_REPOSITORY_NAME +branch=$BUILD_SOURCEBRANCH +commit_sha=$BUILD_SOURCEVERSION +build_number=$BUILD_BUILDNUMBER +internal=false +kind="micro" +run_categories="coreclr corefx" +csproj="src\benchmarks\micro\MicroBenchmarks.csproj" +configurations= +run_from_perf_repo=false +use_core_run=true + +while (($# > 0)); do + lowerI="$(echo $1 | awk '{print tolower($0)}')" + case $lowerI in + --sourcedirectory) + source_directory=$2 + shift 2 + ;; + --corerootdirectory) + core_root_directory=$2 + shift 2 + ;; + --architecture) + architecture=$2 + shift 2 + ;; + --framework) + framework=$2 + shift 2 + ;; + --compilationmode) + compilation_mode=$2 + shift 2 + ;; + --repository) + repository=$2 + shift 2 + ;; + --branch) + branch=$2 + shift 2 + ;; + --commitsha) + commit_sha=$2 + shift 2 + ;; + --buildnumber) + build_number=$2 + shift 2 + ;; + --kind) + kind=$2 + shift 2 + ;; + --runcategories) + run_categories=$2 + shift 2 + ;; + --csproj) + csproj=$2 + shift 2 + ;; + --internal) + internal=true + shift 1 + ;; + --configurations) + configurations=$2 + shift 2 + ;; + --help) + echo "Common settings:" + echo " --corerootdirectory Directory where Core_Root exists, if running perf testing with --corerun" + echo " --architecture Architecture of the testing being run" + echo " --configurations List of key=value pairs that will be passed to perf testing infrastructure." + echo " ex: --configurations \"CompilationMode=Tiered OptimzationLevel=PGO\"" + echo " --help Print help and exit" + echo "" + echo "Advanced settings:" + echo " --framework The framework to run, if not running in master" + echo " --compliationmode The compilation mode if not passing --configurations" + echo " --sourcedirectory The directory of the sources. Defaults to env:BUILD_SOURCESDIRECTORY" + echo " --repository The name of the repository in the / format. Defaults to env:BUILD_REPOSITORY_NAME" + echo " --branch The name of the branch. Defaults to env:BUILD_SOURCEBRANCH" + echo " --commitsha The commit sha1 to run against. Defaults to env:BUILD_SOURCEVERSION" + echo " --buildnumber The build number currently running. Defaults to env:BUILD_BUILDNUMBER" + echo " --csproj The relative path to the benchmark csproj whose tests should be run. Defaults to src\benchmarks\micro\MicroBenchmarks.csproj" + echo " --kind Related to csproj. The kind of benchmarks that should be run. Defaults to micro" + echo " --runcategories Related to csproj. Categories of benchmarks to run. Defaults to \"coreclr corefx\"" + echo " --internal If the benchmarks are running as an official job." + echo "" + exit 0 + ;; + esac +done + +if [[ "$repository" == "dotnet/performance" ]]; then + run_from_perf_repo=true +fi + +if [ -z "$configurations" ]; then + configurations="CompliationMode=$compilation_mode" +fi + +if [ -z "$core_root_directory" ]; then + use_core_run=false +fi + +payload_directory=$source_directory/Payload +performance_directory=$payload_directory/performance +workitem_directory=$source_directory/workitem +extra_benchmark_dotnet_arguments="--iterationCount 1 --warmupCount 0 --invocationCount 1 --unrollFactor 1 --strategy ColdStart --stopOnFirstError true" +perflab_arguments= +queue=Ubuntu.1804.Amd64.Open +creator=$BUILD_DEFINITIONNAME +helix_source_prefix="pr" + +if [[ "$internal" == true ]]; then + perflab_arguments="--upload-to-perflab-container" + helix_source_prefix="official" + creator= + extra_benchmark_dotnet_arguments= + + if [[ "$architecture" = "arm64" ]]; then + queue=Ubuntu.1804.Arm64.Perf + else + queue=Ubuntu.1804.Amd64.Perf + fi +fi + +common_setup_arguments="--frameworks $framework --queue $queue --build-number $build_number --build-configs $configurations" +setup_arguments="--repository https://github.com/$repository --branch $branch --get-perf-hash --commit-sha $commit_sha $common_setup_arguments" + +if [[ "$run_from_perf_repo" = true ]]; then + payload_directory= + workitem_directory=$source_directory + performance_directory=$workitem_directory + setup_arguments="--perf-hash $commit_sha $common_setup_arguments" +else + git clone --branch master --depth 1 --quiet https://github.com/dotnet/performance $performance_directory + + docs_directory=$performance_directory/docs + mv $docs_directory $workitem_directory +fi + +if [[ "$use_core_run" = true ]]; then + new_core_root=$payload_directory/Core_Root + mv $core_root_directory $new_core_root +fi + +# Make sure all of our variables are available for future steps +echo "##vso[task.setvariable variable=UseCoreRun]$use_core_run" +echo "##vso[task.setvariable variable=Architecture]$architecture" +echo "##vso[task.setvariable variable=PayloadDirectory]$payload_directory" +echo "##vso[task.setvariable variable=PerformanceDirectory]$performance_directory" +echo "##vso[task.setvariable variable=WorkItemDirectory]$workitem_directory" +echo "##vso[task.setvariable variable=Queue]$queue" +echo "##vso[task.setvariable variable=SetupArguments]$setup_arguments" +echo "##vso[task.setvariable variable=Python]python3" +echo "##vso[task.setvariable variable=PerfLabArguments]$perflab_arguments" +echo "##vso[task.setvariable variable=ExtraBenchmarkDotNetArguments]$extra_benchmark_dotnet_arguments" +echo "##vso[task.setvariable variable=BDNCategories]$run_categories" +echo "##vso[task.setvariable variable=TargetCsproj]$csproj" +echo "##vso[task.setvariable variable=RunFromPerfRepo]$run_from_perf_repo" +echo "##vso[task.setvariable variable=Creator]$creator" +echo "##vso[task.setvariable variable=HelixSourcePrefix]$helix_source_prefix" +echo "##vso[task.setvariable variable=Kind]$kind" +echo "##vso[task.setvariable variable=_BuildConfig]$architecture.$kind.$framework" \ No newline at end of file diff --git a/eng/common/pipeline-logging-functions.ps1 b/eng/common/pipeline-logging-functions.ps1 new file mode 100644 index 000000000..af5f48aac --- /dev/null +++ b/eng/common/pipeline-logging-functions.ps1 @@ -0,0 +1,234 @@ +# Source for this file was taken from https://github.com/microsoft/azure-pipelines-task-lib/blob/11c9439d4af17e6475d9fe058e6b2e03914d17e6/powershell/VstsTaskSdk/LoggingCommandFunctions.ps1 and modified. + +# NOTE: You should not be calling these method directly as they are likely to change. Instead you should be calling the Write-Pipeline* functions defined in tools.ps1 + +$script:loggingCommandPrefix = '##vso[' +$script:loggingCommandEscapeMappings = @( # TODO: WHAT ABOUT "="? WHAT ABOUT "%"? + New-Object psobject -Property @{ Token = ';' ; Replacement = '%3B' } + New-Object psobject -Property @{ Token = "`r" ; Replacement = '%0D' } + New-Object psobject -Property @{ Token = "`n" ; Replacement = '%0A' } + New-Object psobject -Property @{ Token = "]" ; Replacement = '%5D' } +) +# TODO: BUG: Escape % ??? +# TODO: Add test to verify don't need to escape "=". + +function Write-PipelineTelemetryError { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Category, + [Parameter(Mandatory = $true)] + [string]$Message, + [Parameter(Mandatory = $false)] + [string]$Type = 'error', + [string]$ErrCode, + [string]$SourcePath, + [string]$LineNumber, + [string]$ColumnNumber, + [switch]$AsOutput) + + $PSBoundParameters.Remove("Category") | Out-Null + + $Message = "(NETCORE_ENGINEERING_TELEMETRY=$Category) $Message" + $PSBoundParameters.Remove("Message") | Out-Null + $PSBoundParameters.Add("Message", $Message) + + Write-PipelineTaskError @PSBoundParameters +} + +function Write-PipelineTaskError { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message, + [Parameter(Mandatory = $false)] + [string]$Type = 'error', + [string]$ErrCode, + [string]$SourcePath, + [string]$LineNumber, + [string]$ColumnNumber, + [switch]$AsOutput) + + if(!$ci) { + if($Type -eq 'error') { + Write-Host $Message -ForegroundColor Red + return + } + elseif ($Type -eq 'warning') { + Write-Host $Message -ForegroundColor Yellow + return + } + } + + if(($Type -ne 'error') -and ($Type -ne 'warning')) { + Write-Host $Message + return + } + if(-not $PSBoundParameters.ContainsKey('Type')) { + $PSBoundParameters.Add('Type', 'error') + } + Write-LogIssue @PSBoundParameters + } + + function Write-PipelineSetVariable { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Name, + [string]$Value, + [switch]$Secret, + [switch]$AsOutput, + [bool]$IsMultiJobVariable=$true) + + if($ci) { + Write-LoggingCommand -Area 'task' -Event 'setvariable' -Data $Value -Properties @{ + 'variable' = $Name + 'isSecret' = $Secret + 'isOutput' = $IsMultiJobVariable + } -AsOutput:$AsOutput + } + } + + function Write-PipelinePrependPath { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$Path, + [switch]$AsOutput) + if($ci) { + Write-LoggingCommand -Area 'task' -Event 'prependpath' -Data $Path -AsOutput:$AsOutput + } + } + +<######################################## +# Private functions. +########################################> +function Format-LoggingCommandData { + [CmdletBinding()] + param([string]$Value, [switch]$Reverse) + + if (!$Value) { + return '' + } + + if (!$Reverse) { + foreach ($mapping in $script:loggingCommandEscapeMappings) { + $Value = $Value.Replace($mapping.Token, $mapping.Replacement) + } + } else { + for ($i = $script:loggingCommandEscapeMappings.Length - 1 ; $i -ge 0 ; $i--) { + $mapping = $script:loggingCommandEscapeMappings[$i] + $Value = $Value.Replace($mapping.Replacement, $mapping.Token) + } + } + + return $Value +} + +function Format-LoggingCommand { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Area, + [Parameter(Mandatory = $true)] + [string]$Event, + [string]$Data, + [hashtable]$Properties) + + # Append the preamble. + [System.Text.StringBuilder]$sb = New-Object -TypeName System.Text.StringBuilder + $null = $sb.Append($script:loggingCommandPrefix).Append($Area).Append('.').Append($Event) + + # Append the properties. + if ($Properties) { + $first = $true + foreach ($key in $Properties.Keys) { + [string]$value = Format-LoggingCommandData $Properties[$key] + if ($value) { + if ($first) { + $null = $sb.Append(' ') + $first = $false + } else { + $null = $sb.Append(';') + } + + $null = $sb.Append("$key=$value") + } + } + } + + # Append the tail and output the value. + $Data = Format-LoggingCommandData $Data + $sb.Append(']').Append($Data).ToString() +} + +function Write-LoggingCommand { + [CmdletBinding(DefaultParameterSetName = 'Parameters')] + param( + [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] + [string]$Area, + [Parameter(Mandatory = $true, ParameterSetName = 'Parameters')] + [string]$Event, + [Parameter(ParameterSetName = 'Parameters')] + [string]$Data, + [Parameter(ParameterSetName = 'Parameters')] + [hashtable]$Properties, + [Parameter(Mandatory = $true, ParameterSetName = 'Object')] + $Command, + [switch]$AsOutput) + + if ($PSCmdlet.ParameterSetName -eq 'Object') { + Write-LoggingCommand -Area $Command.Area -Event $Command.Event -Data $Command.Data -Properties $Command.Properties -AsOutput:$AsOutput + return + } + + $command = Format-LoggingCommand -Area $Area -Event $Event -Data $Data -Properties $Properties + if ($AsOutput) { + $command + } else { + Write-Host $command + } +} + +function Write-LogIssue { + [CmdletBinding()] + param( + [ValidateSet('warning', 'error')] + [Parameter(Mandatory = $true)] + [string]$Type, + [string]$Message, + [string]$ErrCode, + [string]$SourcePath, + [string]$LineNumber, + [string]$ColumnNumber, + [switch]$AsOutput) + + $command = Format-LoggingCommand -Area 'task' -Event 'logissue' -Data $Message -Properties @{ + 'type' = $Type + 'code' = $ErrCode + 'sourcepath' = $SourcePath + 'linenumber' = $LineNumber + 'columnnumber' = $ColumnNumber + } + if ($AsOutput) { + return $command + } + + if ($Type -eq 'error') { + $foregroundColor = $host.PrivateData.ErrorForegroundColor + $backgroundColor = $host.PrivateData.ErrorBackgroundColor + if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { + $foregroundColor = [System.ConsoleColor]::Red + $backgroundColor = [System.ConsoleColor]::Black + } + } else { + $foregroundColor = $host.PrivateData.WarningForegroundColor + $backgroundColor = $host.PrivateData.WarningBackgroundColor + if ($foregroundColor -isnot [System.ConsoleColor] -or $backgroundColor -isnot [System.ConsoleColor]) { + $foregroundColor = [System.ConsoleColor]::Yellow + $backgroundColor = [System.ConsoleColor]::Black + } + } + + Write-Host $command -ForegroundColor $foregroundColor -BackgroundColor $backgroundColor +} \ No newline at end of file diff --git a/eng/common/pipeline-logging-functions.sh b/eng/common/pipeline-logging-functions.sh new file mode 100755 index 000000000..1c560a506 --- /dev/null +++ b/eng/common/pipeline-logging-functions.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash + +function Write-PipelineTelemetryError { + local telemetry_category='' + local function_args=() + local message='' + while [[ $# -gt 0 ]]; do + opt="$(echo "${1/#--/-}" | awk '{print tolower($0)}')" + case "$opt" in + -category|-c) + telemetry_category=$2 + shift + ;; + -*) + function_args+=("$1 $2") + shift + ;; + *) + message=$* + ;; + esac + shift + done + + if [[ "$ci" != true ]]; then + echo "$message" >&2 + return + fi + + message="(NETCORE_ENGINEERING_TELEMETRY=$telemetry_category) $message" + function_args+=("$message") + + Write-PipelineTaskError $function_args +} + +function Write-PipelineTaskError { + if [[ "$ci" != true ]]; then + echo "$@" >&2 + return + fi + + local message_type="error" + local sourcepath='' + local linenumber='' + local columnnumber='' + local error_code='' + + while [[ $# -gt 0 ]]; do + opt="$(echo "${1/#--/-}" | awk '{print tolower($0)}')" + case "$opt" in + -type|-t) + message_type=$2 + shift + ;; + -sourcepath|-s) + sourcepath=$2 + shift + ;; + -linenumber|-ln) + linenumber=$2 + shift + ;; + -columnnumber|-cn) + columnnumber=$2 + shift + ;; + -errcode|-e) + error_code=$2 + shift + ;; + *) + break + ;; + esac + + shift + done + + local message="##vso[task.logissue" + + message="$message type=$message_type" + + if [ -n "$sourcepath" ]; then + message="$message;sourcepath=$sourcepath" + fi + + if [ -n "$linenumber" ]; then + message="$message;linenumber=$linenumber" + fi + + if [ -n "$columnnumber" ]; then + message="$message;columnnumber=$columnnumber" + fi + + if [ -n "$error_code" ]; then + message="$message;code=$error_code" + fi + + message="$message]$*" + echo "$message" +} + +function Write-PipelineSetVariable { + if [[ "$ci" != true ]]; then + return + fi + + local name='' + local value='' + local secret=false + local as_output=false + local is_multi_job_variable=true + + while [[ $# -gt 0 ]]; do + opt="$(echo "${1/#--/-}" | awk '{print tolower($0)}')" + case "$opt" in + -name|-n) + name=$2 + shift + ;; + -value|-v) + value=$2 + shift + ;; + -secret|-s) + secret=true + ;; + -as_output|-a) + as_output=true + ;; + -is_multi_job_variable|-i) + is_multi_job_variable=$2 + shift + ;; + esac + shift + done + + value=${value/;/%3B} + value=${value/\\r/%0D} + value=${value/\\n/%0A} + value=${value/]/%5D} + + local message="##vso[task.setvariable variable=$name;isSecret=$secret;isOutput=$is_multi_job_variable]$value" + + if [[ "$as_output" == true ]]; then + $message + else + echo "$message" + fi +} + +function Write-PipelinePrependPath { + local prepend_path='' + + while [[ $# -gt 0 ]]; do + opt="$(echo "${1/#--/-}" | awk '{print tolower($0)}')" + case "$opt" in + -path|-p) + prepend_path=$2 + shift + ;; + esac + shift + done + + export PATH="$prepend_path:$PATH" + + if [[ "$ci" == true ]]; then + echo "##vso[task.prependpath]$prepend_path" + fi +} \ No newline at end of file diff --git a/eng/common/post-build/darc-gather-drop.ps1 b/eng/common/post-build/darc-gather-drop.ps1 new file mode 100644 index 000000000..93a0bd832 --- /dev/null +++ b/eng/common/post-build/darc-gather-drop.ps1 @@ -0,0 +1,35 @@ +param( + [Parameter(Mandatory=$true)][int] $BarBuildId, # ID of the build which assets should be downloaded + [Parameter(Mandatory=$true)][string] $DropLocation, # Where the assets should be downloaded to + [Parameter(Mandatory=$true)][string] $MaestroApiAccessToken, # Token used to access Maestro API + [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = "https://maestro-prod.westus2.cloudapp.azure.com", # Maestro API URL + [Parameter(Mandatory=$false)][string] $MaestroApiVersion = "2019-01-16" # Version of Maestro API to use +) + +. $PSScriptRoot\post-build-utils.ps1 + +try { + Write-Host "Installing DARC ..." + + . $PSScriptRoot\..\darc-init.ps1 + $exitCode = $LASTEXITCODE + + if ($exitCode -ne 0) { + Write-PipelineTaskError "Something failed while running 'darc-init.ps1'. Check for errors above. Exiting now..." + ExitWithExitCode $exitCode + } + + darc gather-drop --non-shipping ` + --continue-on-error ` + --id $BarBuildId ` + --output-dir $DropLocation ` + --bar-uri $MaestroApiEndpoint ` + --password $MaestroApiAccessToken ` + --latest-location +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/dotnetsymbol-init.ps1 b/eng/common/post-build/dotnetsymbol-init.ps1 new file mode 100644 index 000000000..e7659b98c --- /dev/null +++ b/eng/common/post-build/dotnetsymbol-init.ps1 @@ -0,0 +1,29 @@ +param ( + $dotnetsymbolVersion = $null +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 + +. $PSScriptRoot\..\tools.ps1 + +$verbosity = "minimal" + +function Installdotnetsymbol ($dotnetsymbolVersion) { + $dotnetsymbolPackageName = "dotnet-symbol" + + $dotnetRoot = InitializeDotNetCli -install:$true + $dotnet = "$dotnetRoot\dotnet.exe" + $toolList = & "$dotnet" tool list --global + + if (($toolList -like "*$dotnetsymbolPackageName*") -and ($toolList -like "*$dotnetsymbolVersion*")) { + Write-Host "dotnet-symbol version $dotnetsymbolVersion is already installed." + } + else { + Write-Host "Installing dotnet-symbol version $dotnetsymbolVersion..." + Write-Host "You may need to restart your command window if this is the first dotnet tool you have installed." + & "$dotnet" tool install $dotnetsymbolPackageName --version $dotnetsymbolVersion --verbosity $verbosity --global + } +} + +Installdotnetsymbol $dotnetsymbolVersion diff --git a/eng/common/post-build/nuget-validation.ps1 b/eng/common/post-build/nuget-validation.ps1 new file mode 100644 index 000000000..78ed0d540 --- /dev/null +++ b/eng/common/post-build/nuget-validation.ps1 @@ -0,0 +1,25 @@ +# This script validates NuGet package metadata information using this +# tool: https://github.com/NuGet/NuGetGallery/tree/jver-verify/src/VerifyMicrosoftPackage + +param( + [Parameter(Mandatory=$true)][string] $PackagesPath, # Path to where the packages to be validated are + [Parameter(Mandatory=$true)][string] $ToolDestinationPath # Where the validation tool should be downloaded to +) + +. $PSScriptRoot\post-build-utils.ps1 + +try { + $url = "https://raw.githubusercontent.com/NuGet/NuGetGallery/jver-verify/src/VerifyMicrosoftPackage/verify.ps1" + + New-Item -ItemType "directory" -Path ${ToolDestinationPath} -Force + + Invoke-WebRequest $url -OutFile ${ToolDestinationPath}\verify.ps1 + + & ${ToolDestinationPath}\verify.ps1 ${PackagesPath}\*.nupkg +} +catch { + Write-PipelineTaskError "NuGet package validation failed. Please check error logs." + Write-Host $_ + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/post-build-utils.ps1 b/eng/common/post-build/post-build-utils.ps1 new file mode 100644 index 000000000..551ae113f --- /dev/null +++ b/eng/common/post-build/post-build-utils.ps1 @@ -0,0 +1,90 @@ +# Most of the functions in this file require the variables `MaestroApiEndPoint`, +# `MaestroApiVersion` and `MaestroApiAccessToken` to be globally available. + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 + +# `tools.ps1` checks $ci to perform some actions. Since the post-build +# scripts don't necessarily execute in the same agent that run the +# build.ps1/sh script this variable isn't automatically set. +$ci = $true +. $PSScriptRoot\..\tools.ps1 + +function Create-MaestroApiRequestHeaders([string]$ContentType = "application/json") { + Validate-MaestroVars + + $headers = New-Object 'System.Collections.Generic.Dictionary[[String],[String]]' + $headers.Add('Accept', $ContentType) + $headers.Add('Authorization',"Bearer $MaestroApiAccessToken") + return $headers +} + +function Get-MaestroChannel([int]$ChannelId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders + $apiEndpoint = "$MaestroApiEndPoint/api/channels/${ChannelId}?api-version=$MaestroApiVersion" + + $result = try { Invoke-WebRequest -Method Get -Uri $apiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + return $result +} + +function Get-MaestroBuild([int]$BuildId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/builds/${BuildId}?api-version=$MaestroApiVersion" + + $result = try { return Invoke-WebRequest -Method Get -Uri $apiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + return $result +} + +function Get-MaestroSubscriptions([string]$SourceRepository, [int]$ChannelId) { + Validate-MaestroVars + + $SourceRepository = [System.Web.HttpUtility]::UrlEncode($SourceRepository) + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/subscriptions?sourceRepository=$SourceRepository&channelId=$ChannelId&api-version=$MaestroApiVersion" + + $result = try { Invoke-WebRequest -Method Get -Uri $apiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + return $result +} + +function Trigger-Subscription([string]$SubscriptionId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/subscriptions/$SubscriptionId/trigger?api-version=$MaestroApiVersion" + Invoke-WebRequest -Uri $apiEndpoint -Headers $apiHeaders -Method Post | Out-Null +} + +function Assign-BuildToChannel([int]$BuildId, [int]$ChannelId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/channels/${ChannelId}/builds/${BuildId}?api-version=$MaestroApiVersion" + Invoke-WebRequest -Method Post -Uri $apiEndpoint -Headers $apiHeaders | Out-Null +} + +function Validate-MaestroVars { + try { + Get-Variable MaestroApiEndPoint -Scope Global | Out-Null + Get-Variable MaestroApiVersion -Scope Global | Out-Null + Get-Variable MaestroApiAccessToken -Scope Global | Out-Null + + if (!($MaestroApiEndPoint -Match "^http[s]?://maestro-(int|prod).westus2.cloudapp.azure.com$")) { + Write-PipelineTaskError "MaestroApiEndPoint is not a valid Maestro URL. '$MaestroApiEndPoint'" + ExitWithExitCode 1 + } + + if (!($MaestroApiVersion -Match "^[0-9]{4}-[0-9]{2}-[0-9]{2}$")) { + Write-PipelineTaskError "MaestroApiVersion does not match a version string in the format yyyy-MM-DD. '$MaestroApiVersion'" + ExitWithExitCode 1 + } + } + catch { + Write-PipelineTaskError "Error: Variables `MaestroApiEndPoint`, `MaestroApiVersion` and `MaestroApiAccessToken` are required while using this script." + Write-Host $_ + ExitWithExitCode 1 + } +} diff --git a/eng/common/post-build/promote-build.ps1 b/eng/common/post-build/promote-build.ps1 new file mode 100644 index 000000000..e5ae85f25 --- /dev/null +++ b/eng/common/post-build/promote-build.ps1 @@ -0,0 +1,48 @@ +param( + [Parameter(Mandatory=$true)][int] $BuildId, + [Parameter(Mandatory=$true)][int] $ChannelId, + [Parameter(Mandatory=$true)][string] $MaestroApiAccessToken, + [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = "https://maestro-prod.westus2.cloudapp.azure.com", + [Parameter(Mandatory=$false)][string] $MaestroApiVersion = "2019-01-16" +) + +. $PSScriptRoot\post-build-utils.ps1 + +try { + # Check that the channel we are going to promote the build to exist + $channelInfo = Get-MaestroChannel -ChannelId $ChannelId + + if (!$channelInfo) { + Write-Host "Channel with BAR ID $ChannelId was not found in BAR!" + ExitWithExitCode 1 + } + + # Get info about which channels the build has already been promoted to + $buildInfo = Get-MaestroBuild -BuildId $BuildId + + if (!$buildInfo) { + Write-Host "Build with BAR ID $BuildId was not found in BAR!" + ExitWithExitCode 1 + } + + # Find whether the build is already assigned to the channel or not + if ($buildInfo.channels) { + foreach ($channel in $buildInfo.channels) { + if ($channel.Id -eq $ChannelId) { + Write-Host "The build with BAR ID $BuildId is already on channel $ChannelId!" + ExitWithExitCode 0 + } + } + } + + Write-Host "Promoting build '$BuildId' to channel '$ChannelId'." + + Assign-BuildToChannel -BuildId $BuildId -ChannelId $ChannelId + + Write-Host "done." +} +catch { + Write-Host "There was an error while trying to promote build '$BuildId' to channel '$ChannelId'" + Write-Host $_ + Write-Host $_.ScriptStackTrace +} diff --git a/eng/common/post-build/setup-maestro-vars.ps1 b/eng/common/post-build/setup-maestro-vars.ps1 new file mode 100644 index 000000000..d7f64dc63 --- /dev/null +++ b/eng/common/post-build/setup-maestro-vars.ps1 @@ -0,0 +1,26 @@ +param( + [Parameter(Mandatory=$true)][string] $ReleaseConfigsPath # Full path to ReleaseConfigs.txt asset +) + +. $PSScriptRoot\post-build-utils.ps1 + +try { + $Content = Get-Content $ReleaseConfigsPath + + $BarId = $Content | Select -Index 0 + + $Channels = "" + $Content | Select -Index 1 | ForEach-Object { $Channels += "$_ ," } + + $IsStableBuild = $Content | Select -Index 2 + + Write-PipelineSetVariable -Name 'BARBuildId' -Value $BarId + Write-PipelineSetVariable -Name 'InitialChannels' -Value "$Channels" + Write-PipelineSetVariable -Name 'IsStableBuild' -Value $IsStableBuild +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/sourcelink-cli-init.ps1 b/eng/common/post-build/sourcelink-cli-init.ps1 new file mode 100644 index 000000000..9eaa25b3b --- /dev/null +++ b/eng/common/post-build/sourcelink-cli-init.ps1 @@ -0,0 +1,29 @@ +param ( + $sourcelinkCliVersion = $null +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 + +. $PSScriptRoot\..\tools.ps1 + +$verbosity = "minimal" + +function InstallSourcelinkCli ($sourcelinkCliVersion) { + $sourcelinkCliPackageName = "sourcelink" + + $dotnetRoot = InitializeDotNetCli -install:$true + $dotnet = "$dotnetRoot\dotnet.exe" + $toolList = & "$dotnet" tool list --global + + if (($toolList -like "*$sourcelinkCliPackageName*") -and ($toolList -like "*$sourcelinkCliVersion*")) { + Write-Host "SourceLink CLI version $sourcelinkCliVersion is already installed." + } + else { + Write-Host "Installing SourceLink CLI version $sourcelinkCliVersion..." + Write-Host "You may need to restart your command window if this is the first dotnet tool you have installed." + & "$dotnet" tool install $sourcelinkCliPackageName --version $sourcelinkCliVersion --verbosity $verbosity --global + } +} + +InstallSourcelinkCli $sourcelinkCliVersion diff --git a/eng/common/post-build/sourcelink-validation.ps1 b/eng/common/post-build/sourcelink-validation.ps1 new file mode 100644 index 000000000..41e01ae6e --- /dev/null +++ b/eng/common/post-build/sourcelink-validation.ps1 @@ -0,0 +1,227 @@ +param( + [Parameter(Mandatory=$true)][string] $InputPath, # Full path to directory where Symbols.NuGet packages to be checked are stored + [Parameter(Mandatory=$true)][string] $ExtractPath, # Full path to directory where the packages will be extracted during validation + [Parameter(Mandatory=$true)][string] $GHRepoName, # GitHub name of the repo including the Org. E.g., dotnet/arcade + [Parameter(Mandatory=$true)][string] $GHCommit, # GitHub commit SHA used to build the packages + [Parameter(Mandatory=$true)][string] $SourcelinkCliVersion # Version of SourceLink CLI to use +) + +. $PSScriptRoot\post-build-utils.ps1 + +# Cache/HashMap (File -> Exist flag) used to consult whether a file exist +# in the repository at a specific commit point. This is populated by inserting +# all files present in the repo at a specific commit point. +$global:RepoFiles = @{} + +$ValidatePackage = { + param( + [string] $PackagePath # Full path to a Symbols.NuGet package + ) + + . $using:PSScriptRoot\..\tools.ps1 + + # Ensure input file exist + if (!(Test-Path $PackagePath)) { + Write-PipelineTaskError "Input file does not exist: $PackagePath" + ExitWithExitCode 1 + } + + # Extensions for which we'll look for SourceLink information + # For now we'll only care about Portable & Embedded PDBs + $RelevantExtensions = @(".dll", ".exe", ".pdb") + + Write-Host -NoNewLine "Validating" ([System.IO.Path]::GetFileName($PackagePath)) "... " + + $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) + $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageId + $FailedFiles = 0 + + Add-Type -AssemblyName System.IO.Compression.FileSystem + + [System.IO.Directory]::CreateDirectory($ExtractPath); + + try { + $zip = [System.IO.Compression.ZipFile]::OpenRead($PackagePath) + + $zip.Entries | + Where-Object {$RelevantExtensions -contains [System.IO.Path]::GetExtension($_.Name)} | + ForEach-Object { + $FileName = $_.FullName + $Extension = [System.IO.Path]::GetExtension($_.Name) + $FakeName = -Join((New-Guid), $Extension) + $TargetFile = Join-Path -Path $ExtractPath -ChildPath $FakeName + + # We ignore resource DLLs + if ($FileName.EndsWith(".resources.dll")) { + return + } + + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $TargetFile, $true) + + $ValidateFile = { + param( + [string] $FullPath, # Full path to the module that has to be checked + [string] $RealPath, + [ref] $FailedFiles + ) + + $sourcelinkExe = "$env:USERPROFILE\.dotnet\tools" + $sourcelinkExe = Resolve-Path "$sourcelinkExe\sourcelink.exe" + $SourceLinkInfos = & $sourcelinkExe print-urls $FullPath | Out-String + + if ($LASTEXITCODE -eq 0 -and -not ([string]::IsNullOrEmpty($SourceLinkInfos))) { + $NumFailedLinks = 0 + + # We only care about Http addresses + $Matches = (Select-String '(http[s]?)(:\/\/)([^\s,]+)' -Input $SourceLinkInfos -AllMatches).Matches + + if ($Matches.Count -ne 0) { + $Matches.Value | + ForEach-Object { + $Link = $_ + $CommitUrl = "https://raw.githubusercontent.com/${using:GHRepoName}/${using:GHCommit}/" + + $FilePath = $Link.Replace($CommitUrl, "") + $Status = 200 + $Cache = $using:RepoFiles + + if ( !($Cache.ContainsKey($FilePath)) ) { + try { + $Uri = $Link -as [System.URI] + + # Only GitHub links are valid + if ($Uri.AbsoluteURI -ne $null -and ($Uri.Host -match "github" -or $Uri.Host -match "githubusercontent")) { + $Status = (Invoke-WebRequest -Uri $Link -UseBasicParsing -Method HEAD -TimeoutSec 5).StatusCode + } + else { + $Status = 0 + } + } + catch { + write-host $_ + $Status = 0 + } + } + + if ($Status -ne 200) { + if ($NumFailedLinks -eq 0) { + if ($FailedFiles.Value -eq 0) { + Write-Host + } + + Write-Host "`tFile $RealPath has broken links:" + } + + Write-Host "`t`tFailed to retrieve $Link" + + $NumFailedLinks++ + } + } + } + + if ($NumFailedLinks -ne 0) { + $FailedFiles.value++ + $global:LASTEXITCODE = 1 + } + } + } + + &$ValidateFile $TargetFile $FileName ([ref]$FailedFiles) + } + } + catch { + + } + finally { + $zip.Dispose() + } + + if ($FailedFiles -eq 0) { + Write-Host "Passed." + } + else { + Write-PipelineTaskError "$PackagePath has broken SourceLink links." + } +} + +function ValidateSourceLinkLinks { + if (!($GHRepoName -Match "^[^\s\/]+/[^\s\/]+$")) { + if (!($GHRepoName -Match "^[^\s-]+-[^\s]+$")) { + Write-PipelineTaskError "GHRepoName should be in the format / or -" + ExitWithExitCode 1 + } + else { + $GHRepoName = $GHRepoName -replace '^([^\s-]+)-([^\s]+)$', '$1/$2'; + } + } + + if (!($GHCommit -Match "^[0-9a-fA-F]{40}$")) { + Write-PipelineTaskError "GHCommit should be a 40 chars hexadecimal string" + ExitWithExitCode 1 + } + + $RepoTreeURL = -Join("http://api.github.com/repos/", $GHRepoName, "/git/trees/", $GHCommit, "?recursive=1") + $CodeExtensions = @(".cs", ".vb", ".fs", ".fsi", ".fsx", ".fsscript") + + try { + # Retrieve the list of files in the repo at that particular commit point and store them in the RepoFiles hash + $Data = Invoke-WebRequest $RepoTreeURL -UseBasicParsing | ConvertFrom-Json | Select-Object -ExpandProperty tree + + foreach ($file in $Data) { + $Extension = [System.IO.Path]::GetExtension($file.path) + + if ($CodeExtensions.Contains($Extension)) { + $RepoFiles[$file.path] = 1 + } + } + } + catch { + Write-PipelineTaskError "Problems downloading the list of files from the repo. Url used: $RepoTreeURL" + Write-Host $_ + ExitWithExitCode 1 + } + + if (Test-Path $ExtractPath) { + Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue + } + + # Process each NuGet package in parallel + $Jobs = @() + Get-ChildItem "$InputPath\*.symbols.nupkg" | + ForEach-Object { + $Jobs += Start-Job -ScriptBlock $ValidatePackage -ArgumentList $_.FullName + } + + foreach ($Job in $Jobs) { + Wait-Job -Id $Job.Id | Receive-Job + } +} + +function InstallSourcelinkCli { + $sourcelinkCliPackageName = "sourcelink" + + $dotnetRoot = InitializeDotNetCli -install:$true + $dotnet = "$dotnetRoot\dotnet.exe" + $toolList = & "$dotnet" tool list --global + + if (($toolList -like "*$sourcelinkCliPackageName*") -and ($toolList -like "*$sourcelinkCliVersion*")) { + Write-Host "SourceLink CLI version $sourcelinkCliVersion is already installed." + } + else { + Write-Host "Installing SourceLink CLI version $sourcelinkCliVersion..." + Write-Host "You may need to restart your command window if this is the first dotnet tool you have installed." + & "$dotnet" tool install $sourcelinkCliPackageName --version $sourcelinkCliVersion --verbosity "minimal" --global + } +} + +try { + InstallSourcelinkCli + + ValidateSourceLinkLinks +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/symbols-validation.ps1 b/eng/common/post-build/symbols-validation.ps1 new file mode 100644 index 000000000..d5ec51b15 --- /dev/null +++ b/eng/common/post-build/symbols-validation.ps1 @@ -0,0 +1,189 @@ +param( + [Parameter(Mandatory=$true)][string] $InputPath, # Full path to directory where NuGet packages to be checked are stored + [Parameter(Mandatory=$true)][string] $ExtractPath, # Full path to directory where the packages will be extracted during validation + [Parameter(Mandatory=$true)][string] $DotnetSymbolVersion # Version of dotnet symbol to use +) + +. $PSScriptRoot\post-build-utils.ps1 + +Add-Type -AssemblyName System.IO.Compression.FileSystem + +function FirstMatchingSymbolDescriptionOrDefault { + param( + [string] $FullPath, # Full path to the module that has to be checked + [string] $TargetServerParam, # Parameter to pass to `Symbol Tool` indicating the server to lookup for symbols + [string] $SymbolsPath + ) + + $FileName = [System.IO.Path]::GetFileName($FullPath) + $Extension = [System.IO.Path]::GetExtension($FullPath) + + # Those below are potential symbol files that the `dotnet symbol` might + # return. Which one will be returned depend on the type of file we are + # checking and which type of file was uploaded. + + # The file itself is returned + $SymbolPath = $SymbolsPath + "\" + $FileName + + # PDB file for the module + $PdbPath = $SymbolPath.Replace($Extension, ".pdb") + + # PDB file for R2R module (created by crossgen) + $NGenPdb = $SymbolPath.Replace($Extension, ".ni.pdb") + + # DBG file for a .so library + $SODbg = $SymbolPath.Replace($Extension, ".so.dbg") + + # DWARF file for a .dylib + $DylibDwarf = $SymbolPath.Replace($Extension, ".dylib.dwarf") + + $dotnetsymbolExe = "$env:USERPROFILE\.dotnet\tools" + $dotnetsymbolExe = Resolve-Path "$dotnetsymbolExe\dotnet-symbol.exe" + + & $dotnetsymbolExe --symbols --modules --windows-pdbs $TargetServerParam $FullPath -o $SymbolsPath | Out-Null + + if (Test-Path $PdbPath) { + return "PDB" + } + elseif (Test-Path $NGenPdb) { + return "NGen PDB" + } + elseif (Test-Path $SODbg) { + return "DBG for SO" + } + elseif (Test-Path $DylibDwarf) { + return "Dwarf for Dylib" + } + elseif (Test-Path $SymbolPath) { + return "Module" + } + else { + return $null + } +} + +function CountMissingSymbols { + param( + [string] $PackagePath # Path to a NuGet package + ) + + # Ensure input file exist + if (!(Test-Path $PackagePath)) { + Write-PipelineTaskError "Input file does not exist: $PackagePath" + ExitWithExitCode 1 + } + + # Extensions for which we'll look for symbols + $RelevantExtensions = @(".dll", ".exe", ".so", ".dylib") + + # How many files are missing symbol information + $MissingSymbols = 0 + + $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) + $PackageGuid = New-Guid + $ExtractPath = Join-Path -Path $ExtractPath -ChildPath $PackageGuid + $SymbolsPath = Join-Path -Path $ExtractPath -ChildPath "Symbols" + + [System.IO.Compression.ZipFile]::ExtractToDirectory($PackagePath, $ExtractPath) + + Get-ChildItem -Recurse $ExtractPath | + Where-Object {$RelevantExtensions -contains $_.Extension} | + ForEach-Object { + if ($_.FullName -Match "\\ref\\") { + Write-Host "`t Ignoring reference assembly file" $_.FullName + return + } + + $SymbolsOnMSDL = FirstMatchingSymbolDescriptionOrDefault $_.FullName "--microsoft-symbol-server" $SymbolsPath + $SymbolsOnSymWeb = FirstMatchingSymbolDescriptionOrDefault $_.FullName "--internal-server" $SymbolsPath + + Write-Host -NoNewLine "`t Checking file" $_.FullName "... " + + if ($SymbolsOnMSDL -ne $null -and $SymbolsOnSymWeb -ne $null) { + Write-Host "Symbols found on MSDL (" $SymbolsOnMSDL ") and SymWeb (" $SymbolsOnSymWeb ")" + } + else { + $MissingSymbols++ + + if ($SymbolsOnMSDL -eq $null -and $SymbolsOnSymWeb -eq $null) { + Write-Host "No symbols found on MSDL or SymWeb!" + } + else { + if ($SymbolsOnMSDL -eq $null) { + Write-Host "No symbols found on MSDL!" + } + else { + Write-Host "No symbols found on SymWeb!" + } + } + } + } + + Pop-Location + + return $MissingSymbols +} + +function CheckSymbolsAvailable { + if (Test-Path $ExtractPath) { + Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue + } + + Get-ChildItem "$InputPath\*.nupkg" | + ForEach-Object { + $FileName = $_.Name + + # These packages from Arcade-Services include some native libraries that + # our current symbol uploader can't handle. Below is a workaround until + # we get issue: https://github.com/dotnet/arcade/issues/2457 sorted. + if ($FileName -Match "Microsoft\.DotNet\.Darc\.") { + Write-Host "Ignoring Arcade-services file: $FileName" + Write-Host + return + } + elseif ($FileName -Match "Microsoft\.DotNet\.Maestro\.Tasks\.") { + Write-Host "Ignoring Arcade-services file: $FileName" + Write-Host + return + } + + Write-Host "Validating $FileName " + $Status = CountMissingSymbols "$InputPath\$FileName" + + if ($Status -ne 0) { + Write-PipelineTaskError "Missing symbols for $Status modules in the package $FileName" + ExitWithExitCode $exitCode + } + + Write-Host + } +} + +function Installdotnetsymbol { + $dotnetsymbolPackageName = "dotnet-symbol" + + $dotnetRoot = InitializeDotNetCli -install:$true + $dotnet = "$dotnetRoot\dotnet.exe" + $toolList = & "$dotnet" tool list --global + + if (($toolList -like "*$dotnetsymbolPackageName*") -and ($toolList -like "*$dotnetsymbolVersion*")) { + Write-Host "dotnet-symbol version $dotnetsymbolVersion is already installed." + } + else { + Write-Host "Installing dotnet-symbol version $dotnetsymbolVersion..." + Write-Host "You may need to restart your command window if this is the first dotnet tool you have installed." + & "$dotnet" tool install $dotnetsymbolPackageName --version $dotnetsymbolVersion --verbosity "minimal" --global + } +} + +try { + Installdotnetsymbol + + CheckSymbolsAvailable +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/trigger-subscriptions.ps1 b/eng/common/post-build/trigger-subscriptions.ps1 new file mode 100644 index 000000000..926d5b455 --- /dev/null +++ b/eng/common/post-build/trigger-subscriptions.ps1 @@ -0,0 +1,57 @@ +param( + [Parameter(Mandatory=$true)][string] $SourceRepo, + [Parameter(Mandatory=$true)][int] $ChannelId, + [Parameter(Mandatory=$true)][string] $MaestroApiAccessToken, + [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = "https://maestro-prod.westus2.cloudapp.azure.com", + [Parameter(Mandatory=$false)][string] $MaestroApiVersion = "2019-01-16" +) + +. $PSScriptRoot\post-build-utils.ps1 + +# Get all the $SourceRepo subscriptions +$normalizedSourceRepo = $SourceRepo.Replace('dnceng@', '') +$subscriptions = Get-MaestroSubscriptions -SourceRepository $normalizedSourceRepo -ChannelId $ChannelId + +if (!$subscriptions) { + Write-Host "No subscriptions found for source repo '$normalizedSourceRepo' in channel '$ChannelId'" + ExitWithExitCode 0 +} + +$subscriptionsToTrigger = New-Object System.Collections.Generic.List[string] +$failedTriggeredSubscription = $false + +# Get all enabled subscriptions that need dependency flow on 'everyBuild' +foreach ($subscription in $subscriptions) { + if ($subscription.enabled -and $subscription.policy.updateFrequency -like 'everyBuild' -and $subscription.channel.id -eq $ChannelId) { + Write-Host "Should trigger this subscription: $subscription.id" + [void]$subscriptionsToTrigger.Add($subscription.id) + } +} + +foreach ($subscriptionToTrigger in $subscriptionsToTrigger) { + try { + Write-Host "Triggering subscription '$subscriptionToTrigger'." + + Trigger-Subscription -SubscriptionId $subscriptionToTrigger + + Write-Host "done." + } + catch + { + Write-Host "There was an error while triggering subscription '$subscriptionToTrigger'" + Write-Host $_ + Write-Host $_.ScriptStackTrace + $failedTriggeredSubscription = $true + } +} + +if ($subscriptionsToTrigger.Count -eq 0) { + Write-Host "No subscription matched source repo '$normalizedSourceRepo' and channel ID '$ChannelId'." +} +elseif ($failedTriggeredSubscription) { + Write-Host "At least one subscription failed to be triggered..." + ExitWithExitCode 1 +} +else { + Write-Host "All subscriptions were triggered successfully!" +} diff --git a/eng/common/sdk-task.ps1 b/eng/common/sdk-task.ps1 new file mode 100644 index 000000000..d0eec5163 --- /dev/null +++ b/eng/common/sdk-task.ps1 @@ -0,0 +1,79 @@ +[CmdletBinding(PositionalBinding=$false)] +Param( + [string] $configuration = "Debug", + [string] $task, + [string] $verbosity = "minimal", + [string] $msbuildEngine = $null, + [switch] $restore, + [switch] $prepareMachine, + [switch] $help, + [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties +) + +$ci = $true +$binaryLog = $true +$warnAsError = $true + +. $PSScriptRoot\tools.ps1 + +function Print-Usage() { + Write-Host "Common settings:" + Write-Host " -task Name of Arcade task (name of a project in SdkTasks directory of the Arcade SDK package)" + Write-Host " -restore Restore dependencies" + Write-Host " -verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]" + Write-Host " -help Print help and exit" + Write-Host "" + + Write-Host "Advanced settings:" + Write-Host " -prepareMachine Prepare machine for CI run" + Write-Host " -msbuildEngine Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." + Write-Host "" + Write-Host "Command line arguments not listed above are passed thru to msbuild." +} + +function Build([string]$target) { + $logSuffix = if ($target -eq "Execute") { "" } else { ".$target" } + $log = Join-Path $LogDir "$task$logSuffix.binlog" + $outputPath = Join-Path $ToolsetDir "$task\\" + + MSBuild $taskProject ` + /bl:$log ` + /t:$target ` + /p:Configuration=$configuration ` + /p:RepoRoot=$RepoRoot ` + /p:BaseIntermediateOutputPath=$outputPath ` + @properties +} + +try { + if ($help -or (($null -ne $properties) -and ($properties.Contains("/help") -or $properties.Contains("/?")))) { + Print-Usage + exit 0 + } + + if ($task -eq "") { + Write-Host "Missing required parameter '-task '" -ForegroundColor Red + Print-Usage + ExitWithExitCode 1 + } + + $taskProject = GetSdkTaskProject $task + if (!(Test-Path $taskProject)) { + Write-Host "Unknown task: $task" -ForegroundColor Red + ExitWithExitCode 1 + } + + if ($restore) { + Build "Restore" + } + + Build "Execute" +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} + +ExitWithExitCode 0 diff --git a/eng/common/sdl/NuGet.config b/eng/common/sdl/NuGet.config new file mode 100644 index 000000000..0c5451c11 --- /dev/null +++ b/eng/common/sdl/NuGet.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/eng/common/sdl/execute-all-sdl-tools.ps1 b/eng/common/sdl/execute-all-sdl-tools.ps1 new file mode 100644 index 000000000..aab7589f2 --- /dev/null +++ b/eng/common/sdl/execute-all-sdl-tools.ps1 @@ -0,0 +1,99 @@ +Param( + [string] $GuardianPackageName, # Required: the name of guardian CLI package (not needed if GuardianCliLocation is specified) + [string] $NugetPackageDirectory, # Required: directory where NuGet packages are installed (not needed if GuardianCliLocation is specified) + [string] $GuardianCliLocation, # Optional: Direct location of Guardian CLI executable if GuardianPackageName & NugetPackageDirectory are not specified + [string] $Repository=$env:BUILD_REPOSITORY_NAME, # Required: the name of the repository (e.g. dotnet/arcade) + [string] $BranchName=$env:BUILD_SOURCEBRANCH, # Optional: name of branch or version of gdn settings; defaults to master + [string] $SourceDirectory=$env:BUILD_SOURCESDIRECTORY, # Required: the directory where source files are located + [string] $ArtifactsDirectory = (Join-Path $env:BUILD_SOURCESDIRECTORY ("artifacts")), # Required: the directory where build artifacts are located + [string] $AzureDevOpsAccessToken, # Required: access token for dnceng; should be provided via KeyVault + [string[]] $SourceToolsList, # Optional: list of SDL tools to run on source code + [string[]] $ArtifactToolsList, # Optional: list of SDL tools to run on built artifacts + [bool] $TsaPublish=$False, # Optional: true will publish results to TSA; only set to true after onboarding to TSA; TSA is the automated framework used to upload test results as bugs. + [string] $TsaBranchName=$env:BUILD_SOURCEBRANCH, # Optional: required for TSA publish; defaults to $(Build.SourceBranchName); TSA is the automated framework used to upload test results as bugs. + [string] $TsaRepositoryName=$env:BUILD_REPOSITORY_NAME, # Optional: TSA repository name; will be generated automatically if not submitted; TSA is the automated framework used to upload test results as bugs. + [string] $BuildNumber=$env:BUILD_BUILDNUMBER, # Optional: required for TSA publish; defaults to $(Build.BuildNumber) + [bool] $UpdateBaseline=$False, # Optional: if true, will update the baseline in the repository; should only be run after fixing any issues which need to be fixed + [bool] $TsaOnboard=$False, # Optional: if true, will onboard the repository to TSA; should only be run once; TSA is the automated framework used to upload test results as bugs. + [string] $TsaInstanceUrl, # Optional: only needed if TsaOnboard or TsaPublish is true; the instance-url registered with TSA; TSA is the automated framework used to upload test results as bugs. + [string] $TsaCodebaseName, # Optional: only needed if TsaOnboard or TsaPublish is true; the name of the codebase registered with TSA; TSA is the automated framework used to upload test results as bugs. + [string] $TsaProjectName, # Optional: only needed if TsaOnboard or TsaPublish is true; the name of the project registered with TSA; TSA is the automated framework used to upload test results as bugs. + [string] $TsaNotificationEmail, # Optional: only needed if TsaOnboard is true; the email(s) which will receive notifications of TSA bug filings (e.g. alias@microsoft.com); TSA is the automated framework used to upload test results as bugs. + [string] $TsaCodebaseAdmin, # Optional: only needed if TsaOnboard is true; the aliases which are admins of the TSA codebase (e.g. DOMAIN\alias); TSA is the automated framework used to upload test results as bugs. + [string] $TsaBugAreaPath, # Optional: only needed if TsaOnboard is true; the area path where TSA will file bugs in AzDO; TSA is the automated framework used to upload test results as bugs. + [string] $TsaIterationPath, # Optional: only needed if TsaOnboard is true; the iteration path where TSA will file bugs in AzDO; TSA is the automated framework used to upload test results as bugs. + [string] $GuardianLoggerLevel="Standard", # Optional: the logger level for the Guardian CLI; options are Trace, Verbose, Standard, Warning, and Error + [string[]] $CrScanAdditionalRunConfigParams, # Optional: Additional Params to custom build a CredScan run config in the format @("xyz:abc","sdf:1") + [string[]] $PoliCheckAdditionalRunConfigParams # Optional: Additional Params to custom build a Policheck run config in the format @("xyz:abc","sdf:1") +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 +$LASTEXITCODE = 0 + +#Replace repo names to the format of org/repo +if (!($Repository.contains('/'))) { + $RepoName = $Repository -replace '(.*?)-(.*)', '$1/$2'; +} +else{ + $RepoName = $Repository; +} + +if ($GuardianPackageName) { + $guardianCliLocation = Join-Path $NugetPackageDirectory (Join-Path $GuardianPackageName (Join-Path "tools" "guardian.cmd")) +} else { + $guardianCliLocation = $GuardianCliLocation +} + +$ValidPath = Test-Path $guardianCliLocation + +if ($ValidPath -eq $False) +{ + Write-Host "Invalid Guardian CLI Location." + exit 1 +} + +& $(Join-Path $PSScriptRoot "init-sdl.ps1") -GuardianCliLocation $guardianCliLocation -Repository $RepoName -BranchName $BranchName -WorkingDirectory $ArtifactsDirectory -AzureDevOpsAccessToken $AzureDevOpsAccessToken -GuardianLoggerLevel $GuardianLoggerLevel +$gdnFolder = Join-Path $ArtifactsDirectory ".gdn" + +if ($TsaOnboard) { + if ($TsaCodebaseName -and $TsaNotificationEmail -and $TsaCodebaseAdmin -and $TsaBugAreaPath) { + Write-Host "$guardianCliLocation tsa-onboard --codebase-name `"$TsaCodebaseName`" --notification-alias `"$TsaNotificationEmail`" --codebase-admin `"$TsaCodebaseAdmin`" --instance-url `"$TsaInstanceUrl`" --project-name `"$TsaProjectName`" --area-path `"$TsaBugAreaPath`" --iteration-path `"$TsaIterationPath`" --working-directory $ArtifactsDirectory --logger-level $GuardianLoggerLevel" + & $guardianCliLocation tsa-onboard --codebase-name "$TsaCodebaseName" --notification-alias "$TsaNotificationEmail" --codebase-admin "$TsaCodebaseAdmin" --instance-url "$TsaInstanceUrl" --project-name "$TsaProjectName" --area-path "$TsaBugAreaPath" --iteration-path "$TsaIterationPath" --working-directory $ArtifactsDirectory --logger-level $GuardianLoggerLevel + if ($LASTEXITCODE -ne 0) { + Write-Host "Guardian tsa-onboard failed with exit code $LASTEXITCODE." + exit $LASTEXITCODE + } + } else { + Write-Host "Could not onboard to TSA -- not all required values ($$TsaCodebaseName, $$TsaNotificationEmail, $$TsaCodebaseAdmin, $$TsaBugAreaPath) were specified." + exit 1 + } +} + +if ($ArtifactToolsList -and $ArtifactToolsList.Count -gt 0) { + & $(Join-Path $PSScriptRoot "run-sdl.ps1") -GuardianCliLocation $guardianCliLocation -WorkingDirectory $ArtifactsDirectory -TargetDirectory $ArtifactsDirectory -GdnFolder $gdnFolder -ToolsList $ArtifactToolsList -AzureDevOpsAccessToken $AzureDevOpsAccessToken -UpdateBaseline $UpdateBaseline -GuardianLoggerLevel $GuardianLoggerLevel -CrScanAdditionalRunConfigParams $CrScanAdditionalRunConfigParams -PoliCheckAdditionalRunConfigParams $PoliCheckAdditionalRunConfigParams +} +if ($SourceToolsList -and $SourceToolsList.Count -gt 0) { + & $(Join-Path $PSScriptRoot "run-sdl.ps1") -GuardianCliLocation $guardianCliLocation -WorkingDirectory $ArtifactsDirectory -TargetDirectory $SourceDirectory -GdnFolder $gdnFolder -ToolsList $SourceToolsList -AzureDevOpsAccessToken $AzureDevOpsAccessToken -UpdateBaseline $UpdateBaseline -GuardianLoggerLevel $GuardianLoggerLevel -CrScanAdditionalRunConfigParams $CrScanAdditionalRunConfigParams -PoliCheckAdditionalRunConfigParams $PoliCheckAdditionalRunConfigParams +} + +if ($UpdateBaseline) { + & (Join-Path $PSScriptRoot "push-gdn.ps1") -Repository $RepoName -BranchName $BranchName -GdnFolder $GdnFolder -AzureDevOpsAccessToken $AzureDevOpsAccessToken -PushReason "Update baseline" +} + +if ($TsaPublish) { + if ($TsaBranchName -and $BuildNumber) { + if (-not $TsaRepositoryName) { + $TsaRepositoryName = "$($Repository)-$($BranchName)" + } + Write-Host "$guardianCliLocation tsa-publish --all-tools --repository-name `"$TsaRepositoryName`" --branch-name `"$TsaBranchName`" --build-number `"$BuildNumber`" --codebase-name `"$TsaCodebaseName`" --notification-alias `"$TsaNotificationEmail`" --codebase-admin `"$TsaCodebaseAdmin`" --instance-url `"$TsaInstanceUrl`" --project-name `"$TsaProjectName`" --area-path `"$TsaBugAreaPath`" --iteration-path `"$TsaIterationPath`" --working-directory $SourceDirectory --logger-level $GuardianLoggerLevel" + & $guardianCliLocation tsa-publish --all-tools --repository-name "$TsaRepositoryName" --branch-name "$TsaBranchName" --build-number "$BuildNumber" --codebase-name "$TsaCodebaseName" --notification-alias "$TsaNotificationEmail" --codebase-admin "$TsaCodebaseAdmin" --instance-url "$TsaInstanceUrl" --project-name "$TsaProjectName" --area-path "$TsaBugAreaPath" --iteration-path "$TsaIterationPath" --working-directory $ArtifactsDirectory --logger-level $GuardianLoggerLevel + if ($LASTEXITCODE -ne 0) { + Write-Host "Guardian tsa-publish failed with exit code $LASTEXITCODE." + exit $LASTEXITCODE + } + } else { + Write-Host "Could not publish to TSA -- not all required values ($$TsaBranchName, $$BuildNumber) were specified." + exit 1 + } +} diff --git a/eng/common/sdl/extract-artifact-packages.ps1 b/eng/common/sdl/extract-artifact-packages.ps1 new file mode 100644 index 000000000..1fdbb1432 --- /dev/null +++ b/eng/common/sdl/extract-artifact-packages.ps1 @@ -0,0 +1,70 @@ +param( + [Parameter(Mandatory=$true)][string] $InputPath, # Full path to directory where artifact packages are stored + [Parameter(Mandatory=$true)][string] $ExtractPath # Full path to directory where the packages will be extracted +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 +$ExtractPackage = { + param( + [string] $PackagePath # Full path to a NuGet package + ) + + if (!(Test-Path $PackagePath)) { + Write-PipelineTaskError "Input file does not exist: $PackagePath" + ExitWithExitCode 1 + } + + $RelevantExtensions = @(".dll", ".exe", ".pdb") + Write-Host -NoNewLine "Extracting" ([System.IO.Path]::GetFileName($PackagePath)) "... " + + $PackageId = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath) + $ExtractPath = Join-Path -Path $using:ExtractPath -ChildPath $PackageId + + Add-Type -AssemblyName System.IO.Compression.FileSystem + + [System.IO.Directory]::CreateDirectory($ExtractPath); + + try { + $zip = [System.IO.Compression.ZipFile]::OpenRead($PackagePath) + + $zip.Entries | + Where-Object {$RelevantExtensions -contains [System.IO.Path]::GetExtension($_.Name)} | + ForEach-Object { + $TargetFile = Join-Path -Path $ExtractPath -ChildPath $_.Name + + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $TargetFile, $true) + } + } + catch { + + } + finally { + $zip.Dispose() + } + } + function ExtractArtifacts { + if (!(Test-Path $InputPath)) { + Write-Host "Input Path does not exist: $InputPath" + ExitWithExitCode 0 + } + $Jobs = @() + Get-ChildItem "$InputPath\*.nupkg" | + ForEach-Object { + $Jobs += Start-Job -ScriptBlock $ExtractPackage -ArgumentList $_.FullName + } + + foreach ($Job in $Jobs) { + Wait-Job -Id $Job.Id | Receive-Job + } +} + +try { + Measure-Command { ExtractArtifacts } +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} diff --git a/eng/common/sdl/init-sdl.ps1 b/eng/common/sdl/init-sdl.ps1 new file mode 100644 index 000000000..26e01c067 --- /dev/null +++ b/eng/common/sdl/init-sdl.ps1 @@ -0,0 +1,48 @@ +Param( + [string] $GuardianCliLocation, + [string] $Repository, + [string] $BranchName="master", + [string] $WorkingDirectory, + [string] $AzureDevOpsAccessToken, + [string] $GuardianLoggerLevel="Standard" +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 +$LASTEXITCODE = 0 + +# Construct basic auth from AzDO access token; construct URI to the repository's gdn folder stored in that repository; construct location of zip file +$encodedPat = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$AzureDevOpsAccessToken")) +$escapedRepository = [Uri]::EscapeDataString("/$Repository/$BranchName/.gdn") +$uri = "https://dev.azure.com/dnceng/internal/_apis/git/repositories/sdl-tool-cfg/Items?path=$escapedRepository&versionDescriptor[versionOptions]=0&`$format=zip&api-version=5.0-preview.1" +$zipFile = "$WorkingDirectory/gdn.zip" + +Add-Type -AssemblyName System.IO.Compression.FileSystem +$gdnFolder = (Join-Path $WorkingDirectory ".gdn") +Try +{ + # We try to download the zip; if the request fails (e.g. the file doesn't exist), we catch it and init guardian instead + Write-Host "Downloading gdn folder from internal config repostiory..." + Invoke-WebRequest -Headers @{ "Accept"="application/zip"; "Authorization"="Basic $encodedPat" } -Uri $uri -OutFile $zipFile + if (Test-Path $gdnFolder) { + # Remove the gdn folder if it exists (it shouldn't unless there's too much caching; this is just in case) + Remove-Item -Force -Recurse $gdnFolder + } + [System.IO.Compression.ZipFile]::ExtractToDirectory($zipFile, $WorkingDirectory) + Write-Host $gdnFolder +} Catch [System.Net.WebException] { + # if the folder does not exist, we'll do a guardian init and push it to the remote repository + Write-Host "Initializing Guardian..." + Write-Host "$GuardianCliLocation init --working-directory $WorkingDirectory --logger-level $GuardianLoggerLevel" + & $GuardianCliLocation init --working-directory $WorkingDirectory --logger-level $GuardianLoggerLevel + if ($LASTEXITCODE -ne 0) { + Write-Error "Guardian init failed with exit code $LASTEXITCODE." + } + # We create the mainbaseline so it can be edited later + Write-Host "$GuardianCliLocation baseline --working-directory $WorkingDirectory --name mainbaseline" + & $GuardianCliLocation baseline --working-directory $WorkingDirectory --name mainbaseline + if ($LASTEXITCODE -ne 0) { + Write-Error "Guardian baseline failed with exit code $LASTEXITCODE." + } + & $(Join-Path $PSScriptRoot "push-gdn.ps1") -Repository $Repository -BranchName $BranchName -GdnFolder $gdnFolder -AzureDevOpsAccessToken $AzureDevOpsAccessToken -PushReason "Initialize gdn folder" +} \ No newline at end of file diff --git a/eng/common/sdl/packages.config b/eng/common/sdl/packages.config new file mode 100644 index 000000000..3f97ac2f1 --- /dev/null +++ b/eng/common/sdl/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/eng/common/sdl/push-gdn.ps1 b/eng/common/sdl/push-gdn.ps1 new file mode 100644 index 000000000..79c707d6d --- /dev/null +++ b/eng/common/sdl/push-gdn.ps1 @@ -0,0 +1,51 @@ +Param( + [string] $Repository, + [string] $BranchName="master", + [string] $GdnFolder, + [string] $AzureDevOpsAccessToken, + [string] $PushReason +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 +$LASTEXITCODE = 0 + +# We create the temp directory where we'll store the sdl-config repository +$sdlDir = Join-Path $env:TEMP "sdl" +if (Test-Path $sdlDir) { + Remove-Item -Force -Recurse $sdlDir +} + +Write-Host "git clone https://dnceng:`$AzureDevOpsAccessToken@dev.azure.com/dnceng/internal/_git/sdl-tool-cfg $sdlDir" +git clone https://dnceng:$AzureDevOpsAccessToken@dev.azure.com/dnceng/internal/_git/sdl-tool-cfg $sdlDir +if ($LASTEXITCODE -ne 0) { + Write-Error "Git clone failed with exit code $LASTEXITCODE." +} +# We copy the .gdn folder from our local run into the git repository so it can be committed +$sdlRepositoryFolder = Join-Path (Join-Path (Join-Path $sdlDir $Repository) $BranchName) ".gdn" +if (Get-Command Robocopy) { + Robocopy /S $GdnFolder $sdlRepositoryFolder +} else { + rsync -r $GdnFolder $sdlRepositoryFolder +} +# cd to the sdl-config directory so we can run git there +Push-Location $sdlDir +# git add . --> git commit --> git push +Write-Host "git add ." +git add . +if ($LASTEXITCODE -ne 0) { + Write-Error "Git add failed with exit code $LASTEXITCODE." +} +Write-Host "git -c user.email=`"dn-bot@microsoft.com`" -c user.name=`"Dotnet Bot`" commit -m `"$PushReason for $Repository/$BranchName`"" +git -c user.email="dn-bot@microsoft.com" -c user.name="Dotnet Bot" commit -m "$PushReason for $Repository/$BranchName" +if ($LASTEXITCODE -ne 0) { + Write-Error "Git commit failed with exit code $LASTEXITCODE." +} +Write-Host "git push" +git push +if ($LASTEXITCODE -ne 0) { + Write-Error "Git push failed with exit code $LASTEXITCODE." +} + +# Return to the original directory +Pop-Location \ No newline at end of file diff --git a/eng/common/sdl/run-sdl.ps1 b/eng/common/sdl/run-sdl.ps1 new file mode 100644 index 000000000..d7b856445 --- /dev/null +++ b/eng/common/sdl/run-sdl.ps1 @@ -0,0 +1,67 @@ +Param( + [string] $GuardianCliLocation, + [string] $WorkingDirectory, + [string] $TargetDirectory, + [string] $GdnFolder, + [string[]] $ToolsList, + [string] $UpdateBaseline, + [string] $GuardianLoggerLevel="Standard", + [string[]] $CrScanAdditionalRunConfigParams, + [string[]] $PoliCheckAdditionalRunConfigParams +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 +$LASTEXITCODE = 0 + +# We store config files in the r directory of .gdn +Write-Host $ToolsList +$gdnConfigPath = Join-Path $GdnFolder "r" +$ValidPath = Test-Path $GuardianCliLocation + +if ($ValidPath -eq $False) +{ + Write-Host "Invalid Guardian CLI Location." + exit 1 +} + +foreach ($tool in $ToolsList) { + $gdnConfigFile = Join-Path $gdnConfigPath "$tool-configure.gdnconfig" + $config = $False + Write-Host $tool + # We have to manually configure tools that run on source to look at the source directory only + if ($tool -eq "credscan") { + Write-Host "$GuardianCliLocation configure --working-directory $WorkingDirectory --tool $tool --output-path $gdnConfigFile --logger-level $GuardianLoggerLevel --noninteractive --force --args `" TargetDirectory : $TargetDirectory `" $(If ($CrScanAdditionalRunConfigParams) {$CrScanAdditionalRunConfigParams})" + & $GuardianCliLocation configure --working-directory $WorkingDirectory --tool $tool --output-path $gdnConfigFile --logger-level $GuardianLoggerLevel --noninteractive --force --args " TargetDirectory : $TargetDirectory " $(If ($CrScanAdditionalRunConfigParams) {$CrScanAdditionalRunConfigParams}) + if ($LASTEXITCODE -ne 0) { + Write-Host "Guardian configure for $tool failed with exit code $LASTEXITCODE." + exit $LASTEXITCODE + } + $config = $True + } + if ($tool -eq "policheck") { + Write-Host "$GuardianCliLocation configure --working-directory $WorkingDirectory --tool $tool --output-path $gdnConfigFile --logger-level $GuardianLoggerLevel --noninteractive --force --args `" Target : $TargetDirectory `" $(If ($PoliCheckAdditionalRunConfigParams) {$PoliCheckAdditionalRunConfigParams})" + & $GuardianCliLocation configure --working-directory $WorkingDirectory --tool $tool --output-path $gdnConfigFile --logger-level $GuardianLoggerLevel --noninteractive --force --args " Target : $TargetDirectory " $(If ($PoliCheckAdditionalRunConfigParams) {$PoliCheckAdditionalRunConfigParams}) + if ($LASTEXITCODE -ne 0) { + Write-Host "Guardian configure for $tool failed with exit code $LASTEXITCODE." + exit $LASTEXITCODE + } + $config = $True + } + + Write-Host "$GuardianCliLocation run --working-directory $WorkingDirectory --tool $tool --baseline mainbaseline --update-baseline $UpdateBaseline --logger-level $GuardianLoggerLevel --config $gdnConfigFile $config" + if ($config) { + & $GuardianCliLocation run --working-directory $WorkingDirectory --tool $tool --baseline mainbaseline --update-baseline $UpdateBaseline --logger-level $GuardianLoggerLevel --config $gdnConfigFile + if ($LASTEXITCODE -ne 0) { + Write-Host "Guardian run for $tool using $gdnConfigFile failed with exit code $LASTEXITCODE." + exit $LASTEXITCODE + } + } else { + & $GuardianCliLocation run --working-directory $WorkingDirectory --tool $tool --baseline mainbaseline --update-baseline $UpdateBaseline --logger-level $GuardianLoggerLevel + if ($LASTEXITCODE -ne 0) { + Write-Host "Guardian run for $tool failed with exit code $LASTEXITCODE." + exit $LASTEXITCODE + } + } +} + diff --git a/eng/common/templates/job/execute-sdl.yml b/eng/common/templates/job/execute-sdl.yml new file mode 100644 index 000000000..f657a4dc9 --- /dev/null +++ b/eng/common/templates/job/execute-sdl.yml @@ -0,0 +1,54 @@ +parameters: + overrideParameters: '' # Optional: to override values for parameters. + additionalParameters: '' # Optional: parameters that need user specific values eg: '-SourceToolsList @("abc","def") -ArtifactToolsList @("ghi","jkl")' + continueOnError: false # optional: determines whether to continue the build if the step errors; + dependsOn: '' # Optional: dependencies of the job + +jobs: +- job: Run_SDL + dependsOn: ${{ parameters.dependsOn }} + displayName: Run SDL tool + variables: + - group: DotNet-VSTS-Bot + steps: + - checkout: self + clean: true + - task: DownloadBuildArtifacts@0 + displayName: Download Build Artifacts + inputs: + buildType: current + downloadType: specific files + matchingPattern: "**" + downloadPath: $(Build.SourcesDirectory)\artifacts + - powershell: eng/common/sdl/extract-artifact-packages.ps1 + -InputPath $(Build.SourcesDirectory)\artifacts\BlobArtifacts + -ExtractPath $(Build.SourcesDirectory)\artifacts\BlobArtifacts + displayName: Extract Blob Artifacts + continueOnError: ${{ parameters.continueOnError }} + - powershell: eng/common/sdl/extract-artifact-packages.ps1 + -InputPath $(Build.SourcesDirectory)\artifacts\PackageArtifacts + -ExtractPath $(Build.SourcesDirectory)\artifacts\PackageArtifacts + displayName: Extract Package Artifacts + continueOnError: ${{ parameters.continueOnError }} + - task: NuGetToolInstaller@1 + displayName: 'Install NuGet.exe' + - task: NuGetCommand@2 + displayName: 'Install Guardian' + inputs: + restoreSolution: $(Build.SourcesDirectory)\eng\common\sdl\packages.config + feedsToUse: config + nugetConfigPath: $(Build.SourcesDirectory)\eng\common\sdl\NuGet.config + externalFeedCredentials: GuardianConnect + restoreDirectory: $(Build.SourcesDirectory)\.packages + - ${{ if ne(parameters.overrideParameters, '') }}: + - powershell: eng/common/sdl/execute-all-sdl-tools.ps1 ${{ parameters.overrideParameters }} + displayName: Execute SDL + continueOnError: ${{ parameters.continueOnError }} + - ${{ if eq(parameters.overrideParameters, '') }}: + - powershell: eng/common/sdl/execute-all-sdl-tools.ps1 + -GuardianPackageName Microsoft.Guardian.Cli.0.7.1 + -NugetPackageDirectory $(Build.SourcesDirectory)\.packages + -AzureDevOpsAccessToken $(dn-bot-dotnet-build-rw-code-rw) + ${{ parameters.additionalParameters }} + displayName: Execute SDL + continueOnError: ${{ parameters.continueOnError }} diff --git a/eng/common/templates/job/generate-graph-files.yml b/eng/common/templates/job/generate-graph-files.yml new file mode 100644 index 000000000..e54ce956f --- /dev/null +++ b/eng/common/templates/job/generate-graph-files.yml @@ -0,0 +1,48 @@ +parameters: + # Optional: dependencies of the job + dependsOn: '' + + # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool + pool: {} + + # Optional: Include toolset dependencies in the generated graph files + includeToolset: false + +jobs: +- job: Generate_Graph_Files + + dependsOn: ${{ parameters.dependsOn }} + + displayName: Generate Graph Files + + pool: ${{ parameters.pool }} + + variables: + # Publish-Build-Assets provides: MaestroAccessToken, BotAccount-dotnet-maestro-bot-PAT + # DotNet-AllOrgs-Darc-Pats provides: dn-bot-devdiv-dnceng-rw-code-pat + - group: Publish-Build-Assets + - group: DotNet-AllOrgs-Darc-Pats + - name: _GraphArguments + value: -gitHubPat $(BotAccount-dotnet-maestro-bot-PAT) + -azdoPat $(dn-bot-devdiv-dnceng-rw-code-pat) + -barToken $(MaestroAccessToken) + -outputFolder '$(Build.StagingDirectory)/GraphFiles/' + - ${{ if ne(parameters.includeToolset, 'false') }}: + - name: _GraphArguments + value: ${{ variables._GraphArguments }} -includeToolset + + steps: + - task: PowerShell@2 + displayName: Generate Graph Files + inputs: + filePath: eng\common\generate-graph-files.ps1 + arguments: $(_GraphArguments) + continueOnError: true + - task: PublishBuildArtifacts@1 + displayName: Publish Graph to Artifacts + inputs: + PathtoPublish: '$(Build.StagingDirectory)/GraphFiles' + PublishLocation: Container + ArtifactName: GraphFiles + continueOnError: true + condition: always() diff --git a/eng/common/templates/job/job.yml b/eng/common/templates/job/job.yml new file mode 100644 index 000000000..8db456bb7 --- /dev/null +++ b/eng/common/templates/job/job.yml @@ -0,0 +1,209 @@ +parameters: +# Job schema parameters - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job + cancelTimeoutInMinutes: '' + + condition: '' + + continueOnError: false + + container: '' + + dependsOn: '' + + displayName: '' + + steps: [] + + pool: '' + + strategy: '' + + timeoutInMinutes: '' + + variables: [] + + workspace: '' + +# Job base template specific parameters + # Optional: Enable installing Microbuild plugin + # if 'true', these "variables" must be specified in the variables object or as part of the queue matrix + # _TeamName - the name of your team + # _SignType - 'test' or 'real' + enableMicrobuild: false + + # Optional: Include PublishBuildArtifacts task + enablePublishBuildArtifacts: false + + # Optional: Enable publishing to the build asset registry + enablePublishBuildAssets: false + + # Optional: Prevent gather/push manifest from executing when using publishing pipelines + enablePublishUsingPipelines: false + + # Optional: Include PublishTestResults task + enablePublishTestResults: false + + # Optional: enable sending telemetry + enableTelemetry: false + + # Optional: define the helix repo for telemetry (example: 'dotnet/arcade') + helixRepo: '' + + # Optional: define the helix type for telemetry (example: 'build/product/') + helixType: '' + + # Required: name of the job + name: '' + + # Optional: should run as a public build even in the internal project + # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. + runAsPublic: false + +# Internal resources (telemetry, microbuild) can only be accessed from non-public projects, +# and some (Microbuild) should only be applied to non-PR cases for internal builds. + +jobs: +- job: ${{ parameters.name }} + + ${{ if ne(parameters.cancelTimeoutInMinutes, '') }}: + cancelTimeoutInMinutes: ${{ parameters.cancelTimeoutInMinutes }} + + ${{ if ne(parameters.condition, '') }}: + condition: ${{ parameters.condition }} + + ${{ if ne(parameters.container, '') }}: + container: ${{ parameters.container }} + + ${{ if ne(parameters.continueOnError, '') }}: + continueOnError: ${{ parameters.continueOnError }} + + ${{ if ne(parameters.dependsOn, '') }}: + dependsOn: ${{ parameters.dependsOn }} + + ${{ if ne(parameters.displayName, '') }}: + displayName: ${{ parameters.displayName }} + + ${{ if ne(parameters.pool, '') }}: + pool: ${{ parameters.pool }} + + ${{ if ne(parameters.strategy, '') }}: + strategy: ${{ parameters.strategy }} + + ${{ if ne(parameters.timeoutInMinutes, '') }}: + timeoutInMinutes: ${{ parameters.timeoutInMinutes }} + + variables: + - ${{ if eq(parameters.enableTelemetry, 'true') }}: + - name: DOTNET_CLI_TELEMETRY_PROFILE + value: '$(Build.Repository.Uri)' + - ${{ each variable in parameters.variables }}: + # handle name-value variable syntax + # example: + # - name: [key] + # value: [value] + - ${{ if ne(variable.name, '') }}: + - name: ${{ variable.name }} + value: ${{ variable.value }} + + # handle variable groups + - ${{ if ne(variable.group, '') }}: + - group: ${{ variable.group }} + + # handle key-value variable syntax. + # example: + # - [key]: [value] + - ${{ if and(eq(variable.name, ''), eq(variable.group, '')) }}: + - ${{ each pair in variable }}: + - name: ${{ pair.key }} + value: ${{ pair.value }} + + # DotNet-HelixApi-Access provides 'HelixApiAccessToken' for internal builds + - ${{ if and(eq(parameters.enableTelemetry, 'true'), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - group: DotNet-HelixApi-Access + + ${{ if ne(parameters.workspace, '') }}: + workspace: ${{ parameters.workspace }} + + steps: + - ${{ if eq(parameters.enableTelemetry, 'true') }}: + # Telemetry tasks are built from https://github.com/dotnet/arcade-extensions + - task: sendStartTelemetry@0 + displayName: 'Send Helix Start Telemetry' + inputs: + helixRepo: ${{ parameters.helixRepo }} + ${{ if ne(parameters.helixType, '') }}: + helixType: ${{ parameters.helixType }} + buildConfig: $(_BuildConfig) + runAsPublic: ${{ parameters.runAsPublic }} + continueOnError: ${{ parameters.continueOnError }} + condition: always() + + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: MicroBuildSigningPlugin@2 + displayName: Install MicroBuild plugin + inputs: + signType: $(_SignType) + zipSources: false + feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + env: + TeamName: $(_TeamName) + continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) + + - ${{ each step in parameters.steps }}: + - ${{ step }} + + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: MicroBuildCleanup@1 + displayName: Execute Microbuild cleanup tasks + condition: and(always(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} + env: + TeamName: $(_TeamName) + + - ${{ if eq(parameters.enableTelemetry, 'true') }}: + # Telemetry tasks are built from https://github.com/dotnet/arcade-extensions + - task: sendEndTelemetry@0 + displayName: 'Send Helix End Telemetry' + continueOnError: ${{ parameters.continueOnError }} + condition: always() + + - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}: + - task: PublishBuildArtifacts@1 + displayName: Publish Logs + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)' + PublishLocation: Container + ArtifactName: $(Agent.Os)_$(Agent.JobName) + continueOnError: true + condition: always() + + - ${{ if eq(parameters.enablePublishTestResults, 'true') }}: + - task: PublishTestResults@2 + displayName: Publish Test Results + inputs: + testResultsFormat: 'xUnit' + testResultsFiles: '*.xml' + searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)' + continueOnError: true + condition: always() + + - ${{ if and(eq(parameters.enablePublishBuildAssets, true), ne(parameters.enablePublishUsingPipelines, 'true'), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: CopyFiles@2 + displayName: Gather Asset Manifests + inputs: + SourceFolder: '$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/AssetManifest' + TargetFolder: '$(Build.StagingDirectory)/AssetManifests' + continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), eq(variables['_DotNetPublishToBlobFeed'], 'true')) + + - task: PublishBuildArtifacts@1 + displayName: Push Asset Manifests + inputs: + PathtoPublish: '$(Build.StagingDirectory)/AssetManifests' + PublishLocation: Container + ArtifactName: AssetManifests + continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), eq(variables['_DotNetPublishToBlobFeed'], 'true')) diff --git a/eng/common/templates/job/performance.yml b/eng/common/templates/job/performance.yml new file mode 100644 index 000000000..ef809253d --- /dev/null +++ b/eng/common/templates/job/performance.yml @@ -0,0 +1,93 @@ +parameters: + steps: [] # optional -- any additional steps that need to happen before pulling down the performance repo and sending the performance benchmarks to helix (ie building your repo) + variables: [] # optional -- list of additional variables to send to the template + jobName: '' # required -- job name + displayName: '' # optional -- display name for the job. Will use jobName if not passed + pool: '' # required -- name of the Build pool + container: '' # required -- name of the container + extraSetupParameters: '' # optional -- extra arguments to pass to the setup script + frameworks: ['netcoreapp3.0'] # optional -- list of frameworks to run against + continueOnError: 'false' # optional -- determines whether to continue the build if the step errors + dependsOn: '' # optional -- dependencies of the job + timeoutInMinutes: 320 # optional -- timeout for the job + enableTelemetry: false # optional -- enable for telemetry + +jobs: +- template: ../jobs/jobs.yml + parameters: + dependsOn: ${{ parameters.dependsOn }} + enableTelemetry: ${{ parameters.enableTelemetry }} + enablePublishBuildArtifacts: true + continueOnError: ${{ parameters.continueOnError }} + + jobs: + - job: '${{ parameters.jobName }}' + + ${{ if ne(parameters.displayName, '') }}: + displayName: '${{ parameters.displayName }}' + ${{ if eq(parameters.displayName, '') }}: + displayName: '${{ parameters.jobName }}' + + timeoutInMinutes: ${{ parameters.timeoutInMinutes }} + + variables: + + - ${{ each variable in parameters.variables }}: + - ${{ if ne(variable.name, '') }}: + - name: ${{ variable.name }} + value: ${{ variable.value }} + - ${{ if ne(variable.group, '') }}: + - group: ${{ variable.group }} + + - IsInternal: '' + - HelixApiAccessToken: '' + - HelixPreCommand: '' + + - ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - ${{ if eq(variables['Agent.Os'], 'Windows_NT') }}: + - HelixPreCommand: 'set "PERFLAB_UPLOAD_TOKEN=$(PerfCommandUploadToken)"' + - IsInternal: -Internal + - ${{ if ne(variables['Agent.Os'], 'Windows_NT') }}: + - HelixPreCommand: 'export PERFLAB_UPLOAD_TOKEN="$(PerfCommandUploadTokenLinux)"' + - IsInternal: --internal + - group: DotNet-HelixApi-Access + - group: dotnet-benchview + + workspace: + clean: all + pool: + ${{ parameters.pool }} + container: ${{ parameters.container }} + strategy: + matrix: + ${{ each framework in parameters.frameworks }}: + ${{ framework }}: + _Framework: ${{ framework }} + steps: + - checkout: self + clean: true + # Run all of the steps to setup repo + - ${{ each step in parameters.steps }}: + - ${{ step }} + - powershell: $(Build.SourcesDirectory)\eng\common\performance\performance-setup.ps1 $(IsInternal) -Framework $(_Framework) ${{ parameters.extraSetupParameters }} + displayName: Performance Setup (Windows) + condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} + - script: $(Build.SourcesDirectory)/eng/common/performance/performance-setup.sh $(IsInternal) --framework $(_Framework) ${{ parameters.extraSetupParameters }} + displayName: Performance Setup (Unix) + condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} + - script: $(Python) $(PerformanceDirectory)/scripts/ci_setup.py $(SetupArguments) + displayName: Run ci setup script + # Run perf testing in helix + - template: /eng/common/templates/steps/perf-send-to-helix.yml + parameters: + HelixSource: '$(HelixSourcePrefix)/$(Build.Repository.Name)/$(Build.SourceBranch)' # sources must start with pr/, official/, prodcon/, or agent/ + HelixType: 'test/performance/$(Kind)/$(_Framework)/$(Architecture)' + HelixAccessToken: $(HelixApiAccessToken) + HelixTargetQueues: $(Queue) + HelixPreCommands: $(HelixPreCommand) + Creator: $(Creator) + WorkItemTimeout: 4:00 # 4 hours + WorkItemDirectory: '$(WorkItemDirectory)' # WorkItemDirectory can not be empty, so we send it some docs to keep it happy + CorrelationPayloadDirectory: '$(PayloadDirectory)' # it gets checked out to a folder with shorter path than WorkItemDirectory so we can avoid file name too long exceptions \ No newline at end of file diff --git a/eng/common/templates/job/publish-build-assets.yml b/eng/common/templates/job/publish-build-assets.yml new file mode 100644 index 000000000..9e77ef1b5 --- /dev/null +++ b/eng/common/templates/job/publish-build-assets.yml @@ -0,0 +1,84 @@ +parameters: + configuration: 'Debug' + + # Optional: condition for the job to run + condition: '' + + # Optional: 'true' if future jobs should run even if this job fails + continueOnError: false + + # Optional: dependencies of the job + dependsOn: '' + + # Optional: Include PublishBuildArtifacts task + enablePublishBuildArtifacts: false + + # Optional: A defined YAML pool - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#pool + pool: {} + + # Optional: should run as a public build even in the internal project + # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. + runAsPublic: false + + # Optional: whether the build's artifacts will be published using release pipelines or direct feed publishing + publishUsingPipelines: false + +jobs: +- job: Asset_Registry_Publish + + dependsOn: ${{ parameters.dependsOn }} + + displayName: Publish to Build Asset Registry + + pool: ${{ parameters.pool }} + + variables: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - name: _BuildConfig + value: ${{ parameters.configuration }} + - group: Publish-Build-Assets + + steps: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: DownloadBuildArtifacts@0 + displayName: Download artifact + inputs: + artifactName: AssetManifests + downloadPath: '$(Build.StagingDirectory)/Download' + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + - task: PowerShell@2 + displayName: Publish Build Assets + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishBuildAssets -restore -msbuildEngine dotnet + /p:ManifestsPath='$(Build.StagingDirectory)/Download/AssetManifests' + /p:BuildAssetRegistryToken=$(MaestroAccessToken) + /p:MaestroApiEndpoint=https://maestro-prod.westus2.cloudapp.azure.com + /p:PublishUsingPipelines=${{ parameters.publishUsingPipelines }} + /p:Configuration=$(_BuildConfig) + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + - task: powershell@2 + displayName: Create ReleaseConfigs Artifact + inputs: + targetType: inline + script: | + Add-Content -Path "$(Build.StagingDirectory)/ReleaseConfigs.txt" -Value $(BARBuildId) + Add-Content -Path "$(Build.StagingDirectory)/ReleaseConfigs.txt" -Value "$(DefaultChannels)" + Add-Content -Path "$(Build.StagingDirectory)/ReleaseConfigs.txt" -Value $(IsStableBuild) + - task: PublishBuildArtifacts@1 + displayName: Publish ReleaseConfigs Artifact + inputs: + PathtoPublish: '$(Build.StagingDirectory)/ReleaseConfigs.txt' + PublishLocation: Container + ArtifactName: ReleaseConfigs + - ${{ if eq(parameters.enablePublishBuildArtifacts, 'true') }}: + - task: PublishBuildArtifacts@1 + displayName: Publish Logs to VSTS + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)' + PublishLocation: Container + ArtifactName: $(Agent.Os)_PublishBuildAssets + continueOnError: true + condition: always() diff --git a/eng/common/templates/jobs/jobs.yml b/eng/common/templates/jobs/jobs.yml new file mode 100644 index 000000000..6a2f98c03 --- /dev/null +++ b/eng/common/templates/jobs/jobs.yml @@ -0,0 +1,90 @@ +parameters: + # Optional: 'true' if failures in job.yml job should not fail the job + continueOnError: false + + # Optional: Enable installing Microbuild plugin + # if 'true', these "variables" must be specified in the variables object or as part of the queue matrix + # _TeamName - the name of your team + # _SignType - 'test' or 'real' + enableMicrobuild: false + + # Optional: Include PublishBuildArtifacts task + enablePublishBuildArtifacts: false + + # Optional: Enable publishing to the build asset registry + enablePublishBuildAssets: false + + # Optional: Enable publishing using release pipelines + enablePublishUsingPipelines: false + + graphFileGeneration: + # Optional: Enable generating the graph files at the end of the build + enabled: false + # Optional: Include toolset dependencies in the generated graph files + includeToolset: false + + # Optional: Include PublishTestResults task + enablePublishTestResults: false + + # Optional: enable sending telemetry + # if enabled then the 'helixRepo' parameter should also be specified + enableTelemetry: false + + # Required: A collection of jobs to run - https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=vsts&tabs=schema#job + jobs: [] + + # Optional: define the helix repo for telemetry (example: 'dotnet/arcade') + helixRepo: '' + + # Optional: Override automatically derived dependsOn value for "publish build assets" job + publishBuildAssetsDependsOn: '' + + # Optional: should run as a public build even in the internal project + # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. + runAsPublic: false + +# Internal resources (telemetry, microbuild) can only be accessed from non-public projects, +# and some (Microbuild) should only be applied to non-PR cases for internal builds. + +jobs: +- ${{ each job in parameters.jobs }}: + - template: ../job/job.yml + parameters: + # pass along parameters + ${{ each parameter in parameters }}: + ${{ if ne(parameter.key, 'jobs') }}: + ${{ parameter.key }}: ${{ parameter.value }} + + # pass along job properties + ${{ each property in job }}: + ${{ if ne(property.key, 'job') }}: + ${{ property.key }}: ${{ property.value }} + + name: ${{ job.job }} + +- ${{ if and(eq(parameters.enablePublishBuildAssets, true), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - template: ../job/publish-build-assets.yml + parameters: + continueOnError: ${{ parameters.continueOnError }} + dependsOn: + - ${{ if ne(parameters.publishBuildAssetsDependsOn, '') }}: + - ${{ each job in parameters.publishBuildAssetsDependsOn }}: + - ${{ job.job }} + - ${{ if eq(parameters.publishBuildAssetsDependsOn, '') }}: + - ${{ each job in parameters.jobs }}: + - ${{ job.job }} + pool: + vmImage: vs2017-win2016 + runAsPublic: ${{ parameters.runAsPublic }} + publishUsingPipelines: ${{ parameters.enablePublishUsingPipelines }} + enablePublishBuildArtifacts: ${{ parameters.enablePublishBuildArtifacts }} + +- ${{ if and(eq(parameters.graphFileGeneration.enabled, true), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - template: ../job/generate-graph-files.yml + parameters: + continueOnError: ${{ parameters.continueOnError }} + includeToolset: ${{ parameters.graphFileGeneration.includeToolset }} + dependsOn: + - Asset_Registry_Publish + pool: + vmImage: vs2017-win2016 diff --git a/eng/common/templates/phases/base.yml b/eng/common/templates/phases/base.yml new file mode 100644 index 000000000..0123cf43b --- /dev/null +++ b/eng/common/templates/phases/base.yml @@ -0,0 +1,130 @@ +parameters: + # Optional: Clean sources before building + clean: true + + # Optional: Git fetch depth + fetchDepth: '' + + # Optional: name of the phase (not specifying phase name may cause name collisions) + name: '' + # Optional: display name of the phase + displayName: '' + + # Optional: condition for the job to run + condition: '' + + # Optional: dependencies of the phase + dependsOn: '' + + # Required: A defined YAML queue + queue: {} + + # Required: build steps + steps: [] + + # Optional: variables + variables: {} + + # Optional: should run as a public build even in the internal project + # if 'true', the build won't run any of the internal only steps, even if it is running in non-public projects. + runAsPublic: false + + ## Telemetry variables + + # Optional: enable sending telemetry + # if 'true', these "variables" must be specified in the variables object or as part of the queue matrix + # _HelixBuildConfig - differentiate between Debug, Release, other + # _HelixSource - Example: build/product + # _HelixType - Example: official/dotnet/arcade/$(Build.SourceBranch) + enableTelemetry: false + + # Optional: Enable installing Microbuild plugin + # if 'true', these "variables" must be specified in the variables object or as part of the queue matrix + # _TeamName - the name of your team + # _SignType - 'test' or 'real' + enableMicrobuild: false + +# Internal resources (telemetry, microbuild) can only be accessed from non-public projects, +# and some (Microbuild) should only be applied to non-PR cases for internal builds. + +phases: +- phase: ${{ parameters.name }} + + ${{ if ne(parameters.displayName, '') }}: + displayName: ${{ parameters.displayName }} + + ${{ if ne(parameters.condition, '') }}: + condition: ${{ parameters.condition }} + + ${{ if ne(parameters.dependsOn, '') }}: + dependsOn: ${{ parameters.dependsOn }} + + queue: ${{ parameters.queue }} + + ${{ if ne(parameters.variables, '') }}: + variables: + ${{ insert }}: ${{ parameters.variables }} + + steps: + - checkout: self + clean: ${{ parameters.clean }} + ${{ if ne(parameters.fetchDepth, '') }}: + fetchDepth: ${{ parameters.fetchDepth }} + + - ${{ if eq(parameters.enableTelemetry, 'true') }}: + - template: /eng/common/templates/steps/telemetry-start.yml + parameters: + buildConfig: $(_HelixBuildConfig) + helixSource: $(_HelixSource) + helixType: $(_HelixType) + runAsPublic: ${{ parameters.runAsPublic }} + + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + # Internal only resource, and Microbuild signing shouldn't be applied to PRs. + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: MicroBuildSigningPlugin@2 + displayName: Install MicroBuild plugin + inputs: + signType: $(_SignType) + zipSources: false + feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + + env: + TeamName: $(_TeamName) + continueOnError: false + condition: and(succeeded(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) + + # Run provided build steps + - ${{ parameters.steps }} + + - ${{ if eq(parameters.enableMicrobuild, 'true') }}: + # Internal only resources + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: MicroBuildCleanup@1 + displayName: Execute Microbuild cleanup tasks + condition: and(always(), in(variables['_SignType'], 'real', 'test'), eq(variables['Agent.Os'], 'Windows_NT')) + env: + TeamName: $(_TeamName) + + - ${{ if eq(parameters.enableTelemetry, 'true') }}: + - template: /eng/common/templates/steps/telemetry-end.yml + parameters: + helixSource: $(_HelixSource) + helixType: $(_HelixType) + + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: CopyFiles@2 + displayName: Gather Asset Manifests + inputs: + SourceFolder: '$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/AssetManifest' + TargetFolder: '$(Build.StagingDirectory)/AssetManifests' + continueOnError: false + condition: and(succeeded(), eq(variables['_DotNetPublishToBlobFeed'], 'true')) + - task: PublishBuildArtifacts@1 + displayName: Push Asset Manifests + inputs: + PathtoPublish: '$(Build.StagingDirectory)/AssetManifests' + PublishLocation: Container + ArtifactName: AssetManifests + continueOnError: false + condition: and(succeeded(), eq(variables['_DotNetPublishToBlobFeed'], 'true')) diff --git a/eng/common/templates/phases/publish-build-assets.yml b/eng/common/templates/phases/publish-build-assets.yml new file mode 100644 index 000000000..a0a807428 --- /dev/null +++ b/eng/common/templates/phases/publish-build-assets.yml @@ -0,0 +1,51 @@ +parameters: + dependsOn: '' + queue: {} + configuration: 'Debug' + condition: succeeded() + continueOnError: false + runAsPublic: false + publishUsingPipelines: false +phases: + - phase: Asset_Registry_Publish + displayName: Publish to Build Asset Registry + dependsOn: ${{ parameters.dependsOn }} + queue: ${{ parameters.queue }} + variables: + _BuildConfig: ${{ parameters.configuration }} + steps: + - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - task: DownloadBuildArtifacts@0 + displayName: Download artifact + inputs: + artifactName: AssetManifests + downloadPath: '$(Build.StagingDirectory)/Download' + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + - task: AzureKeyVault@1 + inputs: + azureSubscription: 'DotNet-Engineering-Services_KeyVault' + KeyVaultName: EngKeyVault + SecretsFilter: 'MaestroAccessToken' + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + - task: PowerShell@2 + displayName: Publish Build Assets + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishBuildAssets -restore -msbuildEngine dotnet + /p:ManifestsPath='$(Build.StagingDirectory)/Download/AssetManifests' + /p:BuildAssetRegistryToken=$(MaestroAccessToken) + /p:MaestroApiEndpoint=https://maestro-prod.westus2.cloudapp.azure.com + /p:PublishUsingPipelines=${{ parameters.publishUsingPipelines }} + /p:Configuration=$(_BuildConfig) + condition: ${{ parameters.condition }} + continueOnError: ${{ parameters.continueOnError }} + - task: PublishBuildArtifacts@1 + displayName: Publish Logs to VSTS + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)' + PublishLocation: Container + ArtifactName: $(Agent.Os)_Asset_Registry_Publish + continueOnError: true + condition: always() diff --git a/eng/common/templates/post-build/channels/internal-servicing.yml b/eng/common/templates/post-build/channels/internal-servicing.yml new file mode 100644 index 000000000..dc065ab30 --- /dev/null +++ b/eng/common/templates/post-build/channels/internal-servicing.yml @@ -0,0 +1,147 @@ +parameters: + enableSymbolValidation: true + +stages: +- stage: IS_Publish + dependsOn: validate + variables: + - template: ../common-variables.yml + displayName: Internal Servicing + jobs: + - template: ../setup-maestro-vars.yml + + - job: + displayName: Symbol Publishing + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.InternalServicing_30_Channel_Id)) + variables: + - group: DotNet-Symbol-Server-Pats + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Artifacts + inputs: + downloadType: specific files + matchingPattern: "*Artifacts*" + + - task: PowerShell@2 + displayName: Publish + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishToSymbolServers -restore -msbuildEngine dotnet + /p:DotNetSymbolServerTokenMsdl=$(microsoft-symbol-server-pat) + /p:DotNetSymbolServerTokenSymWeb=$(symweb-symbol-server-pat) + /p:PDBArtifactsDirectory='$(Build.ArtifactStagingDirectory)/PDBArtifacts/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:Configuration=Release + + - job: publish_assets + displayName: Publish Assets + dependsOn: setupMaestroVars + variables: + - group: DotNet-Blob-Feed + - group: AzureDevOps-Artifact-Feeds-Pats + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + - name: IsStableBuild + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.InternalServicing_30_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: current + artifactName: BlobArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Asset Manifests + inputs: + buildType: current + artifactName: AssetManifests + + - task: PowerShell@2 + displayName: Add Assets Location + env: + AZURE_DEVOPS_EXT_PAT: $(dn-bot-dnceng-unviersal-packages-rw) + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet + /p:ChannelId=$(InternalServicing_30_Channel_Id) + /p:IsStableBuild=$(IsStableBuild) + /p:IsInternalBuild=$(IsInternalBuild) + /p:RepositoryName=$(Build.Repository.Name) + /p:CommitSha=$(Build.SourceVersion) + /p:AzureStorageAccountName=$(ProxyBackedFeedsAccountName) + /p:AzureStorageAccountKey=$(dotnetfeed-storage-access-key-1) + /p:AzureDevOpsFeedsBaseUrl=$(dotnetfeed-internal-private-feed-url) + /p:StaticInternalFeed=$(dotnetfeed-internal-nonstable-feed-url) + /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe + /p:BARBuildId=$(BARBuildId) + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' + /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)\BlobArtifacts' + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)\PackageArtifacts' + /p:Configuration=Release + + - task: NuGetCommand@2 + displayName: Publish Packages to AzDO Feed + condition: contains(variables['TargetAzDOFeed'], 'pkgs.visualstudio.com') + inputs: + command: push + vstsFeed: $(AzDoFeedName) + packagesToPush: $(Build.ArtifactStagingDirectory)\PackageArtifacts\*.nupkg + publishVstsFeed: $(AzDoFeedName) + + - task: PowerShell@2 + displayName: Publish Blobs to AzDO Feed + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-blobs-to-azdo.ps1 + arguments: -FeedName $(AzDoFeedName) + -SourceFolderCollection $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -PersonalAccessToken $(dn-bot-dnceng-unviersal-packages-rw) + enabled: false + + - template: ../trigger-subscription.yml + parameters: + ChannelId: ${{ variables.InternalServicing_30_Channel_Id }} + +- stage: IS_PublishValidation + displayName: Publish Validation + variables: + - template: ../common-variables.yml + jobs: + - template: ../setup-maestro-vars.yml + + - ${{ if eq(parameters.enableSymbolValidation, 'true') }}: + - job: + displayName: Symbol Availability + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.InternalServicing_30_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: PowerShell@2 + displayName: Check Symbol Availability + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) + + - template: ../promote-build.yml + parameters: + ChannelId: ${{ variables.InternalServicing_30_Channel_Id }} diff --git a/eng/common/templates/post-build/channels/netcore-dev-5.yml b/eng/common/templates/post-build/channels/netcore-dev-5.yml new file mode 100644 index 000000000..f2b0cfb26 --- /dev/null +++ b/eng/common/templates/post-build/channels/netcore-dev-5.yml @@ -0,0 +1,148 @@ +parameters: + enableSymbolValidation: true + +stages: +- stage: NetCore_Dev5_Publish + dependsOn: validate + variables: + - template: ../common-variables.yml + displayName: .NET Core 5 Dev Channel + jobs: + - template: ../setup-maestro-vars.yml + + - job: + displayName: Symbol Publishing + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_5_Dev_Channel_Id)) + variables: + - group: DotNet-Symbol-Server-Pats + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Artifacts + inputs: + downloadType: specific files + matchingPattern: "*Artifacts*" + + - task: PowerShell@2 + displayName: Publish + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishToSymbolServers -restore -msbuildEngine dotnet + /p:DotNetSymbolServerTokenMsdl=$(microsoft-symbol-server-pat) + /p:DotNetSymbolServerTokenSymWeb=$(symweb-symbol-server-pat) + /p:PDBArtifactsDirectory='$(Build.ArtifactStagingDirectory)/PDBArtifacts/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:Configuration=Release + + - job: + displayName: Publish Assets + dependsOn: setupMaestroVars + variables: + - group: DotNet-Blob-Feed + - group: AzureDevOps-Artifact-Feeds-Pats + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + - name: IsStableBuild + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_5_Dev_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: current + artifactName: BlobArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Asset Manifests + inputs: + buildType: current + artifactName: AssetManifests + + - task: PowerShell@2 + displayName: Add Assets Location + env: + AZURE_DEVOPS_EXT_PAT: $(dn-bot-dnceng-unviersal-packages-rw) + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet + /p:ChannelId=$(NetCore_5_Dev_Channel_Id) + /p:ArtifactsCategory=$(_DotNetArtifactsCategory) + /p:IsStableBuild=$(IsStableBuild) + /p:IsInternalBuild=$(IsInternalBuild) + /p:RepositoryName=$(Build.Repository.Name) + /p:CommitSha=$(Build.SourceVersion) + /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe + /p:AzdoTargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:TargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:AzureStorageTargetFeedPAT='$(dotnetfeed-storage-access-key-1)' + /p:BARBuildId=$(BARBuildId) + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' + /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts/' + /p:Configuration=Release + + - task: NuGetCommand@2 + displayName: Publish Packages to AzDO Feed + condition: contains(variables['TargetAzDOFeed'], 'pkgs.visualstudio.com') + inputs: + command: push + vstsFeed: $(AzDoFeedName) + packagesToPush: $(Build.ArtifactStagingDirectory)\PackageArtifacts\*.nupkg + publishVstsFeed: $(AzDoFeedName) + + - task: PowerShell@2 + displayName: Publish Blobs to AzDO Feed + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-blobs-to-azdo.ps1 + arguments: -FeedName $(AzDoFeedName) + -SourceFolderCollection $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -PersonalAccessToken $(dn-bot-dnceng-unviersal-packages-rw) + enabled: false + + +- stage: NetCore_Dev5_PublishValidation + displayName: Publish Validation + variables: + - template: ../common-variables.yml + jobs: + - template: ../setup-maestro-vars.yml + + - ${{ if eq(parameters.enableSymbolValidation, 'true') }}: + - job: + displayName: Symbol Availability + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_5_Dev_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: PowerShell@2 + displayName: Check Symbol Availability + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) + + - template: ../darc-gather-drop.yml + parameters: + ChannelId: ${{ variables.NetCore_5_Dev_Channel_Id }} + + - template: ../promote-build.yml + parameters: + ChannelId: ${{ variables.NetCore_5_Dev_Channel_Id }} diff --git a/eng/common/templates/post-build/channels/netcore-tools-latest.yml b/eng/common/templates/post-build/channels/netcore-tools-latest.yml new file mode 100644 index 000000000..fd6c09b22 --- /dev/null +++ b/eng/common/templates/post-build/channels/netcore-tools-latest.yml @@ -0,0 +1,148 @@ +parameters: + enableSymbolValidation: true + +stages: +- stage: NetCore_Tools_Latest_Publish + dependsOn: validate + variables: + - template: ../common-variables.yml + displayName: .NET Tools - Latest + jobs: + - template: ../setup-maestro-vars.yml + + - job: + displayName: Symbol Publishing + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_Tools_Latest_Channel_Id)) + variables: + - group: DotNet-Symbol-Server-Pats + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Artifacts + inputs: + downloadType: specific files + matchingPattern: "*Artifacts*" + + - task: PowerShell@2 + displayName: Publish + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishToSymbolServers -restore -msbuildEngine dotnet + /p:DotNetSymbolServerTokenMsdl=$(microsoft-symbol-server-pat) + /p:DotNetSymbolServerTokenSymWeb=$(symweb-symbol-server-pat) + /p:PDBArtifactsDirectory='$(Build.ArtifactStagingDirectory)/PDBArtifacts/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:Configuration=Release + + - job: + displayName: Publish Assets + dependsOn: setupMaestroVars + variables: + - group: DotNet-Blob-Feed + - group: AzureDevOps-Artifact-Feeds-Pats + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + - name: IsStableBuild + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_Tools_Latest_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: current + artifactName: BlobArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Asset Manifests + inputs: + buildType: current + artifactName: AssetManifests + + - task: PowerShell@2 + displayName: Add Assets Location + env: + AZURE_DEVOPS_EXT_PAT: $(dn-bot-dnceng-unviersal-packages-rw) + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet + /p:ChannelId=$(NetCore_Tools_Latest_Channel_Id) + /p:ArtifactsCategory=$(_DotNetArtifactsCategory) + /p:IsStableBuild=$(IsStableBuild) + /p:IsInternalBuild=$(IsInternalBuild) + /p:RepositoryName=$(Build.Repository.Name) + /p:CommitSha=$(Build.SourceVersion) + /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe + /p:AzdoTargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:TargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:AzureStorageTargetFeedPAT='$(dotnetfeed-storage-access-key-1)' + /p:BARBuildId=$(BARBuildId) + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' + /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts/' + /p:Configuration=Release + + - task: NuGetCommand@2 + displayName: Publish Packages to AzDO Feed + condition: contains(variables['TargetAzDOFeed'], 'pkgs.visualstudio.com') + inputs: + command: push + vstsFeed: $(AzDoFeedName) + packagesToPush: $(Build.ArtifactStagingDirectory)\PackageArtifacts\*.nupkg + publishVstsFeed: $(AzDoFeedName) + + - task: PowerShell@2 + displayName: Publish Blobs to AzDO Feed + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-blobs-to-azdo.ps1 + arguments: -FeedName $(AzDoFeedName) + -SourceFolderCollection $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -PersonalAccessToken $(dn-bot-dnceng-unviersal-packages-rw) + enabled: false + + +- stage: NetCore_Tools_Latest_PublishValidation + displayName: Publish Validation + variables: + - template: ../common-variables.yml + jobs: + - template: ../setup-maestro-vars.yml + + - ${{ if eq(parameters.enableSymbolValidation, 'true') }}: + - job: + displayName: Symbol Availability + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_Tools_Latest_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: PowerShell@2 + displayName: Check Symbol Availability + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) + + - template: ../darc-gather-drop.yml + parameters: + ChannelId: ${{ variables.NetCore_Tools_Latest_Channel_Id }} + + - template: ../promote-build.yml + parameters: + ChannelId: ${{ variables.NetCore_Tools_Latest_Channel_Id }} diff --git a/eng/common/templates/post-build/channels/public-dev-release.yml b/eng/common/templates/post-build/channels/public-dev-release.yml new file mode 100644 index 000000000..771dcf4ef --- /dev/null +++ b/eng/common/templates/post-build/channels/public-dev-release.yml @@ -0,0 +1,148 @@ +parameters: + enableSymbolValidation: true + +stages: +- stage: Publish + dependsOn: validate + variables: + - template: ../common-variables.yml + displayName: Developer Channel + jobs: + - template: ../setup-maestro-vars.yml + + - job: + displayName: Symbol Publishing + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicDevRelease_30_Channel_Id)) + variables: + - group: DotNet-Symbol-Server-Pats + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Artifacts + inputs: + downloadType: specific files + matchingPattern: "*Artifacts*" + + - task: PowerShell@2 + displayName: Publish + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishToSymbolServers -restore -msbuildEngine dotnet + /p:DotNetSymbolServerTokenMsdl=$(microsoft-symbol-server-pat) + /p:DotNetSymbolServerTokenSymWeb=$(symweb-symbol-server-pat) + /p:PDBArtifactsDirectory='$(Build.ArtifactStagingDirectory)/PDBArtifacts/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:Configuration=Release + + - job: + displayName: Publish Assets + dependsOn: setupMaestroVars + variables: + - group: DotNet-Blob-Feed + - group: AzureDevOps-Artifact-Feeds-Pats + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + - name: IsStableBuild + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicDevRelease_30_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: current + artifactName: BlobArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Asset Manifests + inputs: + buildType: current + artifactName: AssetManifests + + - task: PowerShell@2 + displayName: Add Assets Location + env: + AZURE_DEVOPS_EXT_PAT: $(dn-bot-dnceng-unviersal-packages-rw) + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet + /p:ChannelId=$(PublicDevRelease_30_Channel_Id) + /p:ArtifactsCategory=$(_DotNetArtifactsCategory) + /p:IsStableBuild=$(IsStableBuild) + /p:IsInternalBuild=$(IsInternalBuild) + /p:RepositoryName=$(Build.Repository.Name) + /p:CommitSha=$(Build.SourceVersion) + /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe + /p:AzdoTargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:TargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:AzureStorageTargetFeedPAT='$(dotnetfeed-storage-access-key-1)' + /p:BARBuildId=$(BARBuildId) + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' + /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts/' + /p:Configuration=Release + + - task: NuGetCommand@2 + displayName: Publish Packages to AzDO Feed + condition: contains(variables['TargetAzDOFeed'], 'pkgs.visualstudio.com') + inputs: + command: push + vstsFeed: $(AzDoFeedName) + packagesToPush: $(Build.ArtifactStagingDirectory)\PackageArtifacts\*.nupkg + publishVstsFeed: $(AzDoFeedName) + + - task: PowerShell@2 + displayName: Publish Blobs to AzDO Feed + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-blobs-to-azdo.ps1 + arguments: -FeedName $(AzDoFeedName) + -SourceFolderCollection $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -PersonalAccessToken $(dn-bot-dnceng-unviersal-packages-rw) + enabled: false + + +- stage: PublishValidation + displayName: Publish Validation + variables: + - template: ../common-variables.yml + jobs: + - template: ../setup-maestro-vars.yml + + - ${{ if eq(parameters.enableSymbolValidation, 'true') }}: + - job: + displayName: Symbol Availability + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicDevRelease_30_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: PowerShell@2 + displayName: Check Symbol Availability + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) + + - template: ../darc-gather-drop.yml + parameters: + ChannelId: ${{ variables.PublicDevRelease_30_Channel_Id }} + + - template: ../promote-build.yml + parameters: + ChannelId: ${{ variables.PublicDevRelease_30_Channel_Id }} diff --git a/eng/common/templates/post-build/channels/public-release.yml b/eng/common/templates/post-build/channels/public-release.yml new file mode 100644 index 000000000..00108bd3f --- /dev/null +++ b/eng/common/templates/post-build/channels/public-release.yml @@ -0,0 +1,147 @@ +parameters: + enableSymbolValidation: true + +stages: +- stage: PubRel_Publish + dependsOn: validate + variables: + - template: ../common-variables.yml + displayName: Public Release + jobs: + - template: ../setup-maestro-vars.yml + + - job: + displayName: Symbol Publishing + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicRelease_30_Channel_Id)) + variables: + - group: DotNet-Symbol-Server-Pats + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Artifacts + inputs: + downloadType: specific files + matchingPattern: "*Artifacts*" + + - task: PowerShell@2 + displayName: Publish + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishToSymbolServers -restore -msbuildEngine dotnet + /p:DotNetSymbolServerTokenMsdl=$(microsoft-symbol-server-pat) + /p:DotNetSymbolServerTokenSymWeb=$(symweb-symbol-server-pat) + /p:PDBArtifactsDirectory='$(Build.ArtifactStagingDirectory)/PDBArtifacts/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:Configuration=Release + + - job: publish_assets + displayName: Publish Assets + dependsOn: setupMaestroVars + variables: + - group: DotNet-Blob-Feed + - group: AzureDevOps-Artifact-Feeds-Pats + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + - name: IsStableBuild + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicRelease_30_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: current + artifactName: BlobArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Asset Manifests + inputs: + buildType: current + artifactName: AssetManifests + + - task: PowerShell@2 + displayName: Publish + env: + AZURE_DEVOPS_EXT_PAT: $(dn-bot-dnceng-unviersal-packages-rw) + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet + /p:ChannelId=$(PublicRelease_30_Channel_Id) + /p:IsStableBuild=$(IsStableBuild) + /p:IsInternalBuild=$(IsInternalBuild) + /p:RepositoryName=$(Build.Repository.Name) + /p:CommitSha=$(Build.SourceVersion) + /p:AzureStorageAccountName=$(ProxyBackedFeedsAccountName) + /p:AzureStorageAccountKey=$(dotnetfeed-storage-access-key-1) + /p:AzureDevOpsFeedsBaseUrl=$(dotnetfeed-internal-private-feed-url) + /p:StaticInternalFeed=$(dotnetfeed-internal-nonstable-feed-url) + /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe + /p:BARBuildId=$(BARBuildId) + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' + /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)\BlobArtifacts' + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)\PackageArtifacts' + /p:Configuration=Release + + - task: NuGetCommand@2 + displayName: Publish Packages to AzDO Feed + condition: contains(variables['TargetAzDOFeed'], 'pkgs.visualstudio.com') + inputs: + command: push + vstsFeed: $(AzDoFeedName) + packagesToPush: $(Build.ArtifactStagingDirectory)\PackageArtifacts\*.nupkg + publishVstsFeed: $(AzDoFeedName) + + - task: PowerShell@2 + displayName: Publish Blobs to AzDO Feed + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-blobs-to-azdo.ps1 + arguments: -FeedName $(AzDoFeedName) + -SourceFolderCollection $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -PersonalAccessToken $(dn-bot-dnceng-unviersal-packages-rw) + enabled: false + + - template: ../trigger-subscription.yml + parameters: + ChannelId: ${{ variables.PublicRelease_30_Channel_Id }} + +- stage: PubRel_PublishValidation + displayName: Publish Validation + variables: + - template: ../common-variables.yml + jobs: + - template: ../setup-maestro-vars.yml + + - ${{ if eq(parameters.enableSymbolValidation, 'true') }}: + - job: + displayName: Symbol Availability + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicRelease_30_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: PowerShell@2 + displayName: Check Symbol Availability + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) + + - template: ../promote-build.yml + parameters: + ChannelId: ${{ variables.PublicRelease_30_Channel_Id }} diff --git a/eng/common/templates/post-build/channels/public-validation-release.yml b/eng/common/templates/post-build/channels/public-validation-release.yml new file mode 100644 index 000000000..f64184da9 --- /dev/null +++ b/eng/common/templates/post-build/channels/public-validation-release.yml @@ -0,0 +1,99 @@ +stages: +- stage: PVR_Publish + dependsOn: validate + variables: + - template: ../common-variables.yml + displayName: Validation Channel + jobs: + - template: ../setup-maestro-vars.yml + + - job: + displayName: Publish Assets + dependsOn: setupMaestroVars + variables: + - group: DotNet-Blob-Feed + - group: AzureDevOps-Artifact-Feeds-Pats + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + - name: IsStableBuild + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicValidationRelease_30_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: current + artifactName: BlobArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Asset Manifests + inputs: + buildType: current + artifactName: AssetManifests + + - task: PowerShell@2 + displayName: Add Assets Location + env: + AZURE_DEVOPS_EXT_PAT: $(dn-bot-dnceng-unviersal-packages-rw) + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet + /p:ChannelId=$(PublicValidationRelease_30_Channel_Id) + /p:ArtifactsCategory=$(_DotNetValidationArtifactsCategory) + /p:IsStableBuild=$(IsStableBuild) + /p:IsInternalBuild=$(IsInternalBuild) + /p:RepositoryName=$(Build.Repository.Name) + /p:CommitSha=$(Build.SourceVersion) + /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe + /p:AzdoTargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:TargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:AzureStorageTargetFeedPAT='$(dotnetfeed-storage-access-key-1)' + /p:BARBuildId=$(BARBuildId) + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' + /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)\BlobArtifacts' + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)\PackageArtifacts' + /p:Configuration=Release + + - task: NuGetCommand@2 + displayName: Publish Packages to AzDO Feed + condition: contains(variables['TargetAzDOFeed'], 'pkgs.visualstudio.com') + inputs: + command: push + vstsFeed: $(AzDoFeedName) + packagesToPush: $(Build.ArtifactStagingDirectory)\PackageArtifacts\*.nupkg + publishVstsFeed: $(AzDoFeedName) + + - task: PowerShell@2 + displayName: Publish Blobs to AzDO Feed + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-blobs-to-azdo.ps1 + arguments: -FeedName $(AzDoFeedName) + -SourceFolderCollection $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -PersonalAccessToken $(dn-bot-dnceng-unviersal-packages-rw) + enabled: false + + +- stage: PVR_PublishValidation + displayName: Publish Validation + variables: + - template: ../common-variables.yml + jobs: + - template: ../setup-maestro-vars.yml + + - template: ../darc-gather-drop.yml + parameters: + ChannelId: ${{ variables.PublicValidationRelease_30_Channel_Id }} + + - template: ../promote-build.yml + parameters: + ChannelId: ${{ variables.PublicValidationRelease_30_Channel_Id }} diff --git a/eng/common/templates/post-build/common-variables.yml b/eng/common/templates/post-build/common-variables.yml new file mode 100644 index 000000000..52a74487f --- /dev/null +++ b/eng/common/templates/post-build/common-variables.yml @@ -0,0 +1,47 @@ +variables: + - group: Publish-Build-Assets + + # .NET Core 3 Dev + - name: PublicDevRelease_30_Channel_Id + value: 3 + + # .NET Core 5 Dev + - name: NetCore_5_Dev_Channel_Id + value: 131 + + # .NET Tools - Validation + - name: PublicValidationRelease_30_Channel_Id + value: 9 + + # .NET Tools - Latest + - name: NetCore_Tools_Latest_Channel_Id + value: 2 + + # .NET Core 3.0 Internal Servicing + - name: InternalServicing_30_Channel_Id + value: 184 + + # .NET Core 3.0 Release + - name: PublicRelease_30_Channel_Id + value: 19 + + # Whether the build is internal or not + - name: IsInternalBuild + value: ${{ and(ne(variables['System.TeamProject'], 'public'), contains(variables['Build.SourceBranch'], 'internal')) }} + + # Storage account name for proxy-backed feeds + - name: ProxyBackedFeedsAccountName + value: dotnetfeed + + # Default Maestro++ API Endpoint and API Version + - name: MaestroApiEndPoint + value: "https://maestro-prod.westus2.cloudapp.azure.com" + - name: MaestroApiAccessToken + value: $(MaestroAccessToken) + - name: MaestroApiVersion + value: "2019-01-16" + + - name: SourceLinkCLIVersion + value: 3.0.0 + - name: SymbolToolVersion + value: 1.0.1 diff --git a/eng/common/templates/post-build/darc-gather-drop.yml b/eng/common/templates/post-build/darc-gather-drop.yml new file mode 100644 index 000000000..3268ccaa5 --- /dev/null +++ b/eng/common/templates/post-build/darc-gather-drop.yml @@ -0,0 +1,23 @@ +parameters: + ChannelId: 0 + +jobs: +- job: gatherDrop + displayName: Gather Drop + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', ${{ parameters.ChannelId }})) + variables: + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + pool: + vmImage: 'windows-2019' + steps: + - task: PowerShell@2 + displayName: Darc gather-drop + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/darc-gather-drop.ps1 + arguments: -BarBuildId $(BARBuildId) + -DropLocation $(Agent.BuildDirectory)/Temp/Drop/ + -MaestroApiAccessToken $(MaestroApiAccessToken) + -MaestroApiEndPoint $(MaestroApiEndPoint) + -MaestroApiVersion $(MaestroApiVersion) diff --git a/eng/common/templates/post-build/post-build.yml b/eng/common/templates/post-build/post-build.yml new file mode 100644 index 000000000..33db50ce2 --- /dev/null +++ b/eng/common/templates/post-build/post-build.yml @@ -0,0 +1,102 @@ +parameters: + enableSourceLinkValidation: true + enableSigningValidation: true + enableSymbolValidation: true + enableNugetValidation: true + SDLValidationParameters: + enable: false + params: '' + + # Which stages should finish execution before post-build stages start + dependsOn: [build] + +stages: +- stage: validate + dependsOn: ${{ parameters.dependsOn }} + displayName: Validate + jobs: + - ${{ if eq(parameters.enableNugetValidation, 'true') }}: + - job: + displayName: NuGet Validation + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/nuget-validation.ps1 + arguments: -PackagesPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ + -ToolDestinationPath $(Agent.BuildDirectory)/Extract/ + + - ${{ if eq(parameters.enableSigningValidation, 'true') }}: + - job: + displayName: Signing Validation + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task SigningValidation -restore -msbuildEngine dotnet + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts' + /p:Configuration=Release + + - ${{ if eq(parameters.enableSourceLinkValidation, 'true') }}: + - job: + displayName: SourceLink Validation + variables: + - template: common-variables.yml + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: current + artifactName: BlobArtifacts + + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/sourcelink-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -ExtractPath $(Agent.BuildDirectory)/Extract/ + -GHRepoName $(Build.Repository.Name) + -GHCommit $(Build.SourceVersion) + -SourcelinkCliVersion $(SourceLinkCLIVersion) + + - ${{ if eq(parameters.SDLValidationParameters.enable, 'true') }}: + - template: /eng/common/templates/job/execute-sdl.yml + parameters: + additionalParameters: ${{ parameters.SDLValidationParameters.params }} + +- template: \eng\common\templates\post-build\channels\netcore-dev-5.yml + parameters: + enableSymbolValidation: ${{ parameters.enableSymbolValidation }} + +- template: \eng\common\templates\post-build\channels\public-dev-release.yml + parameters: + enableSymbolValidation: ${{ parameters.enableSymbolValidation }} + +- template: \eng\common\templates\post-build\channels\netcore-tools-latest.yml + parameters: + enableSymbolValidation: ${{ parameters.enableSymbolValidation }} + +- template: \eng\common\templates\post-build\channels\public-validation-release.yml + +- template: \eng\common\templates\post-build\channels\public-release.yml + +- template: \eng\common\templates\post-build\channels\internal-servicing.yml diff --git a/eng/common/templates/post-build/promote-build.yml b/eng/common/templates/post-build/promote-build.yml new file mode 100644 index 000000000..6b479c3b8 --- /dev/null +++ b/eng/common/templates/post-build/promote-build.yml @@ -0,0 +1,25 @@ +parameters: + ChannelId: 0 + +jobs: +- job: + displayName: Promote Build + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', ${{ parameters.ChannelId }})) + variables: + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + - name: ChannelId + value: ${{ parameters.ChannelId }} + pool: + vmImage: 'windows-2019' + steps: + - task: PowerShell@2 + displayName: Add Build to Channel + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/promote-build.ps1 + arguments: -BuildId $(BARBuildId) + -ChannelId $(ChannelId) + -MaestroApiAccessToken $(MaestroApiAccessToken) + -MaestroApiEndPoint $(MaestroApiEndPoint) + -MaestroApiVersion $(MaestroApiVersion) diff --git a/eng/common/templates/post-build/setup-maestro-vars.yml b/eng/common/templates/post-build/setup-maestro-vars.yml new file mode 100644 index 000000000..56242b068 --- /dev/null +++ b/eng/common/templates/post-build/setup-maestro-vars.yml @@ -0,0 +1,18 @@ +jobs: +- job: setupMaestroVars + displayName: Setup Maestro Vars + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Release Configs + inputs: + buildType: current + artifactName: ReleaseConfigs + + - task: PowerShell@2 + name: setReleaseVars + displayName: Set Release Configs Vars + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/setup-maestro-vars.ps1 + arguments: -ReleaseConfigsPath '$(Build.StagingDirectory)/ReleaseConfigs/ReleaseConfigs.txt' diff --git a/eng/common/templates/post-build/trigger-subscription.yml b/eng/common/templates/post-build/trigger-subscription.yml new file mode 100644 index 000000000..da669030d --- /dev/null +++ b/eng/common/templates/post-build/trigger-subscription.yml @@ -0,0 +1,13 @@ +parameters: + ChannelId: 0 + +steps: +- task: PowerShell@2 + displayName: Triggering subscriptions + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/trigger-subscriptions.ps1 + arguments: -SourceRepo $(Build.Repository.Uri) + -ChannelId ${{ parameters.ChannelId }} + -MaestroApiAccessToken $(MaestroAccessToken) + -MaestroApiEndPoint $(MaestroApiEndPoint) + -MaestroApiVersion $(MaestroApiVersion) diff --git a/eng/common/templates/steps/build-reason.yml b/eng/common/templates/steps/build-reason.yml new file mode 100644 index 000000000..eba58109b --- /dev/null +++ b/eng/common/templates/steps/build-reason.yml @@ -0,0 +1,12 @@ +# build-reason.yml +# Description: runs steps if build.reason condition is valid. conditions is a string of valid build reasons +# to include steps (',' separated). +parameters: + conditions: '' + steps: [] + +steps: + - ${{ if and( not(startsWith(parameters.conditions, 'not')), contains(parameters.conditions, variables['build.reason'])) }}: + - ${{ parameters.steps }} + - ${{ if and( startsWith(parameters.conditions, 'not'), not(contains(parameters.conditions, variables['build.reason']))) }}: + - ${{ parameters.steps }} diff --git a/eng/common/templates/steps/perf-send-to-helix.yml b/eng/common/templates/steps/perf-send-to-helix.yml new file mode 100644 index 000000000..b3ea9acf1 --- /dev/null +++ b/eng/common/templates/steps/perf-send-to-helix.yml @@ -0,0 +1,66 @@ +# Please remember to update the documentation if you make changes to these parameters! +parameters: + HelixSource: 'pr/default' # required -- sources must start with pr/, official/, prodcon/, or agent/ + HelixType: 'tests/default/' # required -- Helix telemetry which identifies what type of data this is; should include "test" for clarity and must end in '/' + HelixBuild: $(Build.BuildNumber) # required -- the build number Helix will use to identify this -- automatically set to the AzDO build number + HelixTargetQueues: '' # required -- semicolon delimited list of Helix queues to test on; see https://helix.dot.net/ for a list of queues + HelixAccessToken: '' # required -- access token to make Helix API requests; should be provided by the appropriate variable group + HelixPreCommands: '' # optional -- commands to run before Helix work item execution + HelixPostCommands: '' # optional -- commands to run after Helix work item execution + WorkItemDirectory: '' # optional -- a payload directory to zip up and send to Helix; requires WorkItemCommand; incompatible with XUnitProjects + CorrelationPayloadDirectory: '' # optional -- a directory to zip up and send to Helix as a correlation payload + IncludeDotNetCli: false # optional -- true will download a version of the .NET CLI onto the Helix machine as a correlation payload; requires DotNetCliPackageType and DotNetCliVersion + DotNetCliPackageType: '' # optional -- either 'sdk' or 'runtime'; determines whether the sdk or runtime will be sent to Helix; see https://raw.githubusercontent.com/dotnet/core/master/release-notes/releases.json + DotNetCliVersion: '' # optional -- version of the CLI to send to Helix; based on this: https://raw.githubusercontent.com/dotnet/core/master/release-notes/releases.json + EnableXUnitReporter: false # optional -- true enables XUnit result reporting to Mission Control + WaitForWorkItemCompletion: true # optional -- true will make the task wait until work items have been completed and fail the build if work items fail. False is "fire and forget." + Creator: '' # optional -- if the build is external, use this to specify who is sending the job + DisplayNamePrefix: 'Send job to Helix' # optional -- rename the beginning of the displayName of the steps in AzDO + condition: succeeded() # optional -- condition for step to execute; defaults to succeeded() + continueOnError: false # optional -- determines whether to continue the build if the step errors; defaults to false + +steps: + - powershell: $(Build.SourcesDirectory)\eng\common\msbuild.ps1 $(Build.SourcesDirectory)\eng\common\performance\perfhelixpublish.proj /restore /t:Test /bl:$(Build.SourcesDirectory)\artifacts\log\$env:BuildConfig\SendToHelix.binlog + displayName: ${{ parameters.DisplayNamePrefix }} (Windows) + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} + HelixAccessToken: ${{ parameters.HelixAccessToken }} + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + EnableXUnitReporter: ${{ parameters.EnableXUnitReporter }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + Creator: ${{ parameters.Creator }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, eq(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} + - script: $BUILD_SOURCESDIRECTORY/eng/common/msbuild.sh $BUILD_SOURCESDIRECTORY/eng/common/performance/perfhelixpublish.proj /restore /t:Test /bl:$BUILD_SOURCESDIRECTORY/artifacts/log/$BuildConfig/SendToHelix.binlog + displayName: ${{ parameters.DisplayNamePrefix }} (Unix) + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} + HelixAccessToken: ${{ parameters.HelixAccessToken }} + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + EnableXUnitReporter: ${{ parameters.EnableXUnitReporter }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + Creator: ${{ parameters.Creator }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, ne(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} diff --git a/eng/common/templates/steps/run-on-unix.yml b/eng/common/templates/steps/run-on-unix.yml new file mode 100644 index 000000000..e1733814f --- /dev/null +++ b/eng/common/templates/steps/run-on-unix.yml @@ -0,0 +1,7 @@ +parameters: + agentOs: '' + steps: [] + +steps: +- ${{ if ne(parameters.agentOs, 'Windows_NT') }}: + - ${{ parameters.steps }} diff --git a/eng/common/templates/steps/run-on-windows.yml b/eng/common/templates/steps/run-on-windows.yml new file mode 100644 index 000000000..73e7e9c27 --- /dev/null +++ b/eng/common/templates/steps/run-on-windows.yml @@ -0,0 +1,7 @@ +parameters: + agentOs: '' + steps: [] + +steps: +- ${{ if eq(parameters.agentOs, 'Windows_NT') }}: + - ${{ parameters.steps }} diff --git a/eng/common/templates/steps/run-script-ifequalelse.yml b/eng/common/templates/steps/run-script-ifequalelse.yml new file mode 100644 index 000000000..3d1242f55 --- /dev/null +++ b/eng/common/templates/steps/run-script-ifequalelse.yml @@ -0,0 +1,33 @@ +parameters: + # if parameter1 equals parameter 2, run 'ifScript' command, else run 'elsescript' command + parameter1: '' + parameter2: '' + ifScript: '' + elseScript: '' + + # name of script step + name: Script + + # display name of script step + displayName: If-Equal-Else Script + + # environment + env: {} + + # conditional expression for step execution + condition: '' + +steps: +- ${{ if and(ne(parameters.ifScript, ''), eq(parameters.parameter1, parameters.parameter2)) }}: + - script: ${{ parameters.ifScript }} + name: ${{ parameters.name }} + displayName: ${{ parameters.displayName }} + env: ${{ parameters.env }} + condition: ${{ parameters.condition }} + +- ${{ if and(ne(parameters.elseScript, ''), ne(parameters.parameter1, parameters.parameter2)) }}: + - script: ${{ parameters.elseScript }} + name: ${{ parameters.name }} + displayName: ${{ parameters.displayName }} + env: ${{ parameters.env }} + condition: ${{ parameters.condition }} \ No newline at end of file diff --git a/eng/common/templates/steps/send-to-helix.yml b/eng/common/templates/steps/send-to-helix.yml new file mode 100644 index 000000000..05df886f5 --- /dev/null +++ b/eng/common/templates/steps/send-to-helix.yml @@ -0,0 +1,91 @@ +# Please remember to update the documentation if you make changes to these parameters! +parameters: + HelixSource: 'pr/default' # required -- sources must start with pr/, official/, prodcon/, or agent/ + HelixType: 'tests/default/' # required -- Helix telemetry which identifies what type of data this is; should include "test" for clarity and must end in '/' + HelixBuild: $(Build.BuildNumber) # required -- the build number Helix will use to identify this -- automatically set to the AzDO build number + HelixTargetQueues: '' # required -- semicolon delimited list of Helix queues to test on; see https://helix.dot.net/ for a list of queues + HelixAccessToken: '' # required -- access token to make Helix API requests; should be provided by the appropriate variable group + HelixConfiguration: '' # optional -- additional property attached to a job + HelixPreCommands: '' # optional -- commands to run before Helix work item execution + HelixPostCommands: '' # optional -- commands to run after Helix work item execution + WorkItemDirectory: '' # optional -- a payload directory to zip up and send to Helix; requires WorkItemCommand; incompatible with XUnitProjects + WorkItemCommand: '' # optional -- a command to execute on the payload; requires WorkItemDirectory; incompatible with XUnitProjects + WorkItemTimeout: '' # optional -- a timeout in seconds for the work item command; requires WorkItemDirectory; incompatible with XUnitProjects + CorrelationPayloadDirectory: '' # optional -- a directory to zip up and send to Helix as a correlation payload + XUnitProjects: '' # optional -- semicolon delimited list of XUnitProjects to parse and send to Helix; requires XUnitRuntimeTargetFramework, XUnitPublishTargetFramework, XUnitRunnerVersion, and IncludeDotNetCli=true + XUnitWorkItemTimeout: '' # optional -- the workitem timeout in seconds for all workitems created from the xUnit projects specified by XUnitProjects + XUnitPublishTargetFramework: '' # optional -- framework to use to publish your xUnit projects + XUnitRuntimeTargetFramework: '' # optional -- framework to use for the xUnit console runner + XUnitRunnerVersion: '' # optional -- version of the xUnit nuget package you wish to use on Helix; required for XUnitProjects + IncludeDotNetCli: false # optional -- true will download a version of the .NET CLI onto the Helix machine as a correlation payload; requires DotNetCliPackageType and DotNetCliVersion + DotNetCliPackageType: '' # optional -- either 'sdk' or 'runtime'; determines whether the sdk or runtime will be sent to Helix; see https://raw.githubusercontent.com/dotnet/core/master/release-notes/releases.json + DotNetCliVersion: '' # optional -- version of the CLI to send to Helix; based on this: https://raw.githubusercontent.com/dotnet/core/master/release-notes/releases.json + EnableXUnitReporter: false # optional -- true enables XUnit result reporting to Mission Control + WaitForWorkItemCompletion: true # optional -- true will make the task wait until work items have been completed and fail the build if work items fail. False is "fire and forget." + IsExternal: false # [DEPRECATED] -- doesn't do anything, jobs are external if HelixAccessToken is empty and Creator is set + Creator: '' # optional -- if the build is external, use this to specify who is sending the job + DisplayNamePrefix: 'Run Tests' # optional -- rename the beginning of the displayName of the steps in AzDO + condition: succeeded() # optional -- condition for step to execute; defaults to succeeded() + continueOnError: false # optional -- determines whether to continue the build if the step errors; defaults to false + +steps: + - powershell: 'powershell "$env:BUILD_SOURCESDIRECTORY\eng\common\msbuild.ps1 $env:BUILD_SOURCESDIRECTORY\eng\common\helixpublish.proj /restore /t:Test /bl:$env:BUILD_SOURCESDIRECTORY\artifacts\log\$env:BuildConfig\SendToHelix.binlog"' + displayName: ${{ parameters.DisplayNamePrefix }} (Windows) + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixConfiguration: ${{ parameters.HelixConfiguration }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} + HelixAccessToken: ${{ parameters.HelixAccessToken }} + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + WorkItemCommand: ${{ parameters.WorkItemCommand }} + WorkItemTimeout: ${{ parameters.WorkItemTimeout }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + XUnitProjects: ${{ parameters.XUnitProjects }} + XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} + XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} + XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} + XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + EnableXUnitReporter: ${{ parameters.EnableXUnitReporter }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + Creator: ${{ parameters.Creator }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, eq(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} + - script: $BUILD_SOURCESDIRECTORY/eng/common/msbuild.sh $BUILD_SOURCESDIRECTORY/eng/common/helixpublish.proj /restore /t:Test /bl:$BUILD_SOURCESDIRECTORY/artifacts/log/$BuildConfig/SendToHelix.binlog + displayName: ${{ parameters.DisplayNamePrefix }} (Unix) + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixConfiguration: ${{ parameters.HelixConfiguration }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} + HelixAccessToken: ${{ parameters.HelixAccessToken }} + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + WorkItemCommand: ${{ parameters.WorkItemCommand }} + WorkItemTimeout: ${{ parameters.WorkItemTimeout }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + XUnitProjects: ${{ parameters.XUnitProjects }} + XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} + XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} + XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} + XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + EnableXUnitReporter: ${{ parameters.EnableXUnitReporter }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + Creator: ${{ parameters.Creator }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, ne(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} diff --git a/eng/common/templates/steps/telemetry-end.yml b/eng/common/templates/steps/telemetry-end.yml new file mode 100644 index 000000000..fadc04ca1 --- /dev/null +++ b/eng/common/templates/steps/telemetry-end.yml @@ -0,0 +1,102 @@ +parameters: + maxRetries: 5 + retryDelay: 10 # in seconds + +steps: +- bash: | + if [ "$AGENT_JOBSTATUS" = "Succeeded" ] || [ "$AGENT_JOBSTATUS" = "PartiallySucceeded" ]; then + errorCount=0 + else + errorCount=1 + fi + warningCount=0 + + curlStatus=1 + retryCount=0 + # retry loop to harden against spotty telemetry connections + # we don't retry successes and 4xx client errors + until [[ $curlStatus -eq 0 || ( $curlStatus -ge 400 && $curlStatus -le 499 ) || $retryCount -ge $MaxRetries ]] + do + if [ $retryCount -gt 0 ]; then + echo "Failed to send telemetry to Helix; waiting $RetryDelay seconds before retrying..." + sleep $RetryDelay + fi + + # create a temporary file for curl output + res=`mktemp` + + curlResult=` + curl --verbose --output $res --write-out "%{http_code}"\ + -H 'Content-Type: application/json' \ + -H "X-Helix-Job-Token: $Helix_JobToken" \ + -H 'Content-Length: 0' \ + -X POST -G "https://helix.dot.net/api/2018-03-14/telemetry/job/build/$Helix_WorkItemId/finish" \ + --data-urlencode "errorCount=$errorCount" \ + --data-urlencode "warningCount=$warningCount"` + curlStatus=$? + + if [ $curlStatus -eq 0 ]; then + if [ $curlResult -gt 299 ] || [ $curlResult -lt 200 ]; then + curlStatus=$curlResult + fi + fi + + let retryCount++ + done + + if [ $curlStatus -ne 0 ]; then + echo "Failed to Send Build Finish information after $retryCount retries" + vstsLogOutput="vso[task.logissue type=error;sourcepath=templates/steps/telemetry-end.yml;code=1;]Failed to Send Build Finish information: $curlStatus" + echo "##$vstsLogOutput" + exit 1 + fi + displayName: Send Unix Build End Telemetry + env: + # defined via VSTS variables in start-job.sh + Helix_JobToken: $(Helix_JobToken) + Helix_WorkItemId: $(Helix_WorkItemId) + MaxRetries: ${{ parameters.maxRetries }} + RetryDelay: ${{ parameters.retryDelay }} + condition: and(always(), ne(variables['Agent.Os'], 'Windows_NT')) +- powershell: | + if (($env:Agent_JobStatus -eq 'Succeeded') -or ($env:Agent_JobStatus -eq 'PartiallySucceeded')) { + $ErrorCount = 0 + } else { + $ErrorCount = 1 + } + $WarningCount = 0 + + # Basic retry loop to harden against server flakiness + $retryCount = 0 + while ($retryCount -lt $env:MaxRetries) { + try { + Invoke-RestMethod -Uri "https://helix.dot.net/api/2018-03-14/telemetry/job/build/$env:Helix_WorkItemId/finish?errorCount=$ErrorCount&warningCount=$WarningCount" -Method Post -ContentType "application/json" -Body "" ` + -Headers @{ 'X-Helix-Job-Token'=$env:Helix_JobToken } + break + } + catch { + $statusCode = $_.Exception.Response.StatusCode.value__ + if ($statusCode -ge 400 -and $statusCode -le 499) { + Write-Host "##vso[task.logissue]error Failed to send telemetry to Helix (status code $statusCode); not retrying (4xx client error)" + Write-Host "##vso[task.logissue]error ", $_.Exception.GetType().FullName, $_.Exception.Message + exit 1 + } + Write-Host "Failed to send telemetry to Helix (status code $statusCode); waiting $env:RetryDelay seconds before retrying..." + $retryCount++ + sleep $env:RetryDelay + continue + } + } + + if ($retryCount -ge $env:MaxRetries) { + Write-Host "##vso[task.logissue]error Failed to send telemetry to Helix after $retryCount retries." + exit 1 + } + displayName: Send Windows Build End Telemetry + env: + # defined via VSTS variables in start-job.ps1 + Helix_JobToken: $(Helix_JobToken) + Helix_WorkItemId: $(Helix_WorkItemId) + MaxRetries: ${{ parameters.maxRetries }} + RetryDelay: ${{ parameters.retryDelay }} + condition: and(always(),eq(variables['Agent.Os'], 'Windows_NT')) diff --git a/eng/common/templates/steps/telemetry-start.yml b/eng/common/templates/steps/telemetry-start.yml new file mode 100644 index 000000000..32c01ef0b --- /dev/null +++ b/eng/common/templates/steps/telemetry-start.yml @@ -0,0 +1,241 @@ +parameters: + helixSource: 'undefined_defaulted_in_telemetry.yml' + helixType: 'undefined_defaulted_in_telemetry.yml' + buildConfig: '' + runAsPublic: false + maxRetries: 5 + retryDelay: 10 # in seconds + +steps: +- ${{ if and(eq(parameters.runAsPublic, 'false'), not(eq(variables['System.TeamProject'], 'public'))) }}: + - task: AzureKeyVault@1 + inputs: + azureSubscription: 'HelixProd_KeyVault' + KeyVaultName: HelixProdKV + SecretsFilter: 'HelixApiAccessToken' + condition: always() +- bash: | + # create a temporary file + jobInfo=`mktemp` + + # write job info content to temporary file + cat > $jobInfo <' | Set-Content $proj + + MSBuild-Core $proj $bl /t:__WriteToolsetLocation /clp:ErrorsOnly`;NoSummary /p:__ToolsetLocationOutputFile=$toolsetLocationFile + + $path = Get-Content $toolsetLocationFile -TotalCount 1 + if (!(Test-Path $path)) { + throw "Invalid toolset path: $path" + } + + return $global:_ToolsetBuildProj = $path +} + +function ExitWithExitCode([int] $exitCode) { + if ($ci -and $prepareMachine) { + Stop-Processes + } + exit $exitCode +} + +function Stop-Processes() { + Write-Host "Killing running build processes..." + foreach ($processName in $processesToStopOnExit) { + Get-Process -Name $processName -ErrorAction SilentlyContinue | Stop-Process + } +} + +# +# Executes msbuild (or 'dotnet msbuild') with arguments passed to the function. +# The arguments are automatically quoted. +# Terminates the script if the build fails. +# +function MSBuild() { + if ($pipelinesLog) { + $buildTool = InitializeBuildTool + $toolsetBuildProject = InitializeToolset + $path = Split-Path -parent $toolsetBuildProject + $path = Join-Path $path (Join-Path $buildTool.Framework "Microsoft.DotNet.Arcade.Sdk.dll") + $args += "/logger:$path" + } + + MSBuild-Core @args +} + +# +# Executes msbuild (or 'dotnet msbuild') with arguments passed to the function. +# The arguments are automatically quoted. +# Terminates the script if the build fails. +# +function MSBuild-Core() { + if ($ci) { + if (!$binaryLog) { + Write-PipelineTaskError -Message "Binary log must be enabled in CI build." + ExitWithExitCode 1 + } + + if ($nodeReuse) { + Write-PipelineTaskError -Message "Node reuse must be disabled in CI build." + ExitWithExitCode 1 + } + } + + $buildTool = InitializeBuildTool + + $cmdArgs = "$($buildTool.Command) /m /nologo /clp:Summary /v:$verbosity /nr:$nodeReuse /p:ContinuousIntegrationBuild=$ci" + + if ($warnAsError) { + $cmdArgs += " /warnaserror /p:TreatWarningsAsErrors=true" + } + else { + $cmdArgs += " /p:TreatWarningsAsErrors=false" + } + + foreach ($arg in $args) { + if ($arg -ne $null -and $arg.Trim() -ne "") { + $cmdArgs += " `"$arg`"" + } + } + + $exitCode = Exec-Process $buildTool.Path $cmdArgs + + if ($exitCode -ne 0) { + Write-PipelineTaskError -Message "Build failed." + + $buildLog = GetMSBuildBinaryLogCommandLineArgument $args + if ($buildLog -ne $null) { + Write-Host "See log: $buildLog" -ForegroundColor DarkGray + } + + ExitWithExitCode $exitCode + } +} + +function GetMSBuildBinaryLogCommandLineArgument($arguments) { + foreach ($argument in $arguments) { + if ($argument -ne $null) { + $arg = $argument.Trim() + if ($arg.StartsWith("/bl:", "OrdinalIgnoreCase")) { + return $arg.Substring("/bl:".Length) + } + + if ($arg.StartsWith("/binaryLogger:", "OrdinalIgnoreCase")) { + return $arg.Substring("/binaryLogger:".Length) + } + } + } + + return $null +} + +. $PSScriptRoot\pipeline-logging-functions.ps1 + +$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..") +$EngRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +$ArtifactsDir = Join-Path $RepoRoot "artifacts" +$ToolsetDir = Join-Path $ArtifactsDir "toolset" +$ToolsDir = Join-Path $RepoRoot ".tools" +$LogDir = Join-Path (Join-Path $ArtifactsDir "log") $configuration +$TempDir = Join-Path (Join-Path $ArtifactsDir "tmp") $configuration +$GlobalJson = Get-Content -Raw -Path (Join-Path $RepoRoot "global.json") | ConvertFrom-Json +# true if global.json contains a "runtimes" section +$globalJsonHasRuntimes = if ($GlobalJson.tools.PSObject.Properties.Name -Match 'runtimes') { $true } else { $false } + +Create-Directory $ToolsetDir +Create-Directory $TempDir +Create-Directory $LogDir + +Write-PipelineSetVariable -Name 'Artifacts' -Value $ArtifactsDir +Write-PipelineSetVariable -Name 'Artifacts.Toolset' -Value $ToolsetDir +Write-PipelineSetVariable -Name 'Artifacts.Log' -Value $LogDir +Write-PipelineSetVariable -Name 'TEMP' -Value $TempDir +Write-PipelineSetVariable -Name 'TMP' -Value $TempDir diff --git a/eng/common/tools.sh b/eng/common/tools.sh new file mode 100755 index 000000000..3af9be615 --- /dev/null +++ b/eng/common/tools.sh @@ -0,0 +1,394 @@ +#!/usr/bin/env bash + +# Initialize variables if they aren't already defined. + +# CI mode - set to true on CI server for PR validation build or official build. +ci=${ci:-false} + +# Set to true to use the pipelines logger which will enable Azure logging output. +# https://github.com/Microsoft/azure-pipelines-tasks/blob/master/docs/authoring/commands.md +# This flag is meant as a temporary opt-opt for the feature while validate it across +# our consumers. It will be deleted in the future. +if [[ "$ci" == true ]]; then + pipelines_log=${pipelines_log:-true} +else + pipelines_log=${pipelines_log:-false} +fi + +# Build configuration. Common values include 'Debug' and 'Release', but the repository may use other names. +configuration=${configuration:-'Debug'} + +# Set to true to output binary log from msbuild. Note that emitting binary log slows down the build. +# Binary log must be enabled on CI. +binary_log=${binary_log:-$ci} + +# Turns on machine preparation/clean up code that changes the machine state (e.g. kills build processes). +prepare_machine=${prepare_machine:-false} + +# True to restore toolsets and dependencies. +restore=${restore:-true} + +# Adjusts msbuild verbosity level. +verbosity=${verbosity:-'minimal'} + +# Set to true to reuse msbuild nodes. Recommended to not reuse on CI. +if [[ "$ci" == true ]]; then + node_reuse=${node_reuse:-false} +else + node_reuse=${node_reuse:-true} +fi + +# Configures warning treatment in msbuild. +warn_as_error=${warn_as_error:-true} + +# True to attempt using .NET Core already that meets requirements specified in global.json +# installed on the machine instead of downloading one. +use_installed_dotnet_cli=${use_installed_dotnet_cli:-true} + +# Enable repos to use a particular version of the on-line dotnet-install scripts. +# default URL: https://dot.net/v1/dotnet-install.sh +dotnetInstallScriptVersion=${dotnetInstallScriptVersion:-'v1'} + +# True to use global NuGet cache instead of restoring packages to repository-local directory. +if [[ "$ci" == true ]]; then + use_global_nuget_cache=${use_global_nuget_cache:-false} +else + use_global_nuget_cache=${use_global_nuget_cache:-true} +fi + +# Resolve any symlinks in the given path. +function ResolvePath { + local path=$1 + + while [[ -h $path ]]; do + local dir="$( cd -P "$( dirname "$path" )" && pwd )" + path="$(readlink "$path")" + + # if $path was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $path != /* ]] && path="$dir/$path" + done + + # return value + _ResolvePath="$path" +} + +# ReadVersionFromJson [json key] +function ReadGlobalVersion { + local key=$1 + + local line=`grep -m 1 "$key" "$global_json_file"` + local pattern="\"$key\" *: *\"(.*)\"" + + if [[ ! $line =~ $pattern ]]; then + Write-PipelineTelemetryError -category 'InitializeToolset' "Error: Cannot find \"$key\" in $global_json_file" + ExitWithExitCode 1 + fi + + # return value + _ReadGlobalVersion=${BASH_REMATCH[1]} +} + +function InitializeDotNetCli { + if [[ -n "${_InitializeDotNetCli:-}" ]]; then + return + fi + + local install=$1 + + # Don't resolve runtime, shared framework, or SDK from other locations to ensure build determinism + export DOTNET_MULTILEVEL_LOOKUP=0 + + # Disable first run since we want to control all package sources + export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 + + # Disable telemetry on CI + if [[ $ci == true ]]; then + export DOTNET_CLI_TELEMETRY_OPTOUT=1 + fi + + # LTTNG is the logging infrastructure used by Core CLR. Need this variable set + # so it doesn't output warnings to the console. + export LTTNG_HOME="$HOME" + + # Source Build uses DotNetCoreSdkDir variable + if [[ -n "${DotNetCoreSdkDir:-}" ]]; then + export DOTNET_INSTALL_DIR="$DotNetCoreSdkDir" + fi + + # Find the first path on $PATH that contains the dotnet.exe + if [[ "$use_installed_dotnet_cli" == true && $global_json_has_runtimes == false && -z "${DOTNET_INSTALL_DIR:-}" ]]; then + local dotnet_path=`command -v dotnet` + if [[ -n "$dotnet_path" ]]; then + ResolvePath "$dotnet_path" + export DOTNET_INSTALL_DIR=`dirname "$_ResolvePath"` + fi + fi + + ReadGlobalVersion "dotnet" + local dotnet_sdk_version=$_ReadGlobalVersion + local dotnet_root="" + + # Use dotnet installation specified in DOTNET_INSTALL_DIR if it contains the required SDK version, + # otherwise install the dotnet CLI and SDK to repo local .dotnet directory to avoid potential permission issues. + if [[ $global_json_has_runtimes == false && -n "${DOTNET_INSTALL_DIR:-}" && -d "$DOTNET_INSTALL_DIR/sdk/$dotnet_sdk_version" ]]; then + dotnet_root="$DOTNET_INSTALL_DIR" + else + dotnet_root="$repo_root/.dotnet" + + export DOTNET_INSTALL_DIR="$dotnet_root" + + if [[ ! -d "$DOTNET_INSTALL_DIR/sdk/$dotnet_sdk_version" ]]; then + if [[ "$install" == true ]]; then + InstallDotNetSdk "$dotnet_root" "$dotnet_sdk_version" + else + Write-PipelineTelemetryError -category 'InitializeToolset' "Unable to find dotnet with SDK version '$dotnet_sdk_version'" + ExitWithExitCode 1 + fi + fi + fi + + # Add dotnet to PATH. This prevents any bare invocation of dotnet in custom + # build steps from using anything other than what we've downloaded. + Write-PipelinePrependPath -path "$dotnet_root" + + Write-PipelineSetVariable -name "DOTNET_MULTILEVEL_LOOKUP" -value "0" + Write-PipelineSetVariable -name "DOTNET_SKIP_FIRST_TIME_EXPERIENCE" -value "1" + + # return value + _InitializeDotNetCli="$dotnet_root" +} + +function InstallDotNetSdk { + local root=$1 + local version=$2 + local architecture="" + if [[ $# == 3 ]]; then + architecture=$3 + fi + InstallDotNet "$root" "$version" $architecture +} + +function InstallDotNet { + local root=$1 + local version=$2 + + GetDotNetInstallScript "$root" + local install_script=$_GetDotNetInstallScript + + local archArg='' + if [[ -n "${3:-}" ]]; then + archArg="--architecture $3" + fi + local runtimeArg='' + if [[ -n "${4:-}" ]]; then + runtimeArg="--runtime $4" + fi + + local skipNonVersionedFilesArg="" + if [[ "$#" -ge "5" ]]; then + skipNonVersionedFilesArg="--skip-non-versioned-files" + fi + bash "$install_script" --version $version --install-dir "$root" $archArg $runtimeArg $skipNonVersionedFilesArg || { + local exit_code=$? + Write-PipelineTelemetryError -category 'InitializeToolset' "Failed to install dotnet SDK (exit code '$exit_code')." + ExitWithExitCode $exit_code + } +} + +function GetDotNetInstallScript { + local root=$1 + local install_script="$root/dotnet-install.sh" + local install_script_url="https://dot.net/$dotnetInstallScriptVersion/dotnet-install.sh" + + if [[ ! -a "$install_script" ]]; then + mkdir -p "$root" + + echo "Downloading '$install_script_url'" + + # Use curl if available, otherwise use wget + if command -v curl > /dev/null; then + curl "$install_script_url" -sSL --retry 10 --create-dirs -o "$install_script" + else + wget -q -O "$install_script" "$install_script_url" + fi + fi + + # return value + _GetDotNetInstallScript="$install_script" +} + +function InitializeBuildTool { + if [[ -n "${_InitializeBuildTool:-}" ]]; then + return + fi + + InitializeDotNetCli $restore + + # return values + _InitializeBuildTool="$_InitializeDotNetCli/dotnet" + _InitializeBuildToolCommand="msbuild" + _InitializeBuildToolFramework="netcoreapp2.1" +} + +function GetNuGetPackageCachePath { + if [[ -z ${NUGET_PACKAGES:-} ]]; then + if [[ "$use_global_nuget_cache" == true ]]; then + export NUGET_PACKAGES="$HOME/.nuget/packages" + else + export NUGET_PACKAGES="$repo_root/.packages" + fi + fi + + # return value + _GetNuGetPackageCachePath=$NUGET_PACKAGES +} + +function InitializeNativeTools() { + if grep -Fq "native-tools" $global_json_file + then + local nativeArgs="" + if [[ "$ci" == true ]]; then + nativeArgs="--installDirectory $tools_dir" + fi + "$_script_dir/init-tools-native.sh" $nativeArgs + fi +} + +function InitializeToolset { + if [[ -n "${_InitializeToolset:-}" ]]; then + return + fi + + GetNuGetPackageCachePath + + ReadGlobalVersion "Microsoft.DotNet.Arcade.Sdk" + + local toolset_version=$_ReadGlobalVersion + local toolset_location_file="$toolset_dir/$toolset_version.txt" + + if [[ -a "$toolset_location_file" ]]; then + local path=`cat "$toolset_location_file"` + if [[ -a "$path" ]]; then + # return value + _InitializeToolset="$path" + return + fi + fi + + if [[ "$restore" != true ]]; then + Write-PipelineTelemetryError -category 'InitializeToolset' "Toolset version $toolset_version has not been restored." + ExitWithExitCode 2 + fi + + local proj="$toolset_dir/restore.proj" + + local bl="" + if [[ "$binary_log" == true ]]; then + bl="/bl:$log_dir/ToolsetRestore.binlog" + fi + + echo '' > "$proj" + MSBuild-Core "$proj" $bl /t:__WriteToolsetLocation /clp:ErrorsOnly\;NoSummary /p:__ToolsetLocationOutputFile="$toolset_location_file" + + local toolset_build_proj=`cat "$toolset_location_file"` + + if [[ ! -a "$toolset_build_proj" ]]; then + Write-PipelineTelemetryError -category 'InitializeToolset' "Invalid toolset path: $toolset_build_proj" + ExitWithExitCode 3 + fi + + # return value + _InitializeToolset="$toolset_build_proj" +} + +function ExitWithExitCode { + if [[ "$ci" == true && "$prepare_machine" == true ]]; then + StopProcesses + fi + exit $1 +} + +function StopProcesses { + echo "Killing running build processes..." + pkill -9 "dotnet" || true + pkill -9 "vbcscompiler" || true + return 0 +} + +function MSBuild { + local args=$@ + if [[ "$pipelines_log" == true ]]; then + InitializeBuildTool + InitializeToolset + local toolset_dir="${_InitializeToolset%/*}" + local logger_path="$toolset_dir/$_InitializeBuildToolFramework/Microsoft.DotNet.Arcade.Sdk.dll" + args=( "${args[@]}" "-logger:$logger_path" ) + fi + + MSBuild-Core ${args[@]} +} + +function MSBuild-Core { + if [[ "$ci" == true ]]; then + if [[ "$binary_log" != true ]]; then + Write-PipelineTaskError "Binary log must be enabled in CI build." + ExitWithExitCode 1 + fi + + if [[ "$node_reuse" == true ]]; then + Write-PipelineTaskError "Node reuse must be disabled in CI build." + ExitWithExitCode 1 + fi + fi + + InitializeBuildTool + + local warnaserror_switch="" + if [[ $warn_as_error == true ]]; then + warnaserror_switch="/warnaserror" + fi + + "$_InitializeBuildTool" "$_InitializeBuildToolCommand" /m /nologo /clp:Summary /v:$verbosity /nr:$node_reuse $warnaserror_switch /p:TreatWarningsAsErrors=$warn_as_error /p:ContinuousIntegrationBuild=$ci "$@" || { + local exit_code=$? + Write-PipelineTaskError "Build failed (exit code '$exit_code')." + ExitWithExitCode $exit_code + } +} + +ResolvePath "${BASH_SOURCE[0]}" +_script_dir=`dirname "$_ResolvePath"` + +. "$_script_dir/pipeline-logging-functions.sh" + +eng_root=`cd -P "$_script_dir/.." && pwd` +repo_root=`cd -P "$_script_dir/../.." && pwd` +artifacts_dir="$repo_root/artifacts" +toolset_dir="$artifacts_dir/toolset" +tools_dir="$repo_root/.tools" +log_dir="$artifacts_dir/log/$configuration" +temp_dir="$artifacts_dir/tmp/$configuration" + +global_json_file="$repo_root/global.json" +# determine if global.json contains a "runtimes" entry +global_json_has_runtimes=false +dotnetlocal_key=`grep -m 1 "runtimes" "$global_json_file"` || true +if [[ -n "$dotnetlocal_key" ]]; then + global_json_has_runtimes=true +fi + +# HOME may not be defined in some scenarios, but it is required by NuGet +if [[ -z $HOME ]]; then + export HOME="$repo_root/artifacts/.home/" + mkdir -p "$HOME" +fi + +mkdir -p "$toolset_dir" +mkdir -p "$temp_dir" +mkdir -p "$log_dir" + +Write-PipelineSetVariable -name "Artifacts" -value "$artifacts_dir" +Write-PipelineSetVariable -name "Artifacts.Toolset" -value "$toolset_dir" +Write-PipelineSetVariable -name "Artifacts.Log" -value "$log_dir" +Write-PipelineSetVariable -name "Temp" -value "$temp_dir" +Write-PipelineSetVariable -name "TMP" -value "$temp_dir" diff --git a/build/key.snk b/eng/key.snk similarity index 100% rename from build/key.snk rename to eng/key.snk diff --git a/global.json b/global.json new file mode 100644 index 000000000..2d3e20f2d --- /dev/null +++ b/global.json @@ -0,0 +1,10 @@ +{ + "tools": { + "dotnet": "3.0.100-preview8-013656" + }, + + "msbuild-sdks": { + "Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.19405.1", + "Microsoft.DotNet.Helix.Sdk": "2.0.0-beta.19405.1" + } +} diff --git a/korebuild-lock.txt b/korebuild-lock.txt deleted file mode 100644 index 3673744db..000000000 --- a/korebuild-lock.txt +++ /dev/null @@ -1,2 +0,0 @@ -version:2.1.0-rtm-15783 -commithash:5fc2b2f607f542a2ffde11c19825e786fc1a3774 diff --git a/korebuild.json b/korebuild.json deleted file mode 100644 index 678d8bb94..000000000 --- a/korebuild.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/release/2.1/tools/korebuild.schema.json", - "channel": "release/2.1" -} diff --git a/run.cmd b/run.cmd deleted file mode 100644 index d52d5c7e6..000000000 --- a/run.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@ECHO OFF -PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' %*; exit $LASTEXITCODE" diff --git a/run.ps1 b/run.ps1 deleted file mode 100644 index 27dcf848f..000000000 --- a/run.ps1 +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env powershell -#requires -version 4 - -<# -.SYNOPSIS -Executes KoreBuild commands. - -.DESCRIPTION -Downloads korebuild if required. Then executes the KoreBuild command. To see available commands, execute with `-Command help`. - -.PARAMETER Command -The KoreBuild command to run. - -.PARAMETER Path -The folder to build. Defaults to the folder containing this script. - -.PARAMETER Channel -The channel of KoreBuild to download. Overrides the value from the config file. - -.PARAMETER DotNetHome -The directory where .NET Core tools will be stored. - -.PARAMETER ToolsSource -The base url where build tools can be downloaded. Overrides the value from the config file. - -.PARAMETER Update -Updates KoreBuild to the latest version even if a lock file is present. - -.PARAMETER ConfigFile -The path to the configuration file that stores values. Defaults to korebuild.json. - -.PARAMETER ToolsSourceSuffix -The Suffix to append to the end of the ToolsSource. Useful for query strings in blob stores. - -.PARAMETER Arguments -Arguments to be passed to the command - -.NOTES -This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be. -When the lockfile is not present, KoreBuild will create one using latest available version from $Channel. - -The $ConfigFile is expected to be an JSON file. It is optional, and the configuration values in it are optional as well. Any options set -in the file are overridden by command line parameters. - -.EXAMPLE -Example config file: -```json -{ - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", - "channel": "dev", - "toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools" -} -``` -#> -[CmdletBinding(PositionalBinding = $false)] -param( - [Parameter(Mandatory = $true, Position = 0)] - [string]$Command, - [string]$Path = $PSScriptRoot, - [Alias('c')] - [string]$Channel, - [Alias('d')] - [string]$DotNetHome, - [Alias('s')] - [string]$ToolsSource, - [Alias('u')] - [switch]$Update, - [string]$ConfigFile, - [string]$ToolsSourceSuffix, - [Parameter(ValueFromRemainingArguments = $true)] - [string[]]$Arguments -) - -Set-StrictMode -Version 2 -$ErrorActionPreference = 'Stop' - -# -# Functions -# - -function Get-KoreBuild { - - $lockFile = Join-Path $Path 'korebuild-lock.txt' - - if (!(Test-Path $lockFile) -or $Update) { - Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile $ToolsSourceSuffix - } - - $version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1 - if (!$version) { - Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'" - } - $version = $version.TrimStart('version:').Trim() - $korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version) - - if (!(Test-Path $korebuildPath)) { - Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version" - New-Item -ItemType Directory -Path $korebuildPath | Out-Null - $remotePath = "$ToolsSource/korebuild/artifacts/$version/korebuild.$version.zip" - - try { - $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip" - Get-RemoteFile $remotePath $tmpfile $ToolsSourceSuffix - if (Get-Command -Name 'Expand-Archive' -ErrorAction Ignore) { - # Use built-in commands where possible as they are cross-plat compatible - Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath - } - else { - # Fallback to old approach for old installations of PowerShell - Add-Type -AssemblyName System.IO.Compression.FileSystem - [System.IO.Compression.ZipFile]::ExtractToDirectory($tmpfile, $korebuildPath) - } - } - catch { - Remove-Item -Recurse -Force $korebuildPath -ErrorAction Ignore - throw - } - finally { - Remove-Item $tmpfile -ErrorAction Ignore - } - } - - return $korebuildPath -} - -function Join-Paths([string]$path, [string[]]$childPaths) { - $childPaths | ForEach-Object { $path = Join-Path $path $_ } - return $path -} - -function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$RemoteSuffix) { - if ($RemotePath -notlike 'http*') { - Copy-Item $RemotePath $LocalPath - return - } - - $retries = 10 - while ($retries -gt 0) { - $retries -= 1 - try { - Invoke-WebRequest -UseBasicParsing -Uri $($RemotePath + $RemoteSuffix) -OutFile $LocalPath - return - } - catch { - Write-Verbose "Request failed. $retries retries remaining" - } - } - - Write-Error "Download failed: '$RemotePath'." -} - -# -# Main -# - -# Load configuration or set defaults - -$Path = Resolve-Path $Path -if (!$ConfigFile) { $ConfigFile = Join-Path $Path 'korebuild.json' } - -if (Test-Path $ConfigFile) { - try { - $config = Get-Content -Raw -Encoding UTF8 -Path $ConfigFile | ConvertFrom-Json - if ($config) { - if (!($Channel) -and (Get-Member -Name 'channel' -InputObject $config)) { [string] $Channel = $config.channel } - if (!($ToolsSource) -and (Get-Member -Name 'toolsSource' -InputObject $config)) { [string] $ToolsSource = $config.toolsSource} - } - } - catch { - Write-Warning "$ConfigFile could not be read. Its settings will be ignored." - Write-Warning $Error[0] - } -} - -if (!$DotNetHome) { - $DotNetHome = if ($env:DOTNET_HOME) { $env:DOTNET_HOME } ` - elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet'} ` - elseif ($env:HOME) {Join-Path $env:HOME '.dotnet'}` - else { Join-Path $PSScriptRoot '.dotnet'} -} - -if (!$Channel) { $Channel = 'dev' } -if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' } - -# Execute - -$korebuildPath = Get-KoreBuild -Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') - -try { - Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile - Invoke-KoreBuildCommand $Command @Arguments -} -finally { - Remove-Module 'KoreBuild' -ErrorAction Ignore -} diff --git a/run.sh b/run.sh deleted file mode 100755 index 834961fc3..000000000 --- a/run.sh +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# -# variables -# - -RESET="\033[0m" -RED="\033[0;31m" -YELLOW="\033[0;33m" -MAGENTA="\033[0;95m" -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -[ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet" -verbose=false -update=false -repo_path="$DIR" -channel='' -tools_source='' -tools_source_suffix='' - -# -# Functions -# -__usage() { - echo "Usage: $(basename "${BASH_SOURCE[0]}") command [options] [[--] ...]" - echo "" - echo "Arguments:" - echo " command The command to be run." - echo " ... Arguments passed to the command. Variable number of arguments allowed." - echo "" - echo "Options:" - echo " --verbose Show verbose output." - echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." - echo " --config-file The path to the configuration file that stores values. Defaults to korebuild.json." - echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." - echo " --path The directory to build. Defaults to the directory containing the script." - echo " -s|--tools-source|-ToolsSource The base url where build tools can be downloaded. Overrides the value from the config file." - echo " --tools-source-suffix|-ToolsSourceSuffix The suffix to append to tools-source. Useful for query strings." - echo " -u|--update Update to the latest KoreBuild even if the lock file is present." - echo "" - echo "Description:" - echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be." - echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel." - - if [[ "${1:-}" != '--no-exit' ]]; then - exit 2 - fi -} - -get_korebuild() { - local version - local lock_file="$repo_path/korebuild-lock.txt" - if [ ! -f "$lock_file" ] || [ "$update" = true ]; then - __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" "$tools_source_suffix" - fi - version="$(grep 'version:*' -m 1 "$lock_file")" - if [[ "$version" == '' ]]; then - __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'" - return 1 - fi - version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" - - { - if [ ! -d "$korebuild_path" ]; then - mkdir -p "$korebuild_path" - local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" - tmpfile="$(mktemp)" - echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" - if __get_remote_file "$remote_path" "$tmpfile" "$tools_source_suffix"; then - unzip -q -d "$korebuild_path" "$tmpfile" - fi - rm "$tmpfile" || true - fi - - source "$korebuild_path/KoreBuild.sh" - } || { - if [ -d "$korebuild_path" ]; then - echo "Cleaning up after failed installation" - rm -rf "$korebuild_path" || true - fi - return 1 - } -} - -__error() { - echo -e "${RED}error: $*${RESET}" 1>&2 -} - -__warn() { - echo -e "${YELLOW}warning: $*${RESET}" -} - -__machine_has() { - hash "$1" > /dev/null 2>&1 - return $? -} - -__get_remote_file() { - local remote_path=$1 - local local_path=$2 - local remote_path_suffix=$3 - - if [[ "$remote_path" != 'http'* ]]; then - cp "$remote_path" "$local_path" - return 0 - fi - - local failed=false - if __machine_has wget; then - wget --tries 10 --quiet -O "$local_path" "${remote_path}${remote_path_suffix}" || failed=true - else - failed=true - fi - - if [ "$failed" = true ] && __machine_has curl; then - failed=false - curl --retry 10 -sSL -f --create-dirs -o "$local_path" "${remote_path}${remote_path_suffix}" || failed=true - fi - - if [ "$failed" = true ]; then - __error "Download failed: $remote_path" 1>&2 - return 1 - fi -} - -# -# main -# - -command="${1:-}" -shift - -while [[ $# -gt 0 ]]; do - case $1 in - -\?|-h|--help) - __usage --no-exit - exit 0 - ;; - -c|--channel|-Channel) - shift - channel="${1:-}" - [ -z "$channel" ] && __usage - ;; - --config-file|-ConfigFile) - shift - config_file="${1:-}" - [ -z "$config_file" ] && __usage - if [ ! -f "$config_file" ]; then - __error "Invalid value for --config-file. $config_file does not exist." - exit 1 - fi - ;; - -d|--dotnet-home|-DotNetHome) - shift - DOTNET_HOME="${1:-}" - [ -z "$DOTNET_HOME" ] && __usage - ;; - --path|-Path) - shift - repo_path="${1:-}" - [ -z "$repo_path" ] && __usage - ;; - -s|--tools-source|-ToolsSource) - shift - tools_source="${1:-}" - [ -z "$tools_source" ] && __usage - ;; - --tools-source-suffix|-ToolsSourceSuffix) - shift - tools_source_suffix="${1:-}" - [ -z "$tools_source_suffix" ] && __usage - ;; - -u|--update|-Update) - update=true - ;; - --verbose|-Verbose) - verbose=true - ;; - --) - shift - break - ;; - *) - break - ;; - esac - shift -done - -if ! __machine_has unzip; then - __error 'Missing required command: unzip' - exit 1 -fi - -if ! __machine_has curl && ! __machine_has wget; then - __error 'Missing required command. Either wget or curl is required.' - exit 1 -fi - -[ -z "${config_file:-}" ] && config_file="$repo_path/korebuild.json" -if [ -f "$config_file" ]; then - if __machine_has jq ; then - if jq '.' "$config_file" >/dev/null ; then - config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")" - config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")" - else - __warn "$config_file is invalid JSON. Its settings will be ignored." - fi - elif __machine_has python ; then - if python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then - config_channel="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" - config_tools_source="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" - else - __warn "$config_file is invalid JSON. Its settings will be ignored." - fi - else - __warn 'Missing required command: jq or pyton. Could not parse the JSON file. Its settings will be ignored.' - fi - - [ ! -z "${config_channel:-}" ] && channel="$config_channel" - [ ! -z "${config_tools_source:-}" ] && tools_source="$config_tools_source" -fi - -[ -z "$channel" ] && channel='dev' -[ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' - -get_korebuild -set_korebuildsettings "$tools_source" "$DOTNET_HOME" "$repo_path" "$config_file" -invoke_korebuild_command "$command" "$@" diff --git a/samples/Mvc.Client/Mvc.Client.csproj b/samples/Mvc.Client/Mvc.Client.csproj index 1c09d535a..57121e63a 100644 --- a/samples/Mvc.Client/Mvc.Client.csproj +++ b/samples/Mvc.Client/Mvc.Client.csproj @@ -1,25 +1,12 @@  - - - net461;netcoreapp2.0 + netcoreapp3.0 + false - - - - - - - - - - - - diff --git a/samples/Mvc.Client/Startup.cs b/samples/Mvc.Client/Startup.cs index 668d2f09a..18f0c1908 100644 --- a/samples/Mvc.Client/Startup.cs +++ b/samples/Mvc.Client/Startup.cs @@ -32,7 +32,7 @@ public void ConfigureServices(IServiceCollection services) options.ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654"; options.RequireHttpsMetadata = false; - options.GetClaimsFromUserInfoEndpoint = true; + options.GetClaimsFromUserInfoEndpoint = false; options.SaveTokens = true; // Use the authorization code flow. @@ -71,7 +71,13 @@ public void Configure(IApplicationBuilder app) app.UseAuthentication(); - app.UseMvc(); + app.UseAuthorization(); + + app.UseRouting(); + + app.UseEndpoints(options => options.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}")); } } } \ No newline at end of file diff --git a/samples/Mvc.Client/web.config b/samples/Mvc.Client/web.config index 3fd33e555..5b2b1cbca 100644 --- a/samples/Mvc.Client/web.config +++ b/samples/Mvc.Client/web.config @@ -2,8 +2,13 @@ - + - + + + + + + \ No newline at end of file diff --git a/samples/Mvc.Server/Controllers/AuthorizationController.cs b/samples/Mvc.Server/Controllers/AuthorizationController.cs index 50ddc0791..4ebe0af9f 100644 --- a/samples/Mvc.Server/Controllers/AuthorizationController.cs +++ b/samples/Mvc.Server/Controllers/AuthorizationController.cs @@ -4,14 +4,14 @@ * the license and the contributors participating to this project. */ +using System; using System.Collections.Generic; -using System.Diagnostics; using System.Security.Claims; using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Primitives; +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Mvc.Server.Helpers; @@ -21,7 +21,8 @@ using OpenIddict.Abstractions; using OpenIddict.Core; using OpenIddict.EntityFrameworkCore.Models; -using OpenIddict.Server; +using OpenIddict.Server.AspNetCore; +using static OpenIddict.Abstractions.OpenIddictConstants; namespace Mvc.Server { @@ -46,11 +47,13 @@ public AuthorizationController( // you must provide your own authorization endpoint action: [Authorize, HttpGet("~/connect/authorize")] - public async Task Authorize(OpenIdConnectRequest request) + public async Task Authorize() { - Debug.Assert(request.IsAuthorizationRequest(), - "The OpenIddict binder for ASP.NET Core MVC is not registered. " + - "Make sure services.AddOpenIddict().AddServer().UseMvc() is correctly called."); + var request = HttpContext.GetOpenIddictServerRequest(); + if (request == null) + { + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + } // Retrieve the application details from the database. var application = await _applicationManager.FindByClientIdAsync(request.ClientId); @@ -58,7 +61,7 @@ public async Task Authorize(OpenIdConnectRequest request) { return View("Error", new ErrorViewModel { - Error = OpenIddictConstants.Errors.InvalidClient, + Error = Errors.InvalidClient, ErrorDescription = "Details concerning the calling client application cannot be found in the database" }); } @@ -68,18 +71,20 @@ public async Task Authorize(OpenIdConnectRequest request) return View(new AuthorizeViewModel { ApplicationName = await _applicationManager.GetDisplayNameAsync(application), - RequestId = request.RequestId, + Parameters = request.GetFlattenedParameters(), Scope = request.Scope }); } [Authorize, FormValueRequired("submit.Accept")] [HttpPost("~/connect/authorize"), ValidateAntiForgeryToken] - public async Task Accept(OpenIdConnectRequest request) + public async Task Accept() { - Debug.Assert(request.IsAuthorizationRequest(), - "The OpenIddict binder for ASP.NET Core MVC is not registered. " + - "Make sure services.AddOpenIddict().AddServer().UseMvc() is correctly called."); + var request = HttpContext.GetOpenIddictServerRequest(); + if (request == null) + { + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + } // Retrieve the profile of the logged in user. var user = await _userManager.GetUserAsync(User); @@ -87,47 +92,56 @@ public async Task Accept(OpenIdConnectRequest request) { return View("Error", new ErrorViewModel { - Error = OpenIddictConstants.Errors.ServerError, + Error = Errors.ServerError, ErrorDescription = "An internal error has occurred" }); } - // Create a new authentication ticket. - var ticket = await CreateTicketAsync(request, user); + var principal = await _signInManager.CreateUserPrincipalAsync(user); + + // Note: in this sample, the granted scopes match the requested scope + // but you may want to allow the user to uncheck specific scopes. + // For that, simply restrict the list of scopes before calling SetScopes. + principal.SetScopes(request.GetScopes()); + principal.SetResources("resource_server"); + + foreach (var claim in principal.Claims) + { + claim.SetDestinations(GetDestinations(claim, principal)); + } // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. - return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); + return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } [Authorize, FormValueRequired("submit.Deny")] [HttpPost("~/connect/authorize"), ValidateAntiForgeryToken] - public IActionResult Deny() - { - // Notify OpenIddict that the authorization grant has been denied by the resource owner - // to redirect the user agent to the client application using the appropriate response_mode. - return Forbid(OpenIddictServerDefaults.AuthenticationScheme); - } + // Notify OpenIddict that the authorization grant has been denied by the resource owner + // to redirect the user agent to the client application using the appropriate response_mode. + public IActionResult Deny() => Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); // Note: the logout action is only useful when implementing interactive // flows like the authorization code flow or the implicit flow. [HttpGet("~/connect/logout")] - public IActionResult Logout(OpenIdConnectRequest request) + public IActionResult Logout() { - Debug.Assert(request.IsLogoutRequest(), - "The OpenIddict binder for ASP.NET Core MVC is not registered. " + - "Make sure services.AddOpenIddict().AddServer().UseMvc() is correctly called."); + var request = HttpContext.GetOpenIddictServerRequest(); + if (request == null) + { + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + } // Flow the request_id to allow OpenIddict to restore // the original logout request from the distributed cache. return View(new LogoutViewModel { - RequestId = request.RequestId + Parameters = request.GetFlattenedParameters() }); } - [HttpPost("~/connect/logout"), ValidateAntiForgeryToken] - public async Task Logout() + [ActionName(nameof(Logout)), HttpPost("~/connect/logout"), ValidateAntiForgeryToken] + public async Task LogoutPost() { // Ask ASP.NET Core Identity to delete the local and external cookies created // when the user agent is redirected from the external identity provider @@ -136,7 +150,7 @@ public async Task Logout() // Returning a SignOutResult will ask OpenIddict to redirect the user agent // to the post_logout_redirect_uri specified by the client application. - return SignOut(OpenIddictServerDefaults.AuthenticationScheme); + return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } #endregion @@ -145,115 +159,105 @@ public async Task Logout() // you must provide your own token endpoint action: [HttpPost("~/connect/token"), Produces("application/json")] - public async Task Exchange(OpenIdConnectRequest request) + public async Task Exchange() { - Debug.Assert(request.IsTokenRequest(), - "The OpenIddict binder for ASP.NET Core MVC is not registered. " + - "Make sure services.AddOpenIddict().AddMvcBinders() is correctly called."); + var request = HttpContext.GetOpenIddictServerRequest(); + if (request == null) + { + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + } if (request.IsPasswordGrantType()) { var user = await _userManager.FindByNameAsync(request.Username); if (user == null) { - return BadRequest(new OpenIdConnectResponse + var properties = new AuthenticationProperties(new Dictionary { - Error = OpenIddictConstants.Errors.InvalidGrant, - ErrorDescription = "The username/password couple is invalid." + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid." }); + + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } // Validate the username/password parameters and ensure the account is not locked out. var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true); if (!result.Succeeded) { - return BadRequest(new OpenIdConnectResponse + var properties = new AuthenticationProperties(new Dictionary { - Error = OpenIddictConstants.Errors.InvalidGrant, - ErrorDescription = "The username/password couple is invalid." + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid." }); + + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } - // Create a new authentication ticket. - var ticket = await CreateTicketAsync(request, user); + var principal = await _signInManager.CreateUserPrincipalAsync(user); - return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); + // Note: in this sample, the granted scopes match the requested scope + // but you may want to allow the user to uncheck specific scopes. + // For that, simply restrict the list of scopes before calling SetScopes. + principal.SetScopes(request.GetScopes()); + principal.SetResources("resource_server"); + + foreach (var claim in principal.Claims) + { + claim.SetDestinations(GetDestinations(claim, principal)); + } + + // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. + return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } else if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType()) { // Retrieve the claims principal stored in the authorization code/refresh token. - var info = await HttpContext.AuthenticateAsync(OpenIddictServerDefaults.AuthenticationScheme); + var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal; // Retrieve the user profile corresponding to the authorization code/refresh token. // Note: if you want to automatically invalidate the authorization code/refresh token // when the user password/roles change, use the following line instead: // var user = _signInManager.ValidateSecurityStampAsync(info.Principal); - var user = await _userManager.GetUserAsync(info.Principal); + var user = await _userManager.GetUserAsync(principal); if (user == null) { - return BadRequest(new OpenIdConnectResponse + var properties = new AuthenticationProperties(new Dictionary { - Error = OpenIddictConstants.Errors.InvalidGrant, - ErrorDescription = "The token is no longer valid." + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." }); + + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } // Ensure the user is still allowed to sign in. if (!await _signInManager.CanSignInAsync(user)) { - return BadRequest(new OpenIdConnectResponse + var properties = new AuthenticationProperties(new Dictionary { - Error = OpenIddictConstants.Errors.InvalidGrant, - ErrorDescription = "The user is no longer allowed to sign in." + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." }); + + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } - // Create a new authentication ticket, but reuse the properties stored in the - // authorization code/refresh token, including the scopes originally granted. - var ticket = await CreateTicketAsync(request, user, info.Properties); + foreach (var claim in principal.Claims) + { + claim.SetDestinations(GetDestinations(claim, principal)); + } - return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); + // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. + return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } - return BadRequest(new OpenIdConnectResponse - { - Error = OpenIddictConstants.Errors.UnsupportedGrantType, - ErrorDescription = "The specified grant type is not supported." - }); + throw new InvalidOperationException("The specified grant type is not supported."); } #endregion - private async Task CreateTicketAsync( - OpenIdConnectRequest request, ApplicationUser user, - AuthenticationProperties properties = null) - { - // Create a new ClaimsPrincipal containing the claims that - // will be used to create an id_token, a token or a code. - var principal = await _signInManager.CreateUserPrincipalAsync(user); - - // Create a new authentication ticket holding the user identity. - var ticket = new AuthenticationTicket(principal, properties, - OpenIddictServerDefaults.AuthenticationScheme); - - if (!request.IsAuthorizationCodeGrantType() && !request.IsRefreshTokenGrantType()) - { - // Note: in this sample, the granted scopes match the requested scope - // but you may want to allow the user to uncheck specific scopes. - // For that, simply restrict the list of scopes before calling SetScopes. - ticket.SetScopes(request.GetScopes()); - ticket.SetResources("resource_server"); - } - - foreach (var claim in ticket.Principal.Claims) - { - claim.SetDestinations(GetDestinations(claim, ticket)); - } - - return ticket; - } - - private IEnumerable GetDestinations(Claim claim, AuthenticationTicket ticket) + private IEnumerable GetDestinations(Claim claim, ClaimsPrincipal principal) { // Note: by default, claims are NOT automatically included in the access and identity tokens. // To allow OpenIddict to serialize them, you must attach them a destination, that specifies @@ -261,27 +265,27 @@ private IEnumerable GetDestinations(Claim claim, AuthenticationTicket ti switch (claim.Type) { - case OpenIddictConstants.Claims.Name: - yield return OpenIddictConstants.Destinations.AccessToken; + case Claims.Name: + yield return Destinations.AccessToken; - if (ticket.HasScope(OpenIddictConstants.Scopes.Profile)) - yield return OpenIddictConstants.Destinations.IdentityToken; + if (principal.HasScope(Scopes.Profile)) + yield return Destinations.IdentityToken; yield break; - case OpenIddictConstants.Claims.Email: - yield return OpenIddictConstants.Destinations.AccessToken; + case Claims.Email: + yield return Destinations.AccessToken; - if (ticket.HasScope(OpenIddictConstants.Scopes.Email)) - yield return OpenIddictConstants.Destinations.IdentityToken; + if (principal.HasScope(Scopes.Email)) + yield return Destinations.IdentityToken; yield break; - case OpenIddictConstants.Claims.Role: - yield return OpenIddictConstants.Destinations.AccessToken; + case Claims.Role: + yield return Destinations.AccessToken; - if (ticket.HasScope(OpenIddictConstants.Scopes.Roles)) - yield return OpenIddictConstants.Destinations.IdentityToken; + if (principal.HasScope(Scopes.Roles)) + yield return Destinations.IdentityToken; yield break; @@ -289,7 +293,7 @@ private IEnumerable GetDestinations(Claim claim, AuthenticationTicket ti case "AspNet.Identity.SecurityStamp": yield break; default: - yield return OpenIddictConstants.Destinations.AccessToken; + yield return Destinations.AccessToken; yield break; } } diff --git a/samples/Mvc.Server/Controllers/ErrorController.cs b/samples/Mvc.Server/Controllers/ErrorController.cs index 8c24da99c..18e20424a 100644 --- a/samples/Mvc.Server/Controllers/ErrorController.cs +++ b/samples/Mvc.Server/Controllers/ErrorController.cs @@ -4,7 +4,7 @@ * the license and the contributors participating to this project. */ -using AspNet.Security.OpenIdConnect.Primitives; +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Mvc; using Mvc.Server.ViewModels.Shared; @@ -13,10 +13,11 @@ namespace Mvc.Server public class ErrorController : Controller { [HttpGet, HttpPost, Route("~/error")] - public IActionResult Error(OpenIdConnectResponse response) + public IActionResult Error() { // If the error was not caused by an invalid // OIDC request, display a generic error page. + var response = HttpContext.GetOpenIddictServerResponse(); if (response == null) { return View(new ErrorViewModel()); diff --git a/samples/Mvc.Server/Controllers/ResourceController.cs b/samples/Mvc.Server/Controllers/ResourceController.cs index 3bfa2b466..10d8353e7 100644 --- a/samples/Mvc.Server/Controllers/ResourceController.cs +++ b/samples/Mvc.Server/Controllers/ResourceController.cs @@ -1,9 +1,7 @@ using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Mvc.Server.Models; -using OpenIddict.Validation; namespace Mvc.Server.Controllers { @@ -17,7 +15,7 @@ public ResourceController(UserManager userManager) _userManager = userManager; } - [Authorize(AuthenticationSchemes = OpenIddictValidationDefaults.AuthenticationScheme)] + //[Authorize(AuthenticationSchemes = OpenIddictValidationDefaults.AuthenticationScheme)] [HttpGet("message")] public async Task GetMessage() { diff --git a/samples/Mvc.Server/Controllers/UserinfoController.cs b/samples/Mvc.Server/Controllers/UserinfoController.cs index 576d0d632..b4e0a15b7 100644 --- a/samples/Mvc.Server/Controllers/UserinfoController.cs +++ b/samples/Mvc.Server/Controllers/UserinfoController.cs @@ -1,12 +1,9 @@ using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Primitives; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Mvc.Server.Models; using Newtonsoft.Json.Linq; using OpenIddict.Abstractions; -using OpenIddict.Validation; namespace Mvc.Server.Controllers { @@ -22,18 +19,14 @@ public UserinfoController(UserManager userManager) // // GET: /api/userinfo - [Authorize(AuthenticationSchemes = OpenIddictValidationDefaults.AuthenticationScheme)] + //[Authorize(AuthenticationSchemes = OpenIddictValidationDefaults.AuthenticationScheme)] [HttpGet("userinfo"), Produces("application/json")] public async Task Userinfo() { var user = await _userManager.GetUserAsync(User); if (user == null) { - return BadRequest(new OpenIdConnectResponse - { - Error = OpenIddictConstants.Errors.InvalidGrant, - ErrorDescription = "The user profile is no longer available." - }); + return Challenge(); } var claims = new JObject(); @@ -53,9 +46,9 @@ public async Task Userinfo() claims[OpenIddictConstants.Claims.PhoneNumberVerified] = await _userManager.IsPhoneNumberConfirmedAsync(user); } - if (User.HasClaim(OpenIddictConstants.Claims.Scope, OpenIddictConstants.Scopes.Roles)) + if (User.HasClaim(OpenIddictConstants.Claims.Scope, "roles")) { - claims[OpenIddictConstants.Claims.Roles] = JArray.FromObject(await _userManager.GetRolesAsync(user)); + claims["roles"] = JArray.FromObject(await _userManager.GetRolesAsync(user)); } // Note: the complete list of standard claims supported by the OpenID Connect specification diff --git a/samples/Mvc.Server/Mvc.Server.csproj b/samples/Mvc.Server/Mvc.Server.csproj index ed22a33b4..1ec7f82b2 100644 --- a/samples/Mvc.Server/Mvc.Server.csproj +++ b/samples/Mvc.Server/Mvc.Server.csproj @@ -1,12 +1,8 @@  - - - - net461;netcoreapp2.0 - - + netcoreapp3.0 + false false @@ -15,28 +11,13 @@ - + - - - - - - - - - - - - - - - - + diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 0447f8aec..f4f020662 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -16,9 +17,7 @@ namespace Mvc.Server public class Startup { public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + => Configuration = configuration; public IConfiguration Configuration { get; } @@ -52,19 +51,6 @@ public void ConfigureServices(IServiceCollection services) options.ClaimsIdentity.RoleClaimType = OpenIddictConstants.Claims.Role; }); - services.AddAuthentication() - .AddGoogle(options => - { - options.ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com"; - options.ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f"; - }) - - .AddTwitter(options => - { - options.ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g"; - options.ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI"; - }); - services.AddOpenIddict() // Register the OpenIddict core services. @@ -78,16 +64,11 @@ public void ConfigureServices(IServiceCollection services) // Register the OpenIddict server handler. .AddServer(options => { - // Register the ASP.NET Core MVC services used by OpenIddict. - // Note: if you don't call this method, you won't be able to - // bind OpenIdConnectRequest or OpenIdConnectResponse parameters. - options.UseMvc(); - // Enable the authorization, logout, token and userinfo endpoints. - options.EnableAuthorizationEndpoint("/connect/authorize") - .EnableLogoutEndpoint("/connect/logout") - .EnableTokenEndpoint("/connect/token") - .EnableUserinfoEndpoint("/api/userinfo"); + options.SetAuthorizationEndpointUris("/connect/authorize") + .SetLogoutEndpointUris("/connect/logout") + .SetTokenEndpointUris("/connect/token") + .SetUserinfoEndpointUris("/connect/userinfo"); // Note: the Mvc.Client sample only uses the code flow and the password flow, but you // can enable the other flows if you need to support implicit or client credentials. @@ -100,21 +81,17 @@ public void ConfigureServices(IServiceCollection services) OpenIddictConstants.Scopes.Profile, OpenIddictConstants.Scopes.Roles); - // When request caching is enabled, authorization and logout requests - // are stored in the distributed cache by OpenIddict and the user agent - // is redirected to the same page with a single parameter (request_id). - // This allows flowing large OpenID Connect requests even when using - // an external authentication provider like Google, Facebook or Twitter. - options.EnableRequestCaching(); + // Register the signing and encryption credentials. + options.AddDevelopmentEncryptionCertificate() + .AddDevelopmentSigningCertificate(); - // During development, you can disable the HTTPS requirement. - options.DisableHttpsRequirement(); - - // Note: to use JWT access tokens instead of the default - // encrypted format, the following lines are required: - // - // options.UseJsonWebTokens(); - // options.AddEphemeralSigningKey(); + // Register the ASP.NET Core host and configure the ASP.NET Core-specific options. + options.UseAspNetCore() + .EnableStatusCodePagesIntegration() + .EnableAuthorizationEndpointPassthrough() + .EnableTokenEndpointPassthrough() + .EnableUserinfoEndpointPassthrough() + .DisableTransportSecurityRequirement(); // During development, you can disable the HTTPS requirement. // Note: if you don't want to specify a client_id when sending // a token or revocation request, uncomment the following line: @@ -132,13 +109,7 @@ public void ConfigureServices(IServiceCollection services) // options.IgnoreEndpointPermissions() // .IgnoreGrantTypePermissions() // .IgnoreScopePermissions(); - }) - - // Register the OpenIddict validation handler. - // Note: the OpenIddict validation handler is only compatible with the - // default token format or with reference tokens and cannot be used with - // JWT tokens. For JWT tokens, use the Microsoft JWT bearer handler. - .AddValidation(); + }); services.AddTransient(); services.AddTransient(); @@ -152,9 +123,15 @@ public void Configure(IApplicationBuilder app) app.UseStatusCodePagesWithReExecute("/error"); + app.UseRouting(); + app.UseAuthentication(); - app.UseMvcWithDefaultRoute(); + app.UseAuthorization(); + + app.UseEndpoints(options => options.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}")); // Seed the database with the sample applications. // Note: in a real world application, this step should be part of a setup script. @@ -164,67 +141,67 @@ public void Configure(IApplicationBuilder app) private async Task InitializeAsync(IServiceProvider services) { // Create a new service scope to ensure the database context is correctly disposed when this methods returns. - using (var scope = services.GetRequiredService().CreateScope()) - { - var context = scope.ServiceProvider.GetRequiredService(); - await context.Database.EnsureCreatedAsync(); + using var scope = services.GetRequiredService().CreateScope(); - var manager = scope.ServiceProvider.GetRequiredService>(); + var context = scope.ServiceProvider.GetRequiredService(); + await context.Database.EnsureCreatedAsync(); - if (await manager.FindByClientIdAsync("mvc") == null) + var manager = scope.ServiceProvider.GetRequiredService>(); + + if (await manager.FindByClientIdAsync("mvc") == null) + { + var descriptor = new OpenIddictApplicationDescriptor { - var descriptor = new OpenIddictApplicationDescriptor + ClientId = "mvc", + ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", + DisplayName = "MVC client application", + PostLogoutRedirectUris = { new Uri("http://localhost:53507/signout-callback-oidc") }, + RedirectUris = { new Uri("http://localhost:53507/signin-oidc") }, + Permissions = { - ClientId = "mvc", - ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", - DisplayName = "MVC client application", - PostLogoutRedirectUris = { new Uri("http://localhost:53507/signout-callback-oidc") }, - RedirectUris = { new Uri("http://localhost:53507/signin-oidc") }, - Permissions = - { - OpenIddictConstants.Permissions.Endpoints.Authorization, - OpenIddictConstants.Permissions.Endpoints.Logout, - OpenIddictConstants.Permissions.Endpoints.Token, - OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, - OpenIddictConstants.Permissions.GrantTypes.RefreshToken, - OpenIddictConstants.Permissions.Scopes.Email, - OpenIddictConstants.Permissions.Scopes.Profile, - OpenIddictConstants.Permissions.Scopes.Roles - } - }; - - await manager.CreateAsync(descriptor); - } - - // To test this sample with Postman, use the following settings: - // - // * Authorization URL: http://localhost:54540/connect/authorize - // * Access token URL: http://localhost:54540/connect/token - // * Client ID: postman - // * Client secret: [blank] (not used with public clients) - // * Scope: openid email profile roles - // * Grant type: authorization code - // * Request access token locally: yes - if (await manager.FindByClientIdAsync("postman") == null) + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Logout, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, + OpenIddictConstants.Permissions.Scopes.Email, + OpenIddictConstants.Permissions.Scopes.Profile, + OpenIddictConstants.Permissions.Scopes.Roles + } + }; + + await manager.CreateAsync(descriptor); + } + + // To test this sample with Postman, use the following settings: + // + // * Authorization URL: http://localhost:54540/connect/authorize + // * Access token URL: http://localhost:54540/connect/token + // * Client ID: postman + // * Client secret: [blank] (not used with public clients) + // * Scope: openid email profile roles + // * Grant type: authorization code + // * Request access token locally: yes + if (await manager.FindByClientIdAsync("postman") == null) + { + var descriptor = new OpenIddictApplicationDescriptor { - var descriptor = new OpenIddictApplicationDescriptor + ClientId = "postman", + DisplayName = "Postman", + RedirectUris = { new Uri("https://www.getpostman.com/oauth2/callback") }, + Permissions = { - ClientId = "postman", - DisplayName = "Postman", - RedirectUris = { new Uri("https://www.getpostman.com/oauth2/callback") }, - Permissions = - { - OpenIddictConstants.Permissions.Endpoints.Authorization, - OpenIddictConstants.Permissions.Endpoints.Token, - OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, - OpenIddictConstants.Permissions.Scopes.Email, - OpenIddictConstants.Permissions.Scopes.Profile, - OpenIddictConstants.Permissions.Scopes.Roles - } - }; - - await manager.CreateAsync(descriptor); - } + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.GrantTypes.Password, + OpenIddictConstants.Permissions.Scopes.Email, + OpenIddictConstants.Permissions.Scopes.Profile, + OpenIddictConstants.Permissions.Scopes.Roles + } + }; + + await manager.CreateAsync(descriptor); } } } diff --git a/samples/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs b/samples/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs index 35561ed71..e708ec48c 100644 --- a/samples/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs +++ b/samples/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Mvc.Server.ViewModels.Authorization @@ -9,7 +10,7 @@ public class AuthorizeViewModel public string ApplicationName { get; set; } [BindNever] - public string RequestId { get; set; } + public IEnumerable> Parameters { get; set; } [Display(Name = "Scope")] public string Scope { get; set; } diff --git a/samples/Mvc.Server/ViewModels/Authorization/LogoutViewModel.cs b/samples/Mvc.Server/ViewModels/Authorization/LogoutViewModel.cs index 3a9a96617..5777b155c 100644 --- a/samples/Mvc.Server/ViewModels/Authorization/LogoutViewModel.cs +++ b/samples/Mvc.Server/ViewModels/Authorization/LogoutViewModel.cs @@ -1,10 +1,11 @@ -using Microsoft.AspNetCore.Mvc.ModelBinding; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Mvc.Server.ViewModels.Authorization { public class LogoutViewModel { [BindNever] - public string RequestId { get; set; } + public IEnumerable> Parameters { get; set; } } } diff --git a/samples/Mvc.Server/Views/Account/Login.cshtml b/samples/Mvc.Server/Views/Account/Login.cshtml index f560d1cb3..cfbc37929 100644 --- a/samples/Mvc.Server/Views/Account/Login.cshtml +++ b/samples/Mvc.Server/Views/Account/Login.cshtml @@ -1,6 +1,5 @@ @using System.Collections.Generic @using Microsoft.AspNetCore.Http -@using Microsoft.AspNetCore.Http.Authentication @model LoginViewModel @inject SignInManager SignInManager diff --git a/samples/Mvc.Server/Views/Authorization/Authorize.cshtml b/samples/Mvc.Server/Views/Authorization/Authorize.cshtml index d9bee489d..7d07c4c2a 100644 --- a/samples/Mvc.Server/Views/Authorization/Authorize.cshtml +++ b/samples/Mvc.Server/Views/Authorization/Authorize.cshtml @@ -5,10 +5,11 @@

Do you want to grant @Model.ApplicationName access to your data? (scopes requested: @Model.Scope)

-
- @Html.AntiForgeryToken() - - + + @foreach (var parameter in Model.Parameters) + { + + } diff --git a/samples/Mvc.Server/Views/Authorization/Logout.cshtml b/samples/Mvc.Server/Views/Authorization/Logout.cshtml index 9848315d6..3e994b29b 100644 --- a/samples/Mvc.Server/Views/Authorization/Logout.cshtml +++ b/samples/Mvc.Server/Views/Authorization/Logout.cshtml @@ -5,7 +5,10 @@

Are you sure you want to sign out?

- + @foreach (var parameter in Model.Parameters) + { + + }
diff --git a/samples/Mvc.Server/Views/Manage/ManageLogins.cshtml b/samples/Mvc.Server/Views/Manage/ManageLogins.cshtml index 98ebfd342..5d9d56b33 100644 --- a/samples/Mvc.Server/Views/Manage/ManageLogins.cshtml +++ b/samples/Mvc.Server/Views/Manage/ManageLogins.cshtml @@ -1,5 +1,4 @@ @model ManageLoginsViewModel -@using Microsoft.AspNetCore.Http.Authentication @{ ViewData["Title"] = "Manage your external logins"; } diff --git a/samples/Mvc.Server/web.config b/samples/Mvc.Server/web.config index 3fd33e555..5b2b1cbca 100644 --- a/samples/Mvc.Server/web.config +++ b/samples/Mvc.Server/web.config @@ -2,8 +2,13 @@ - + - + + + + + + \ No newline at end of file diff --git a/shared/OpenIddict.Extensions/OpenIddict.Extensions.csproj b/shared/OpenIddict.Extensions/OpenIddict.Extensions.csproj index 0f2e59bf5..e57111d9e 100644 --- a/shared/OpenIddict.Extensions/OpenIddict.Extensions.csproj +++ b/shared/OpenIddict.Extensions/OpenIddict.Extensions.csproj @@ -1,7 +1,5 @@  - - netstandard2.0 false diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs index 62247fd69..44cc33539 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictAuthorizationDescriptor.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Security.Claims; namespace OpenIddict.Abstractions { @@ -15,17 +14,10 @@ public class OpenIddictAuthorizationDescriptor public string ApplicationId { get; set; } /// - /// Gets or sets the optional principal associated with the authorization. + /// Gets the claims associated with the authorization. /// Note: this property is not stored by the default authorization stores. /// - public ClaimsPrincipal Principal { get; set; } - - /// - /// Gets the optional authentication properties associated with the authorization. - /// Note: this property is not stored by the default authorization stores. - /// - public IDictionary Properties { get; } = - new Dictionary(StringComparer.Ordinal); + public IDictionary Claims { get; } = new Dictionary(StringComparer.Ordinal); /// /// Gets the scopes associated with the authorization. diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs index 2367b029c..91ee6d577 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs @@ -2,7 +2,6 @@ using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -45,20 +44,18 @@ Task CountAsync( /// /// Creates a new permanent authorization based on the specified parameters. /// - /// The principal associated with the authorization. + /// The claims associated with the authorization. /// The subject associated with the authorization. /// The client associated with the authorization. /// The authorization type. /// The minimal scopes associated with the authorization. - /// The authentication properties associated with the authorization. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. /// Task CreateAsync( - [NotNull] ClaimsPrincipal principal, [NotNull] string subject, [NotNull] string client, - [NotNull] string type, ImmutableArray scopes, - [CanBeNull] ImmutableDictionary properties, CancellationToken cancellationToken = default); + [NotNull] ImmutableDictionary claims, [NotNull] string subject, [NotNull] string client, + [NotNull] string type, ImmutableArray scopes, CancellationToken cancellationToken = default); /// /// Creates a new authorization based on the specified descriptor. diff --git a/src/OpenIddict.Abstractions/OpenIddict.Abstractions.csproj b/src/OpenIddict.Abstractions/OpenIddict.Abstractions.csproj index afbe268d7..6ea13d670 100644 --- a/src/OpenIddict.Abstractions/OpenIddict.Abstractions.csproj +++ b/src/OpenIddict.Abstractions/OpenIddict.Abstractions.csproj @@ -1,23 +1,24 @@ - - - + - netstandard2.0 + netstandard2.0;netstandard2.1 OpenIddict's abstractions. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security - + + + + + + diff --git a/src/OpenIddict.Abstractions/OpenIddictBuilder.cs b/src/OpenIddict.Abstractions/OpenIddictBuilder.cs index 7fea9e496..c3bc62e1d 100644 --- a/src/OpenIddict.Abstractions/OpenIddictBuilder.cs +++ b/src/OpenIddict.Abstractions/OpenIddictBuilder.cs @@ -20,14 +20,7 @@ public class OpenIddictBuilder /// /// The services collection. public OpenIddictBuilder([NotNull] IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - Services = services; - } + => Services = services ?? throw new ArgumentNullException(nameof(services)); /// /// Gets the services collection. diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index 9b73b2ba1..64b22e1c9 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -14,6 +14,22 @@ public static class AuthorizationTypes public const string Permanent = "permanent"; } + public static class Algorithms + { + public const string EcdsaSha256 = "ES256"; + public const string EcdsaSha384 = "ES384"; + public const string EcdsaSha512 = "ES512"; + public const string HmacSha256 = "HS256"; + public const string HmacSha384 = "HS384"; + public const string HmacSha512 = "HS512"; + public const string RsaSha256 = "RS256"; + public const string RsaSha384 = "RS384"; + public const string RsaSha512 = "RS512"; + public const string RsaSsaPssSha256 = "PS256"; + public const string RsaSsaPssSha384 = "PS384"; + public const string RsaSsaPssSha512 = "PS512"; + } + public static class Claims { public const string AccessTokenHash = "at_hash"; @@ -27,7 +43,6 @@ public static class Claims public const string Birthdate = "birthdate"; public const string ClientId = "client_id"; public const string CodeHash = "c_hash"; - public const string ConfidentialityLevel = "cfd_lvl"; public const string Country = "country"; public const string Email = "email"; public const string EmailVerified = "email_verified"; @@ -54,17 +69,40 @@ public static class Claims public const string PreferredUsername = "preferred_username"; public const string Profile = "profile"; public const string Region = "region"; + public const string Resource = "resource"; public const string Role = "role"; - public const string Roles = "roles"; public const string Scope = "scope"; public const string StreetAddress = "street_address"; public const string Subject = "sub"; public const string TokenType = "token_type"; - public const string TokenUsage = "token_usage"; public const string UpdatedAt = "updated_at"; public const string Username = "username"; public const string Website = "website"; public const string Zoneinfo = "zoneinfo"; + + public static class Prefixes + { + public const string Private = "oi_"; + } + + public static class Private + { + public const string AccessTokenLifetime = "oi_act_lft"; + public const string AuthorizationCodeLifetime = "oi_auc_lft"; + public const string ClaimDestinations = "oi_cl_dstn"; + public const string CodeChallenge = "oi_cd_chlg"; + public const string CodeChallengeMethod = "oi_cd_chlg_meth"; + public const string IdentityTokenLifetime = "oi_idt_lft"; + public const string OriginalRedirectUri = "oi_reduri"; + public const string RefreshTokenLifetime = "oi_reft_lft"; + public const string TokenUsage = "oi_tkn_use"; + } + } + + public static class ClientAuthenticationMethods + { + public const string ClientSecretBasic = "client_secret_basic"; + public const string ClientSecretPost = "client_secret_post"; } public static class ClientTypes @@ -74,6 +112,12 @@ public static class ClientTypes public const string Public = "public"; } + public static class CodeChallengeMethods + { + public const string Plain = "plain"; + public const string Sha256 = "S256"; + } + public static class ConsentTypes { public const string Explicit = "explicit"; @@ -87,12 +131,6 @@ public static class Destinations public const string IdentityToken = "id_token"; } - public static class Environment - { - public const string AuthorizationRequest = "openiddict-authorization-request:"; - public const string LogoutRequest = "openiddict-logout-request:"; - } - public static class Errors { public const string AccessDenied = "access_denied"; @@ -272,20 +310,29 @@ public static class Prompts public static class Properties { - public const string AuthenticationTicket = ".authentication_ticket"; + public const string AccessTokenLifetime = ".access_token_lifetime"; + public const string AuthorizationCodeLifetime = ".authorization_code_lifetime"; + public const string Audiences = ".audiences"; + public const string CodeChallenge = ".code_challenge"; + public const string CodeChallengeMethod = ".code_challenge_method"; + public const string Destinations = ".destinations"; public const string Error = ".error"; public const string ErrorDescription = ".error_description"; public const string ErrorUri = ".error_uri"; - public const string InternalAuthorizationId = ".internal_authorization_id"; - public const string InternalTokenId = ".internal_token_id"; - } - - public static class PropertyTypes - { - public const string Boolean = "#public_boolean"; - public const string Integer = "#public_integer"; - public const string Json = "#public_json"; - public const string String = "#public_string"; + public const string Expires = ".expires"; + public const string IdentityTokenLifetime = ".identity_token_lifetime"; + public const string Issued = ".issued"; + public const string Nonce = ".nonce"; + public const string OriginalPrincipal = ".original_principal"; + public const string OriginalRedirectUri = ".original_redirect_uri"; + public const string PostLogoutRedirectUri = ".post_logout_redirect_uri"; + public const string Presenters = ".presenters"; + public const string RefreshTokenLifetime = ".refresh_token_lifetime"; + public const string Resources = ".resources"; + public const string Scopes = ".scopes"; + public const string TokenId = ".token_id"; + public const string TokenUsage = ".token_usage"; + public const string ValidatedRedirectUri = ".validated_redirect_uri"; } public static class ResponseModes @@ -305,7 +352,14 @@ public static class ResponseTypes public static class Separators { - public const string Space = " "; + public static readonly char[] Ampersand = { '&' }; + public static readonly char[] Space = { ' ' }; + } + + public static class Schemes + { + public const string Basic = "Basic"; + public const string Bearer = "Bearer"; } public static class Scopes @@ -326,7 +380,26 @@ public static class Statuses public const string Valid = "valid"; } + public static class SubjectTypes + { + public const string Pairwise = "pairwise"; + public const string Public = "public"; + } + + public static class TokenTypeHints + { + public const string AccessToken = "access_token"; + public const string AuthorizationCode = "authorization_code"; + public const string IdToken = "id_token"; + public const string RefreshToken = "refresh_token"; + } + public static class TokenTypes + { + public const string Bearer = "Bearer"; + } + + public static class TokenUsages { public const string AccessToken = "access_token"; public const string AuthorizationCode = "authorization_code"; diff --git a/src/OpenIddict.Abstractions/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/OpenIddictExtensions.cs index 6173a38e5..8e75e14d8 100644 --- a/src/OpenIddict.Abstractions/OpenIddictExtensions.cs +++ b/src/OpenIddict.Abstractions/OpenIddictExtensions.cs @@ -36,7 +36,7 @@ public static OpenIddictBuilder AddOpenIddict([NotNull] this IServiceCollection /// The services collection. /// The configuration delegate used to register new services. /// This extension can be safely called multiple times. - /// The . + /// The . public static IServiceCollection AddOpenIddict( [NotNull] this IServiceCollection services, [NotNull] Action configuration) diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictConverter.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictConverter.cs new file mode 100644 index 000000000..cafbb8747 --- /dev/null +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictConverter.cs @@ -0,0 +1,142 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace OpenIddict.Abstractions +{ + /// + /// Represents a JSON.NET converter able to convert OpenIddict primitives. + /// + public class OpenIddictConverter : JsonConverter + { + /// + /// Determines whether the specified type is supported by this converter. + /// + /// The type to convert. + /// true if the type is supported, false otherwise. + public override bool CanConvert([NotNull] Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + return typeof(OpenIddictMessage).IsAssignableFrom(type); + } + + /// + /// Deserializes an instance. + /// + /// The JSON reader. + /// The type of the deserialized instance. + /// The existing , if applicable. + /// The JSON serializer. + /// The deserialized instance. + public override object ReadJson( + [NotNull] JsonReader reader, [NotNull] Type type, + [CanBeNull] object value, [CanBeNull] JsonSerializer serializer) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + // Note: OpenIddict primitives are always represented as JSON objects. + var payload = JToken.Load(reader) as JObject; + if (payload == null) + { + throw new JsonSerializationException("An error occurred while reading the JSON payload."); + } + + // If no existing value was specified, instantiate a + // new request/response depending on the requested type. + var message = value as OpenIddictMessage; + if (message == null) + { + if (type == typeof(OpenIddictMessage)) + { + message = new OpenIddictMessage(); + } + + else if (type == typeof(OpenIddictRequest)) + { + message = new OpenIddictRequest(); + } + + else if (type == typeof(OpenIddictResponse)) + { + message = new OpenIddictResponse(); + } + } + + if (message != null) + { + foreach (var parameter in payload.Properties()) + { + message.AddParameter(parameter.Name, parameter.Value); + } + + return message; + } + + throw new ArgumentException("The specified type is not supported.", nameof(type)); + } + + /// + /// Serializes an OpenIddict primitive. + /// + /// The JSON writer. + /// The instance. + /// The JSON serializer. + public override void WriteJson([NotNull] JsonWriter writer, [NotNull] object value, [CanBeNull] JsonSerializer serializer) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (value is OpenIddictMessage message) + { + writer.WriteStartObject(); + + foreach (var parameter in message.GetParameters()) + { + writer.WritePropertyName(parameter.Key); + + var token = (JToken) parameter.Value; + if (token == null) + { + writer.WriteNull(); + + continue; + } + + token.WriteTo(writer); + } + + writer.WriteEndObject(); + + return; + } + + throw new ArgumentException("The specified object is not supported.", nameof(value)); + } + } +} diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs new file mode 100644 index 000000000..b9ef95537 --- /dev/null +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs @@ -0,0 +1,1772 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Security.Claims; +using JetBrains.Annotations; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace OpenIddict.Abstractions +{ + /// + /// Provides extension methods to make + /// and easier to work with. + /// + public static class OpenIddictExtensions + { + /// + /// Gets all the parameters associated with the specified message as a flattened collection: + /// array parameters are automatically converted to multiple parameters and parameters that + /// can't be converted to string instances are ignored and excluded from the returned collection. + /// This extension is primarily intended to be used by components that need to represent + /// an OpenID Connect message as a query string or as a list of key/value pairs in a HTTP form. + /// + /// The instance. + /// The parameters, as a flattened collection. + public static ImmutableList> GetFlattenedParameters([NotNull] this OpenIddictMessage message) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + var parameters = ImmutableList.CreateBuilder>(); + + foreach (var parameter in message.GetParameters()) + { + var values = (string[]) parameter.Value; + if (values == null) + { + continue; + } + + foreach (var value in values) + { + parameters.Add(new KeyValuePair(parameter.Key, value)); + } + } + + return parameters.ToImmutable(); + } + + /// + /// Extracts the authentication context class values from an . + /// + /// The instance. + public static ImmutableHashSet GetAcrValues([NotNull] this OpenIddictRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(request.AcrValues)) + { + return ImmutableHashSet.Create(StringComparer.Ordinal); + } + + return ImmutableHashSet.CreateRange(StringComparer.Ordinal, GetValues(request.AcrValues, Separators.Space)); + } + + /// + /// Extracts the scopes from an . + /// + /// The instance. + public static ImmutableHashSet GetScopes([NotNull] this OpenIddictRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(request.Scope)) + { + return ImmutableHashSet.Create(StringComparer.Ordinal); + } + + return ImmutableHashSet.CreateRange(StringComparer.Ordinal, GetValues(request.Scope, Separators.Space)); + } + + /// + /// Determines whether the requested authentication context class values contain the specified item. + /// + /// The instance. + /// The component to look for in the parameter. + public static bool HasAcrValue([NotNull] this OpenIddictRequest request, [NotNull] string value) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException("The value cannot be null or empty.", nameof(value)); + } + + if (string.IsNullOrEmpty(request.AcrValues)) + { + return false; + } + + return HasValue(request.AcrValues, value, Separators.Space); + } + + /// + /// Determines whether the requested prompt contains the specified value. + /// + /// The instance. + /// The component to look for in the parameter. + public static bool HasPrompt([NotNull] this OpenIddictRequest request, [NotNull] string prompt) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(prompt)) + { + throw new ArgumentException("The prompt cannot be null or empty.", nameof(prompt)); + } + + if (string.IsNullOrEmpty(request.Prompt)) + { + return false; + } + + return HasValue(request.Prompt, prompt, Separators.Space); + } + + /// + /// Determines whether the requested response type contains the specified value. + /// + /// The instance. + /// The component to look for in the parameter. + public static bool HasResponseType([NotNull] this OpenIddictRequest request, [NotNull] string type) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The response type cannot be null or empty.", nameof(type)); + } + + if (string.IsNullOrEmpty(request.ResponseType)) + { + return false; + } + + return HasValue(request.ResponseType, type, Separators.Space); + } + + /// + /// Determines whether the requested scope contains the specified value. + /// + /// The instance. + /// The component to look for in the parameter. + public static bool HasScope([NotNull] this OpenIddictRequest request, [NotNull] string scope) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(scope)) + { + throw new ArgumentException("The scope cannot be null or empty.", nameof(scope)); + } + + if (string.IsNullOrEmpty(request.Scope)) + { + return false; + } + + return HasValue(request.Scope, scope, Separators.Space); + } + + /// + /// Determines whether the "response_type" parameter corresponds to the "none" response type. + /// See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#none for more information. + /// + /// The instance. + /// true if the request is a response_type=none request, false otherwise. + public static bool IsNoneFlow([NotNull] this OpenIddictRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(request.ResponseType)) + { + return false; + } + + var segment = Trim(new StringSegment(request.ResponseType), Separators.Space); + if (segment.Length == 0) + { + return false; + } + + return segment.Equals(ResponseTypes.None, StringComparison.Ordinal); + } + + /// + /// Determines whether the "response_type" parameter corresponds to the authorization code flow. + /// See http://tools.ietf.org/html/rfc6749#section-4.1.1 for more information. + /// + /// The instance. + /// true if the request is a code flow request, false otherwise. + public static bool IsAuthorizationCodeFlow([NotNull] this OpenIddictRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(request.ResponseType)) + { + return false; + } + + var segment = Trim(new StringSegment(request.ResponseType), Separators.Space); + if (segment.Length == 0) + { + return false; + } + + return segment.Equals(ResponseTypes.Code, StringComparison.Ordinal); + } + + /// + /// Determines whether the "response_type" parameter corresponds to the implicit flow. + /// See http://tools.ietf.org/html/rfc6749#section-4.2.1 and + /// http://openid.net/specs/openid-connect-core-1_0.html for more information + /// + /// The instance. + /// true if the request is an implicit flow request, false otherwise. + public static bool IsImplicitFlow([NotNull] this OpenIddictRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(request.ResponseType)) + { + return false; + } + + var flags = /* none: */ 0x00; + + foreach (var element in new StringTokenizer(request.ResponseType, Separators.Space)) + { + var segment = Trim(element, Separators.Space); + if (segment.Length == 0) + { + continue; + } + + if (segment.Equals(ResponseTypes.IdToken, StringComparison.Ordinal)) + { + flags |= /* id_token: */ 0x01; + + continue; + } + + // Note: though the OIDC core specs does not include the OAuth 2.0-inherited response_type=token, + // it is considered as a valid response_type for the implicit flow for backward compatibility. + else if (segment.Equals(ResponseTypes.Token, StringComparison.Ordinal)) + { + flags |= /* token */ 0x02; + + continue; + } + + // Always return false if the response_type item + // is not a valid component for the implicit flow. + return false; + } + + // Return true if the response_type parameter contains "id_token" or "token". + return (flags & /* id_token: */ 0x01) == 0x01 || (flags & /* token: */ 0x02) == 0x02; + } + + /// + /// Determines whether the "response_type" parameter corresponds to the hybrid flow. + /// See http://tools.ietf.org/html/rfc6749#section-4.2.1 and + /// http://openid.net/specs/openid-connect-core-1_0.html for more information. + /// + /// The instance. + /// true if the request is an hybrid flow request, false otherwise. + public static bool IsHybridFlow([NotNull] this OpenIddictRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(request.ResponseType)) + { + return false; + } + + var flags = /* none */ 0x00; + + foreach (var element in new StringTokenizer(request.ResponseType, Separators.Space)) + { + var segment = Trim(element, Separators.Space); + if (segment.Length == 0) + { + continue; + } + + if (segment.Equals(ResponseTypes.Code, StringComparison.Ordinal)) + { + flags |= /* code: */ 0x01; + + continue; + } + + else if (segment.Equals(ResponseTypes.IdToken, StringComparison.Ordinal)) + { + flags |= /* id_token: */ 0x02; + + continue; + } + + else if (segment.Equals(ResponseTypes.Token, StringComparison.Ordinal)) + { + flags |= /* token: */ 0x04; + + continue; + } + + // Always return false if the response_type item + // is not a valid component for the hybrid flow. + return false; + } + + // Return false if the response_type parameter doesn't contain "code". + if ((flags & /* code: */ 0x01) != 0x01) + { + return false; + } + + // Return true if the response_type parameter contains "id_token" or "token". + return (flags & /* id_token: */ 0x02) == 0x02 || (flags & /* token: */ 0x04) == 0x04; + } + + /// + /// Determines whether the "response_mode" parameter corresponds to the fragment response mode. + /// See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html for more information. + /// + /// The instance. + /// + /// true if the request specified the fragment response mode or if + /// it's the default value for the requested flow, false otherwise. + /// + public static bool IsFragmentResponseMode([NotNull] this OpenIddictRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.Equals(request.ResponseMode, ResponseModes.Fragment, StringComparison.Ordinal)) + { + return true; + } + + // Don't guess the response_mode value + // if an explicit value has been provided. + if (!string.IsNullOrEmpty(request.ResponseMode)) + { + return false; + } + + // Both the implicit and the hybrid flows + // use response_mode=fragment by default. + return request.IsImplicitFlow() || request.IsHybridFlow(); + } + + /// + /// Determines whether the "response_mode" parameter corresponds to the query response mode. + /// See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html for more information. + /// + /// The instance. + /// + /// true if the request specified the query response mode or if + /// it's the default value for the requested flow, false otherwise. + /// + public static bool IsQueryResponseMode([NotNull] this OpenIddictRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.Equals(request.ResponseMode, ResponseModes.Query, StringComparison.Ordinal)) + { + return true; + } + + // Don't guess the response_mode value + // if an explicit value has been provided. + if (!string.IsNullOrEmpty(request.ResponseMode)) + { + return false; + } + + // Code flow and "response_type=none" use response_mode=query by default. + return request.IsAuthorizationCodeFlow() || request.IsNoneFlow(); + } + + /// + /// Determines whether the "response_mode" parameter corresponds to the form post response mode. + /// See http://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html for more information. + /// + /// The instance. + /// + /// true if the request specified the form post response mode or if + /// it's the default value for the requested flow, false otherwise. + /// + public static bool IsFormPostResponseMode([NotNull] this OpenIddictRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + return string.Equals(request.ResponseMode, ResponseModes.FormPost, StringComparison.Ordinal); + } + + /// + /// Determines whether the "grant_type" parameter corresponds to the authorization code grant. + /// See http://tools.ietf.org/html/rfc6749#section-4.1.3 for more information. + /// + /// The instance. + /// true if the request is a code grant request, false otherwise. + public static bool IsAuthorizationCodeGrantType([NotNull] this OpenIddictRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + return string.Equals(request.GrantType, GrantTypes.AuthorizationCode, StringComparison.Ordinal); + } + + /// + /// Determines whether the "grant_type" parameter corresponds to the client credentials grant. + /// See http://tools.ietf.org/html/rfc6749#section-4.4.2 for more information. + /// + /// The instance. + /// true if the request is a client credentials grant request, false otherwise. + public static bool IsClientCredentialsGrantType([NotNull] this OpenIddictRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + return string.Equals(request.GrantType, GrantTypes.ClientCredentials, StringComparison.Ordinal); + } + + /// + /// Determines whether the "grant_type" parameter corresponds to the password grant. + /// See http://tools.ietf.org/html/rfc6749#section-4.3.2 for more information. + /// + /// The instance. + /// true if the request is a password grant request, false otherwise. + public static bool IsPasswordGrantType([NotNull] this OpenIddictRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + return string.Equals(request.GrantType, GrantTypes.Password, StringComparison.Ordinal); + } + + /// + /// Determines whether the "grant_type" parameter corresponds to the refresh token grant. + /// See http://tools.ietf.org/html/rfc6749#section-6 for more information. + /// + /// The instance. + /// true if the request is a refresh token grant request, false otherwise. + public static bool IsRefreshTokenGrantType([NotNull] this OpenIddictRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + return string.Equals(request.GrantType, GrantTypes.RefreshToken, StringComparison.Ordinal); + } + + /// + /// Gets the destinations associated with a claim. + /// + /// The instance. + /// The destinations associated with the claim. + public static ImmutableHashSet GetDestinations([NotNull] this Claim claim) + { + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + + claim.Properties.TryGetValue(Properties.Destinations, out string destinations); + + if (string.IsNullOrEmpty(destinations)) + { + return ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase); + } + + return ImmutableHashSet.CreateRange(StringComparer.OrdinalIgnoreCase, JArray.Parse(destinations).Values()); + } + + /// + /// Determines whether the given claim contains the required destination. + /// + /// The instance. + /// The required destination. + public static bool HasDestination([NotNull] this Claim claim, [NotNull] string destination) + { + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + + if (string.IsNullOrEmpty(destination)) + { + throw new ArgumentException("The destination cannot be null or empty.", nameof(destination)); + } + + return GetDestinations(claim).Contains(destination, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Adds specific destinations to a claim. + /// + /// The instance. + /// The destinations. + public static Claim SetDestinations([NotNull] this Claim claim, IEnumerable destinations) + { + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + + if (destinations == null || !destinations.Any()) + { + claim.Properties.Remove(Properties.Destinations); + + return claim; + } + + if (destinations.Any(destination => string.IsNullOrEmpty(destination))) + { + throw new ArgumentException("Destinations cannot be null or empty.", nameof(destinations)); + } + + claim.Properties[Properties.Destinations] = + new JArray(destinations.Distinct(StringComparer.OrdinalIgnoreCase)).ToString(Formatting.None); + + return claim; + } + + /// + /// Adds specific destinations to a claim. + /// + /// The instance. + /// The destinations. + public static Claim SetDestinations([NotNull] this Claim claim, params string[] destinations) + // Note: guarding the destinations parameter against null values + // is not necessary as AsEnumerable() doesn't throw on null values. + => claim.SetDestinations(destinations.AsEnumerable()); + + /// + /// Clones an identity by filtering its claims and the claims of its actor, recursively. + /// + /// The instance to filter. + /// + /// The delegate filtering the claims: return true + /// to accept the claim, false to remove it. + /// + public static ClaimsIdentity Clone( + [NotNull] this ClaimsIdentity identity, + [NotNull] Func filter) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + if (filter == null) + { + throw new ArgumentNullException(nameof(filter)); + } + + var clone = identity.Clone(); + + // Note: make sure to call ToList() to avoid modifying + // the initial collection iterated by ClaimsIdentity.Claims. + foreach (var claim in clone.Claims.ToList()) + { + if (!filter(claim)) + { + clone.RemoveClaim(claim); + } + } + + if (clone.Actor != null) + { + clone.Actor = clone.Actor.Clone(filter); + } + + return clone; + } + + /// + /// Clones a principal by filtering its identities. + /// + /// The instance to filter. + /// + /// The delegate filtering the claims: return true + /// to accept the claim, false to remove it. + /// + public static ClaimsPrincipal Clone( + [NotNull] this ClaimsPrincipal principal, + [NotNull] Func filter) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (filter == null) + { + throw new ArgumentNullException(nameof(filter)); + } + + var clone = new ClaimsPrincipal(); + + foreach (var identity in principal.Identities) + { + clone.AddIdentity(identity.Clone(filter)); + } + + return clone; + } + + /// + /// Adds a claim to a given identity. + /// + /// The identity. + /// The type associated with the claim. + /// The value associated with the claim. + public static ClaimsIdentity AddClaim( + [NotNull] this ClaimsIdentity identity, + [NotNull] string type, [NotNull] string value) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException("The claim value cannot be null or empty.", nameof(value)); + } + + identity.AddClaim(new Claim(type, value)); + return identity; + } + + /// + /// Adds a claim to a given identity and specify one or more destinations. + /// + /// The identity. + /// The type associated with the claim. + /// The value associated with the claim. + /// The destinations associated with the claim. + public static ClaimsIdentity AddClaim( + [NotNull] this ClaimsIdentity identity, + [NotNull] string type, [NotNull] string value, + [NotNull] IEnumerable destinations) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException("The claim value cannot be null or empty.", nameof(value)); + } + + if (destinations == null) + { + throw new ArgumentNullException(nameof(destinations)); + } + + identity.AddClaim(new Claim(type, value).SetDestinations(destinations)); + return identity; + } + + /// + /// Adds a claim to a given identity and specify one or more destinations. + /// + /// The identity. + /// The type associated with the claim. + /// The value associated with the claim. + /// The destinations associated with the claim. + public static ClaimsIdentity AddClaim( + [NotNull] this ClaimsIdentity identity, + [NotNull] string type, [NotNull] string value, + [NotNull] params string[] destinations) + // Note: guarding the destinations parameter against null values + // is not necessary as AsEnumerable() doesn't throw on null values. + => identity.AddClaim(type, value, destinations.AsEnumerable()); + + /// + /// Gets the claim value corresponding to the given type. + /// + /// The identity. + /// The type associated with the claim. + /// The claim value. + public static string GetClaim([NotNull] this ClaimsIdentity identity, [NotNull] string type) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + return identity.FindFirst(type)?.Value; + } + + /// + /// Gets the claim value corresponding to the given type. + /// + /// The principal. + /// The type associated with the claim. + /// The claim value. + public static string GetClaim([NotNull] this ClaimsPrincipal principal, [NotNull] string type) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + return principal.FindFirst(type)?.Value; + } + + /// + /// Gets the claim values corresponding to the given type. + /// + /// The identity. + /// The type associated with the claims. + /// The claim values. + public static ImmutableHashSet GetClaims([NotNull] this ClaimsIdentity identity, [NotNull] string type) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + return ImmutableHashSet.CreateRange(StringComparer.Ordinal, identity.FindAll(type).Select(claim => claim.Value)); + } + + /// + /// Gets the claim values corresponding to the given type. + /// + /// The principal. + /// The type associated with the claims. + /// The claim values. + public static ImmutableHashSet GetClaims([NotNull] this ClaimsPrincipal principal, [NotNull] string type) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.FindAll(type).Select(claim => claim.Value)); + } + + /// + /// Removes all the claims corresponding to the given type. + /// + /// The identity. + /// The type associated with the claims. + /// The claims identity. + public static ClaimsIdentity RemoveClaims([NotNull] this ClaimsIdentity identity, [NotNull] string type) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + foreach (var claim in identity.FindAll(type).ToList()) + { + identity.RemoveClaim(claim); + } + + return identity; + } + + /// + /// Removes all the claims corresponding to the given type. + /// + /// The principal. + /// The type associated with the claims. + /// The claims identity. + public static ClaimsPrincipal RemoveClaims([NotNull] this ClaimsPrincipal principal, [NotNull] string type) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + foreach (var identity in principal.Identities) + { + foreach (var claim in identity.FindAll(type).ToList()) + { + identity.RemoveClaim(claim); + } + } + + return principal; + } + + /// + /// Sets the claim value corresponding to the given type. + /// + /// The identity. + /// The type associated with the claims. + /// The claim value. + /// The claims identity. + public static ClaimsIdentity SetClaims( + [NotNull] this ClaimsIdentity identity, + [NotNull] string type, [CanBeNull] string value) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + identity.RemoveClaims(type); + + if (!string.IsNullOrEmpty(value)) + { + identity.AddClaim(type, value); + } + + return identity; + } + + /// + /// Sets the claim value corresponding to the given type. + /// + /// The principal. + /// The type associated with the claims. + /// The claim value. + /// The claims identity. + public static ClaimsPrincipal SetClaim( + [NotNull] this ClaimsPrincipal principal, + [NotNull] string type, [CanBeNull] string value) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + principal.RemoveClaims(type); + + if (!string.IsNullOrEmpty(value)) + { + ((ClaimsIdentity) principal.Identity).AddClaim(type, value); + } + + return principal; + } + + /// + /// Sets the claim values corresponding to the given type. + /// + /// The identity. + /// The type associated with the claims. + /// The claim values. + /// The claims identity. + public static ClaimsIdentity SetClaims([NotNull] this ClaimsIdentity identity, + [NotNull] string type, [NotNull] IEnumerable values) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + identity.RemoveClaims(type); + + foreach (var value in values) + { + identity.AddClaim(type, value); + } + + return identity; + } + + /// + /// Sets the claim values corresponding to the given type. + /// + /// The principal. + /// The type associated with the claims. + /// The claim values. + /// The claims identity. + public static ClaimsPrincipal SetClaims([NotNull] this ClaimsPrincipal principal, + [NotNull] string type, [NotNull] IEnumerable values) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + principal.RemoveClaims(type); + + foreach (var value in values) + { + ((ClaimsIdentity) principal.Identity).AddClaim(type, value); + } + + return principal; + } + + /// + /// Gets the creation date stored in the claims principal. + /// + /// The claims principal. + /// The creation date or null if the claim cannot be found. + public static DateTimeOffset? GetCreationDate([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + var claim = principal.FindFirst(Claims.IssuedAt); + if (claim == null) + { + return null; + } + + if (!long.TryParse(claim.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + return null; + } + + return DateTimeOffset.FromUnixTimeSeconds(value); + } + + /// + /// Gets the expiration date stored in the claims principal. + /// + /// The claims principal. + /// The expiration date or null if the claim cannot be found. + public static DateTimeOffset? GetExpirationDate([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + var claim = principal.FindFirst(Claims.ExpiresAt); + if (claim == null) + { + return null; + } + + if (!long.TryParse(claim.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + return null; + } + + return DateTimeOffset.FromUnixTimeSeconds(value); + } + + /// + /// Gets the audiences list stored in the claims principal. + /// + /// The claims principal. + /// The audiences list or an empty set if the claims cannot be found. + public static ImmutableHashSet GetAudiences([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.GetClaims(Claims.Audience)); + } + + /// + /// Gets the presenters list stored in the claims principal. + /// + /// The claims principal. + /// The presenters list or an empty set if the claims cannot be found. + public static ImmutableHashSet GetPresenters([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.GetClaims(Claims.AuthorizedParty)); + } + + /// + /// Gets the resources list stored in the claims principal. + /// + /// The claims principal. + /// The resources list or an empty set if the claims cannot be found. + public static ImmutableHashSet GetResources([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.GetClaims(Claims.Resource)); + } + + /// + /// Gets the scopes list stored in the claims principal. + /// + /// The claims principal. + /// The scopes list or an empty set if the claim cannot be found. + public static ImmutableHashSet GetScopes([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + // Note: scopes are deliberately formatted as a single space-separated + // string to respect the usual representation of the standard scope claim. + // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-02. + var value = principal.GetClaim(Claims.Scope); + if (string.IsNullOrEmpty(value)) + { + return ImmutableHashSet.Create(StringComparer.Ordinal); + } + + return ImmutableHashSet.CreateRange(StringComparer.Ordinal, GetValues(value, Separators.Space)); + } + + /// + /// Gets the access token lifetime associated with the claims principal. + /// + /// The claims principal. + /// The access token lifetime or null if the claim cannot be found. + + public static TimeSpan? GetAccessTokenLifetime([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + var value = principal.GetClaim(Claims.Private.AccessTokenLifetime); + if (string.IsNullOrEmpty(value)) + { + return null; + } + + if (double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out double result)) + { + return TimeSpan.FromSeconds(result); + } + + return null; + } + + /// + /// Gets the authorization code lifetime associated with the claims principal. + /// + /// The claims principal. + /// The authorization code lifetime or null if the claim cannot be found. + + public static TimeSpan? GetAuthorizationCodeLifetime([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + var value = principal.GetClaim(Claims.Private.AuthorizationCodeLifetime); + if (string.IsNullOrEmpty(value)) + { + return null; + } + + if (double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out double result)) + { + return TimeSpan.FromSeconds(result); + } + + return null; + } + + /// + /// Gets the identity token lifetime associated with the claims principal. + /// + /// The claims principal. + /// The identity token lifetime or null if the claim cannot be found. + + public static TimeSpan? GetIdentityTokenLifetime([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + var value = principal.GetClaim(Claims.Private.IdentityTokenLifetime); + if (string.IsNullOrEmpty(value)) + { + return null; + } + + if (double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out double result)) + { + return TimeSpan.FromSeconds(result); + } + + return null; + } + + /// + /// Gets the refresh token lifetime associated with the claims principal. + /// + /// The claims principal. + /// The refresh token lifetime or null if the claim cannot be found. + + public static TimeSpan? GetRefreshTokenLifetime([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + var value = principal.GetClaim(Claims.Private.RefreshTokenLifetime); + if (string.IsNullOrEmpty(value)) + { + return null; + } + + if (double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out double result)) + { + return TimeSpan.FromSeconds(result); + } + + return null; + } + + /// + /// Gets the unique identifier associated with the claims principal. + /// + /// The claims principal. + /// The unique identifier or null if the claim cannot be found. + public static string GetTokenId([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.GetClaim(Claims.JwtId); + } + + /// + /// Determines whether the claims principal contains at least one audience. + /// + /// The claims principal. + /// true if the principal contains at least one audience. + public static bool HasAudience([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.FindAll(Claims.Audience).Any(); + } + + /// + /// Determines whether the claims principal contains the given audience. + /// + /// The claims principal. + /// The audience. + /// true if the principal contains the given audience. + public static bool HasAudience([NotNull] this ClaimsPrincipal principal, [NotNull] string audience) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (string.IsNullOrEmpty(audience)) + { + throw new ArgumentException("The audience cannot be null or empty.", nameof(audience)); + } + + return principal.GetAudiences().Contains(audience); + } + + /// + /// Determines whether the claims principal contains at least one presenter. + /// + /// The claims principal. + /// true if the principal contains at least one presenter. + public static bool HasPresenter([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.FindAll(Claims.AuthorizedParty).Any(); + } + + /// + /// Determines whether the claims principal contains the given presenter. + /// + /// The claims principal. + /// The presenter. + /// true if the principal contains the given presenter. + public static bool HasPresenter([NotNull] this ClaimsPrincipal principal, [NotNull] string presenter) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (string.IsNullOrEmpty(presenter)) + { + throw new ArgumentException("The presenter cannot be null or empty.", nameof(presenter)); + } + + return principal.GetPresenters().Contains(presenter); + } + + /// + /// Determines whether the claims principal contains at least one resource. + /// + /// The claims principal. + /// true if the principal contains at least one resource. + public static bool HasResource([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.FindAll(Claims.Resource).Any(); + } + + /// + /// Determines whether the claims principal contains the given resource. + /// + /// The claims principal. + /// The resource. + /// true if the principal contains the given resource. + public static bool HasResource([NotNull] this ClaimsPrincipal principal, [NotNull] string resource) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (string.IsNullOrEmpty(resource)) + { + throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); + } + + return principal.GetResources().Contains(resource); + } + + /// + /// Determines whether the claims principal contains at least one scope. + /// + /// The claims principal. + /// true if the principal contains at least one scope. + public static bool HasScope([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.FindAll(Claims.Scope).Any(); + } + + /// + /// Determines whether the claims principal contains the given scope. + /// + /// The claims principal. + /// The scope. + /// true if the principal contains the given scope. + public static bool HasScope([NotNull] this ClaimsPrincipal principal, [NotNull] string scope) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (string.IsNullOrEmpty(scope)) + { + throw new ArgumentException("The scope cannot be null or empty.", nameof(scope)); + } + + return principal.GetScopes().Contains(scope); + } + + /// + /// Sets the creation date in the claims principal. + /// + /// The claims principal. + /// The creation date + /// The claims principal. + public static ClaimsPrincipal SetCreationDate([NotNull] this ClaimsPrincipal principal, [CanBeNull] DateTimeOffset? date) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + principal.RemoveClaims(Claims.IssuedAt); + + if (date.HasValue) + { + var claim = new Claim(Claims.IssuedAt, date?.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64); + ((ClaimsIdentity) principal.Identity).AddClaim(claim); + } + + return principal; + } + + /// + /// Sets the expiration date in the claims principal. + /// + /// The claims principal. + /// The expiration date + /// The claims principal. + public static ClaimsPrincipal SetExpirationDate([NotNull] this ClaimsPrincipal principal, [CanBeNull] DateTimeOffset? date) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + principal.RemoveClaims(Claims.ExpiresAt); + + if (date.HasValue) + { + var claim = new Claim(Claims.ExpiresAt, date?.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64); + ((ClaimsIdentity) principal.Identity).AddClaim(claim); + } + + return principal; + } + + /// + /// Sets the audiences list in the claims principal. + /// Note: this method automatically excludes duplicate audiences. + /// + /// The claims principal. + /// The audiences to store. + /// The claims principal. + public static ClaimsPrincipal SetAudiences( + [NotNull] this ClaimsPrincipal principal, + [CanBeNull] IEnumerable audiences) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.SetClaims(Claims.Audience, audiences.Distinct(StringComparer.Ordinal)); + } + + /// + /// Sets the audiences list in the claims principal. + /// Note: this method automatically excludes duplicate audiences. + /// + /// The claims principal. + /// The audiences to store. + /// The claims principal. + public static ClaimsPrincipal SetAudiences( + [NotNull] this ClaimsPrincipal principal, [CanBeNull] params string[] audiences) + // Note: guarding the audiences parameter against null values + // is not necessary as AsEnumerable() doesn't throw on null values. + => principal.SetAudiences(audiences.AsEnumerable()); + + /// + /// Sets the presenters list in the claims principal. + /// Note: this method automatically excludes duplicate presenters. + /// + /// The claims principal. + /// The presenters to store. + /// The claims principal. + public static ClaimsPrincipal SetPresenters( + [NotNull] this ClaimsPrincipal principal, + [CanBeNull] IEnumerable presenters) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.SetClaims(Claims.AuthorizedParty, presenters.Distinct(StringComparer.Ordinal)); + } + + /// + /// Sets the presenters list in the claims principal. + /// Note: this method automatically excludes duplicate presenters. + /// + /// The claims principal. + /// The presenters to store. + /// The claims principal. + public static ClaimsPrincipal SetPresenters( + [NotNull] this ClaimsPrincipal principal, [CanBeNull] params string[] presenters) + // Note: guarding the presenters parameter against null values + // is not necessary as AsEnumerable() doesn't throw on null values. + => principal.SetPresenters(presenters.AsEnumerable()); + + /// + /// Sets the resources list in the claims principal. + /// Note: this method automatically excludes duplicate resources. + /// + /// The claims principal. + /// The resources to store. + /// The claims principal. + public static ClaimsPrincipal SetResources( + [NotNull] this ClaimsPrincipal principal, + [CanBeNull] IEnumerable resources) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.SetClaims(Claims.Resource, resources.Distinct(StringComparer.Ordinal)); + } + + /// + /// Sets the resources list in the claims principal. + /// Note: this method automatically excludes duplicate resources. + /// + /// The claims principal. + /// The resources to store. + /// The claims principal. + public static ClaimsPrincipal SetResources( + [NotNull] this ClaimsPrincipal principal, [CanBeNull] params string[] resources) + // Note: guarding the resources parameter against null values + // is not necessary as AsEnumerable() doesn't throw on null values. + => principal.SetResources(resources.AsEnumerable()); + + /// + /// Sets the scopes list in the claims principal. + /// Note: this method automatically excludes duplicate scopes. + /// + /// The claims principal. + /// The scopes to store. + /// The claims principal. + public static ClaimsPrincipal SetScopes( + [NotNull] this ClaimsPrincipal principal, [CanBeNull] IEnumerable scopes) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (scopes == null) + { + return principal.RemoveClaims(Claims.Scope); + } + + // Note: scopes are deliberately formatted as a single space-separated + // string to respect the usual representation of the standard scope claim. + // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-02. + return principal.SetClaim(Claims.Scope, string.Join(" ", scopes.Distinct(StringComparer.Ordinal))); + } + + /// + /// Sets the scopes list in the claims principal. + /// Note: this method automatically excludes duplicate scopes. + /// + /// The claims principal. + /// The scopes to store. + /// The claims principal. + public static ClaimsPrincipal SetScopes( + [NotNull] this ClaimsPrincipal principal, [CanBeNull] params string[] scopes) + // Note: guarding the scopes parameter against null values + // is not necessary as AsEnumerable() doesn't throw on null values. + => principal.SetScopes(scopes.AsEnumerable()); + + /// + /// Sets the access token lifetime associated with the claims principal. + /// + /// The claims principal. + /// The access token lifetime to store. + /// The claims principal. + public static ClaimsPrincipal SetAccessTokenLifetime([NotNull] this ClaimsPrincipal principal, TimeSpan? lifetime) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.SetClaim(Claims.Private.AccessTokenLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture)); + } + + /// + /// Sets the authorization code lifetime associated with the claims principal. + /// + /// The claims principal. + /// The authorization code lifetime to store. + /// The claims principal. + public static ClaimsPrincipal SetAuthorizationCodeLifetime([NotNull] this ClaimsPrincipal principal, TimeSpan? lifetime) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.SetClaim(Claims.Private.AuthorizationCodeLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture)); + } + + /// + /// Sets the identity token lifetime associated with the claims principal. + /// + /// The claims principal. + /// The identity token lifetime to store. + /// The claims principal. + public static ClaimsPrincipal SetIdentityTokenLifetime([NotNull] this ClaimsPrincipal principal, TimeSpan? lifetime) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.SetClaim(Claims.Private.IdentityTokenLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture)); + } + + /// + /// Sets the refresh token lifetime associated with the claims principal. + /// + /// The claims principal. + /// The refresh token lifetime to store. + /// The claims principal. + public static ClaimsPrincipal SetRefreshTokenLifetime([NotNull] this ClaimsPrincipal principal, TimeSpan? lifetime) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.SetClaim(Claims.Private.RefreshTokenLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture)); + } + + /// + /// Sets the unique identifier associated with the claims principal. + /// + /// The claims principal. + /// The unique identifier to store. + /// The claims principal. + public static ClaimsPrincipal SetTokenId([NotNull] this ClaimsPrincipal principal, string identifier) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.SetClaim(Claims.JwtId, identifier); + } + + private static IEnumerable GetValues(string source, char[] separators) + { + Debug.Assert(!string.IsNullOrEmpty(source), "The source string shouldn't be null or empty."); + Debug.Assert(separators?.Length != 0, "The separators collection shouldn't be null or empty."); + + foreach (var element in new StringTokenizer(source, separators)) + { + var segment = Trim(element, separators); + if (segment.Length == 0) + { + continue; + } + + yield return segment.Value; + } + + yield break; + } + + private static bool HasValue(string source, string value, char[] separators) + { + Debug.Assert(!string.IsNullOrEmpty(source), "The source string shouldn't be null or empty."); + Debug.Assert(!string.IsNullOrEmpty(value), "The value string shouldn't be null or empty."); + Debug.Assert(separators?.Length != 0, "The separators collection shouldn't be null or empty."); + + foreach (var element in new StringTokenizer(source, separators)) + { + var segment = Trim(element, separators); + if (segment.Length == 0) + { + continue; + } + + if (segment.Equals(value, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + private static StringSegment TrimStart(StringSegment segment, char[] separators) + { + Debug.Assert(separators?.Length != 0, "The separators collection shouldn't be null or empty."); + + var index = segment.Offset; + + while (index < segment.Offset + segment.Length) + { + if (!IsSeparator(segment.Buffer[index], separators)) + { + break; + } + + index++; + } + + return new StringSegment(segment.Buffer, index, segment.Offset + segment.Length - index); + } + + private static StringSegment TrimEnd(StringSegment segment, char[] separators) + { + Debug.Assert(separators?.Length != 0, "The separators collection shouldn't be null or empty."); + + var index = segment.Offset + segment.Length - 1; + + while (index >= segment.Offset) + { + if (!IsSeparator(segment.Buffer[index], separators)) + { + break; + } + + index--; + } + + return new StringSegment(segment.Buffer, segment.Offset, index - segment.Offset + 1); + } + + private static StringSegment Trim(StringSegment segment, char[] separators) + { + Debug.Assert(separators?.Length != 0, "The separators collection shouldn't be null or empty."); + + return TrimEnd(TrimStart(segment, separators), separators); + } + + private static bool IsSeparator(char character, char[] separators) + { + Debug.Assert(separators?.Length != 0, "The separators collection shouldn't be null or empty."); + + for (var index = 0; index < separators.Length; index++) + { + if (character == separators[index]) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs new file mode 100644 index 000000000..07ebc16d1 --- /dev/null +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs @@ -0,0 +1,348 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text; +using JetBrains.Annotations; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace OpenIddict.Abstractions +{ + /// + /// Represents an abstract OpenIddict message. + /// + /// + /// Security notice: developers instantiating this type are responsible of ensuring that the + /// imported parameters are safe and won't cause the resulting message to grow abnormally, + /// which may result in an excessive memory consumption and a potential denial of service. + /// + [DebuggerDisplay("Parameters: {Parameters.Count}")] + [JsonConverter(typeof(OpenIddictConverter))] + public class OpenIddictMessage + { + /// + /// Initializes a new OpenIddict message. + /// + public OpenIddictMessage() { } + + /// + /// Initializes a new OpenIddict message. + /// + /// The message parameters. + public OpenIddictMessage([NotNull] IEnumerable> parameters) + { + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + foreach (var parameter in parameters) + { + if (string.IsNullOrEmpty(parameter.Key)) + { + continue; + } + + AddParameter(parameter.Key, parameter.Value); + } + } + + /// + /// Initializes a new OpenIddict message. + /// + /// The message parameters. + public OpenIddictMessage([NotNull] IEnumerable> parameters) + { + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + foreach (var parameter in parameters) + { + if (string.IsNullOrEmpty(parameter.Key)) + { + continue; + } + + AddParameter(parameter.Key, parameter.Value); + } + } + + /// + /// Initializes a new OpenIddict message. + /// + /// The message parameters. + public OpenIddictMessage([NotNull] IEnumerable> parameters) + { + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + foreach (var parameter in parameters) + { + if (string.IsNullOrEmpty(parameter.Key)) + { + continue; + } + + AddParameter(parameter.Key, parameter.Value); + } + } + + /// + /// Initializes a new OpenIddict message. + /// + /// The message parameters. + public OpenIddictMessage([NotNull] IEnumerable> parameters) + { + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + foreach (var parameter in parameters) + { + if (string.IsNullOrEmpty(parameter.Key)) + { + continue; + } + + // Note: the core OAuth 2.0 specification requires that request parameters + // not be present more than once but derived specifications like the + // token exchange RFC deliberately allow specifying multiple resource + // parameters with the same name to represent a multi-valued parameter. + switch (parameter.Value?.Length ?? 0) + { + case 0: AddParameter(parameter.Key, default); break; + case 1: AddParameter(parameter.Key, parameter.Value[0]); break; + default: AddParameter(parameter.Key, parameter.Value); break; + } + } + } + + /// + /// Initializes a new OpenIddict message. + /// + /// The message parameters. + public OpenIddictMessage([NotNull] IEnumerable> parameters) + { + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + foreach (var parameter in parameters) + { + if (string.IsNullOrEmpty(parameter.Key)) + { + continue; + } + + // Note: the core OAuth 2.0 specification requires that request parameters + // not be present more than once but derived specifications like the + // token exchange RFC deliberately allow specifying multiple resource + // parameters with the same name to represent a multi-valued parameter. + switch (parameter.Value.Count) + { + case 0: AddParameter(parameter.Key, default); break; + case 1: AddParameter(parameter.Key, parameter.Value[0]); break; + default: AddParameter(parameter.Key, parameter.Value.ToArray()); break; + } + } + } + + /// + /// Gets or sets a parameter. + /// + /// The parameter name. + /// The parameter value. + public OpenIddictParameter? this[string name] + { + get => GetParameter(name); + set => SetParameter(name, value); + } + + /// + /// Gets the dictionary containing the parameters. + /// + protected Dictionary Parameters { get; } + = new Dictionary(StringComparer.Ordinal); + + /// + /// Adds a parameter. + /// + /// The parameter name. + /// The parameter value. + /// The current instance, which allows chaining calls. + public OpenIddictMessage AddParameter([NotNull] string name, OpenIddictParameter value) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The parameter name cannot be null or empty.", nameof(name)); + } + + if (!Parameters.ContainsKey(name)) + { + Parameters.Add(name, value); + } + + return this; + } + + /// + /// Gets the value corresponding to a given parameter. + /// + /// The parameter name. + /// The parameter value, or null if it cannot be found. + public OpenIddictParameter? GetParameter([NotNull] string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The parameter name cannot be null or empty.", nameof(name)); + } + + if (Parameters.TryGetValue(name, out OpenIddictParameter value)) + { + return value; + } + + return null; + } + + /// + /// Gets all the parameters associated with this instance. + /// + /// The parameters associated with this instance. + public ImmutableDictionary GetParameters() + => Parameters.ToImmutableDictionary(StringComparer.Ordinal); + + /// + /// Determines whether the current message contains the specified parameter. + /// + /// The parameter name. + /// true if the parameter is present, false otherwise. + public bool HasParameter([NotNull] string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The parameter name cannot be null or empty.", nameof(name)); + } + + return Parameters.ContainsKey(name); + } + + /// + /// Removes a parameter. + /// + /// The parameter name. + /// The current instance, which allows chaining calls. + public OpenIddictMessage RemoveParameter([NotNull] string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The parameter name cannot be null or empty.", nameof(name)); + } + + Parameters.Remove(name); + + return this; + } + + /// + /// Adds, replaces or removes a parameter. + /// Note: this method automatically removes empty parameters. + /// + /// The parameter name. + /// The parameter value. + /// The current instance, which allows chaining calls. + public OpenIddictMessage SetParameter([NotNull] string name, [CanBeNull] OpenIddictParameter? value) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The parameter name cannot be null or empty.", nameof(name)); + } + + // If the parameter value is null or empty, + // remove the corresponding entry from the collection. + if (value == null || OpenIddictParameter.IsNullOrEmpty(value.GetValueOrDefault())) + { + Parameters.Remove(name); + } + + else + { + Parameters[name] = value.GetValueOrDefault(); + } + + return this; + } + + /// + /// Returns a representation of the current instance that can be used in logs. + /// Note: sensitive parameters like client secrets are automatically removed for security reasons. + /// + /// The indented JSON representation corresponding to this message. + public override string ToString() + { + var builder = new StringBuilder(); + + using (var writer = new JsonTextWriter(new StringWriter(builder, CultureInfo.InvariantCulture))) + { + writer.Formatting = Formatting.Indented; + + writer.WriteStartObject(); + + foreach (var parameter in Parameters) + { + writer.WritePropertyName(parameter.Key); + + // Remove sensitive parameters from the generated payload. + switch (parameter.Key) + { + case OpenIddictConstants.Parameters.AccessToken: + case OpenIddictConstants.Parameters.Assertion: + case OpenIddictConstants.Parameters.ClientAssertion: + case OpenIddictConstants.Parameters.ClientSecret: + case OpenIddictConstants.Parameters.Code: + case OpenIddictConstants.Parameters.IdToken: + case OpenIddictConstants.Parameters.IdTokenHint: + case OpenIddictConstants.Parameters.Password: + case OpenIddictConstants.Parameters.RefreshToken: + case OpenIddictConstants.Parameters.Token: + { + writer.WriteValue("[removed for security reasons]"); + + continue; + } + } + + var token = (JToken) parameter.Value; + if (token == null) + { + writer.WriteNull(); + + continue; + } + + token.WriteTo(writer); + } + + writer.WriteEndObject(); + } + + return builder.ToString(); + } + } +} diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs new file mode 100644 index 000000000..591c90649 --- /dev/null +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs @@ -0,0 +1,477 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace OpenIddict.Abstractions +{ + /// + /// Represents an OpenID Connect parameter value, that can be either a primitive value, + /// an array of strings or a complex JSON representation containing child nodes. + /// + public readonly struct OpenIddictParameter : IEquatable + { + /// + /// Initializes a new OpenID Connect + /// parameter using the specified value. + /// + /// The parameter value. + public OpenIddictParameter(bool value) => Value = value; + + /// + /// Initializes a new OpenID Connect + /// parameter using the specified value. + /// + /// The parameter value. + public OpenIddictParameter(bool? value) => Value = value; + + /// + /// Initializes a new OpenID Connect + /// parameter using the specified value. + /// + /// The parameter value. + public OpenIddictParameter(JToken value) => Value = value; + + /// + /// Initializes a new OpenID Connect + /// parameter using the specified value. + /// + /// The parameter value. + public OpenIddictParameter(long value) => Value = value; + + /// + /// Initializes a new OpenID Connect + /// parameter using the specified value. + /// + /// The parameter value. + public OpenIddictParameter(long? value) => Value = value; + + /// + /// Initializes a new OpenID Connect + /// parameter using the specified value. + /// + /// The parameter value. + public OpenIddictParameter(string value) => Value = value; + + /// + /// Initializes a new OpenID Connect + /// parameter using the specified value. + /// + /// The parameter value. + public OpenIddictParameter(string[] value) => Value = value; + + /// + /// Gets the child item corresponding to the specified index. + /// + /// The index of the child item. + /// An instance containing the item value. + public OpenIddictParameter? this[int index] => GetParameter(index); + + /// + /// Gets the child item corresponding to the specified name. + /// + /// The name of the child item. + /// An instance containing the item value. + public OpenIddictParameter? this[string name] => GetParameter(name); + + /// + /// Gets the associated value, that can be either a primitive CLR type + /// (e.g bool, string, long), an array of strings or a complex JSON object. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public object Value { get; } + + /// + /// Determines whether the current + /// instance is equal to the specified . + /// + /// The other object to which to compare this instance. + /// true if the two instances are equal, false otherwise. + public bool Equals(OpenIddictParameter parameter) => Value switch + { + // If the two parameters reference the same instance, return true. + // Note: true will also be returned if the two parameters are null. + var value when ReferenceEquals(value, parameter.Value) => true, + + // If one of the two parameters is null, return false. + null => false, + var _ when parameter.Value == null => false, + + // If the two parameters are string arrays, use SequenceEqual(). + string[] array when parameter.Value is string[] other => array.SequenceEqual(other), + + // If the two parameters are JSON values, use JToken.DeepEquals(). + JToken token when parameter.Value is JToken other => JToken.DeepEquals(token, other), + + // If the current instance is a JValue, compare the + // underlying value to the other parameter value. + JValue value => value.Value != null && value.Value.Equals(parameter.Value), + + // If the other parameter is a JValue, compare the + // underlying value to the current parameter value. + var value when parameter.Value is JValue other => other.Value != null && other.Value.Equals(value), + + // Otherwise, directly compare the two underlying values. + _ => Value.Equals(parameter.Value) + }; + + /// + /// Determines whether the current + /// instance is equal to the specified . + /// + /// The other object to which to compare this instance. + /// true if the two instances are equal, false otherwise. + public override bool Equals(object value) + => value is OpenIddictParameter parameter && Equals(parameter); + + /// + /// Returns the hash code of the current instance. + /// + /// The hash code for the current instance. + // Note: if the value is a JValue, JSON.NET will automatically + // return the hash code corresponding to the underlying value. + public override int GetHashCode() => Value?.GetHashCode() ?? 0; + + /// + /// Gets the child item corresponding to the specified index. + /// + /// The index of the child item. + /// An instance containing the item value. + public OpenIddictParameter? GetParameter(int index) + { + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index), "The item index cannot be negative."); + } + + if (Value is string[] array) + { + // If the specified index goes beyond the + // number of items in the array, return null. + if (index >= array.Length) + { + return null; + } + + return new OpenIddictParameter(array[index]); + } + + // If the value is not a JSON array, return null. + if (Value is JArray token) + { + // If the specified index goes beyond the + // number of items in the array, return null. + if (index >= token.Count) + { + return null; + } + + // If the item doesn't exist, return a null parameter. + var value = token[index]; + if (value == null) + { + return null; + } + + return new OpenIddictParameter(value); + } + + return null; + } + + /// + /// Gets the child item corresponding to the specified name. + /// + /// The name of the child item. + /// An instance containing the item value. + public OpenIddictParameter? GetParameter([NotNull] string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The item name cannot be null or empty.", nameof(name)); + } + + if (Value is JObject dictionary) + { + // If the item doesn't exist, return a null parameter. + var value = dictionary[name]; + if (value == null) + { + return null; + } + + return new OpenIddictParameter(value); + } + + return null; + } + + /// + /// Gets the child items associated with the current parameter. + /// + /// An enumeration of all the parameters associated with the current instance. + public IEnumerable> GetParameters() + { + if (Value is string[] array) + { + for (var index = 0; index < array.Length; index++) + { + yield return new KeyValuePair(null, array[index]); + } + } + + if (Value is JToken token) + { + foreach (var child in token.Children()) + { + if (!(child is JProperty property)) + { + yield return new KeyValuePair(null, child); + + continue; + } + + yield return new KeyValuePair(property.Name, property.Value); + } + } + + yield break; + } + + /// + /// Returns the representation of the current instance. + /// + /// The representation associated with the parameter value. + public override string ToString() => Value switch + { + null => string.Empty, + JValue value when value.Value == null => string.Empty, + + string[] array => string.Join(", ", array), + + JValue value => value.Value.ToString(), + JToken token => token.ToString(Formatting.None), + + _ => Value.ToString() + }; + + /// + /// Determines whether two instances are equal. + /// + /// The first instance. + /// The second instance. + /// true if the two instances are equal, false otherwise. + public static bool operator ==(OpenIddictParameter left, OpenIddictParameter right) => left.Equals(right); + + /// + /// Determines whether two instances are not equal. + /// + /// The first instance. + /// The second instance. + /// true if the two instances are not equal, false otherwise. + public static bool operator !=(OpenIddictParameter left, OpenIddictParameter right) => !left.Equals(right); + + /// + /// Converts an instance to a boolean. + /// + /// The parameter to convert. + /// The converted value. + public static explicit operator bool(OpenIddictParameter? parameter) => Convert(parameter); + + /// + /// Converts an instance to a nullable boolean. + /// + /// The parameter to convert. + /// The converted value. + public static explicit operator bool?(OpenIddictParameter? parameter) => Convert(parameter); + + /// + /// Converts an instance to a . + /// + /// The parameter to convert. + /// The converted value. + public static explicit operator JArray(OpenIddictParameter? parameter) => Convert(parameter); + + /// + /// Converts an instance to a . + /// + /// The parameter to convert. + /// The converted value. + public static explicit operator JObject(OpenIddictParameter? parameter) => Convert(parameter); + + /// + /// Converts an instance to a . + /// + /// The parameter to convert. + /// The converted value. + public static explicit operator JToken(OpenIddictParameter? parameter) => Convert(parameter); + + /// + /// Converts an instance to a . + /// + /// The parameter to convert. + /// The converted value. + public static explicit operator JValue(OpenIddictParameter? parameter) => Convert(parameter); + + /// + /// Converts an instance to a long integer. + /// + /// The parameter to convert. + /// The converted value. + public static explicit operator long(OpenIddictParameter? parameter) => Convert(parameter); + + /// + /// Converts an instance to a nullable long integer. + /// + /// The parameter to convert. + /// The converted value. + public static explicit operator long?(OpenIddictParameter? parameter) => Convert(parameter); + + /// + /// Converts an instance to a string. + /// + /// The parameter to convert. + /// The converted value. + public static explicit operator string(OpenIddictParameter? parameter) => Convert(parameter); + + /// + /// Converts an instance to an array of strings. + /// + /// The parameter to convert. + /// The converted value. + public static explicit operator string[](OpenIddictParameter? parameter) => Convert(parameter); + + /// + /// Converts a boolean to an instance. + /// + /// The value to convert + /// An instance. + public static implicit operator OpenIddictParameter(bool value) => new OpenIddictParameter(value); + + /// + /// Converts a nullable boolean to an instance. + /// + /// The value to convert + /// An instance. + public static implicit operator OpenIddictParameter(bool? value) => new OpenIddictParameter(value); + + /// + /// Converts a to an instance. + /// + /// The value to convert + /// An instance. + public static implicit operator OpenIddictParameter(JToken value) => new OpenIddictParameter(value); + + /// + /// Converts a long integer to an instance. + /// + /// The value to convert + /// An instance. + public static implicit operator OpenIddictParameter(long value) => new OpenIddictParameter(value); + + /// + /// Converts a nullable long integer to an instance. + /// + /// The value to convert + /// An instance. + public static implicit operator OpenIddictParameter(long? value) => new OpenIddictParameter(value); + + /// + /// Converts a string to an instance. + /// + /// The value to convert + /// An instance. + public static implicit operator OpenIddictParameter(string value) => new OpenIddictParameter(value); + + /// + /// Converts an array of strings to an instance. + /// + /// The value to convert + /// An instance. + public static implicit operator OpenIddictParameter(string[] value) => new OpenIddictParameter(value); + + /// + /// Converts the parameter to the specified generic type. + /// + /// The type the parameter will be converted to. + /// The instance. + /// The converted parameter. + private static T Convert(OpenIddictParameter? parameter) + { + try + { + return parameter?.Value switch + { + null => default, + + T value => value, + + string value when typeof(T) == typeof(string[]) => (T) (object) new string[] { value }, + + // Note: when the parameter is represented as a string, try to + // deserialize it if the requested type is a JArray or a JObject. + string value when typeof(T) == typeof(JArray) => (T) (object) JArray.Parse(value), + + string value when typeof(T) == typeof(JObject) => (T) (object) JObject.Parse(value), + + string[] array => new JArray(array).ToObject(), + + JValue value when typeof(T) == typeof(string[]) => (T) (object) new string[] + { + value.ToObject() + }, + + JToken token => token.ToObject(), + + var value when typeof(T) == typeof(string[]) => (T) (object) new string[] + { + new JValue(value).ToObject() + }, + + _ => new JValue(parameter?.Value).ToObject() + }; + } + + // Swallow the conversion exceptions thrown by JSON.NET. + catch (Exception exception) when (exception is ArgumentException || + exception is FormatException || + exception is InvalidCastException || + exception is JsonReaderException || + exception is JsonSerializationException) + { + return default; + } + + // Other exceptions will be automatically re-thrown. + } + + /// + /// Determines whether an OpenID Connect parameter is null or empty. + /// + /// The OpenID Connect parameter. + /// true if the parameter is null or empty, false otherwise. + public static bool IsNullOrEmpty(OpenIddictParameter parameter) => parameter.Value switch + { + null => true, + + string value => string.IsNullOrEmpty(value), + + string[] array => array.Length == 0, + + JValue value when value.Value is string text => string.IsNullOrEmpty(text), + JArray array => !array.HasValues, + JToken token => !token.HasValues, + + _ => false + }; + } +} diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs new file mode 100644 index 000000000..57c8c456c --- /dev/null +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs @@ -0,0 +1,420 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using JetBrains.Annotations; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace OpenIddict.Abstractions +{ + /// + /// Represents a generic OpenIddict request. + /// + /// + /// Security notice: developers instantiating this type are responsible of ensuring that the + /// imported parameters are safe and won't cause the resulting message to grow abnormally, + /// which may result in an excessive memory consumption and a potential denial of service. + /// + [DebuggerDisplay("Parameters: {Parameters.Count}")] + [JsonConverter(typeof(OpenIddictConverter))] + public class OpenIddictRequest : OpenIddictMessage + { + /// + /// Initializes a new OpenIddict request. + /// + public OpenIddictRequest() + : base() { } + + /// + /// Initializes a new OpenIddict request. + /// + /// The request parameters. + public OpenIddictRequest([NotNull] IEnumerable> parameters) + : base(parameters) { } + + /// + /// Initializes a new OpenIddict request. + /// + /// The request parameters. + public OpenIddictRequest([NotNull] IEnumerable> parameters) + : base(parameters) { } + + /// + /// Initializes a new OpenIddict request. + /// + /// The request parameters. + public OpenIddictRequest([NotNull] IEnumerable> parameters) + : base(parameters) { } + + /// + /// Initializes a new OpenIddict request. + /// + /// The request parameters. + public OpenIddictRequest([NotNull] IEnumerable> parameters) + : base(parameters) { } + + /// + /// Initializes a new OpenIddict request. + /// + /// The request parameters. + public OpenIddictRequest([NotNull] IEnumerable> parameters) + : base(parameters) { } + + /// + /// Gets or sets the "access_token" parameter. + /// + public string AccessToken + { + get => (string) GetParameter(OpenIddictConstants.Parameters.AccessToken); + set => SetParameter(OpenIddictConstants.Parameters.AccessToken, value); + } + + /// + /// Gets or sets the "acr_values" parameter. + /// + public string AcrValues + { + get => (string) GetParameter(OpenIddictConstants.Parameters.AcrValues); + set => SetParameter(OpenIddictConstants.Parameters.AcrValues, value); + } + + /// + /// Gets or sets the "assertion" parameter. + /// + public string Assertion + { + get => (string) GetParameter(OpenIddictConstants.Parameters.Assertion); + set => SetParameter(OpenIddictConstants.Parameters.Assertion, value); + } + + /// + /// Gets or sets the "audience" parameters. + /// + public string[] Audiences + { + get => (string[]) GetParameter(OpenIddictConstants.Parameters.Audience); + set => SetParameter(OpenIddictConstants.Parameters.Audience, value); + } + + /// + /// Gets or sets the "claims" parameter. + /// + public JObject Claims + { + get => (JObject) GetParameter(OpenIddictConstants.Parameters.Claims); + set => SetParameter(OpenIddictConstants.Parameters.Claims, value); + } + + /// + /// Gets or sets the "claims_locales" parameter. + /// + public string ClaimsLocales + { + get => (string) GetParameter(OpenIddictConstants.Parameters.ClaimsLocales); + set => SetParameter(OpenIddictConstants.Parameters.ClaimsLocales, value); + } + + /// + /// Gets or sets the "client_assertion" parameter. + /// + public string ClientAssertion + { + get => (string) GetParameter(OpenIddictConstants.Parameters.ClientAssertion); + set => SetParameter(OpenIddictConstants.Parameters.ClientAssertion, value); + } + + /// + /// Gets or sets the "client_assertion_type" parameter. + /// + public string ClientAssertionType + { + get => (string) GetParameter(OpenIddictConstants.Parameters.ClientAssertionType); + set => SetParameter(OpenIddictConstants.Parameters.ClientAssertionType, value); + } + + /// + /// Gets or sets the "client_id" parameter. + /// + public string ClientId + { + get => (string) GetParameter(OpenIddictConstants.Parameters.ClientId); + set => SetParameter(OpenIddictConstants.Parameters.ClientId, value); + } + + /// + /// Gets or sets the "client_secret" parameter. + /// + public string ClientSecret + { + get => (string) GetParameter(OpenIddictConstants.Parameters.ClientSecret); + set => SetParameter(OpenIddictConstants.Parameters.ClientSecret, value); + } + + /// + /// Gets or sets the "code" parameter. + /// + public string Code + { + get => (string) GetParameter(OpenIddictConstants.Parameters.Code); + set => SetParameter(OpenIddictConstants.Parameters.Code, value); + } + + /// + /// Gets or sets the "code_challenge" parameter. + /// + public string CodeChallenge + { + get => (string) GetParameter(OpenIddictConstants.Parameters.CodeChallenge); + set => SetParameter(OpenIddictConstants.Parameters.CodeChallenge, value); + } + + /// + /// Gets or sets the "code_challenge_method" parameter. + /// + public string CodeChallengeMethod + { + get => (string) GetParameter(OpenIddictConstants.Parameters.CodeChallengeMethod); + set => SetParameter(OpenIddictConstants.Parameters.CodeChallengeMethod, value); + } + + /// + /// Gets or sets the "code_verifier" parameter. + /// + public string CodeVerifier + { + get => (string) GetParameter(OpenIddictConstants.Parameters.CodeVerifier); + set => SetParameter(OpenIddictConstants.Parameters.CodeVerifier, value); + } + + /// + /// Gets or sets the "display" parameter. + /// + public string Display + { + get => (string) GetParameter(OpenIddictConstants.Parameters.Display); + set => SetParameter(OpenIddictConstants.Parameters.Display, value); + } + + /// + /// Gets or sets the "grant_type" parameter. + /// + public string GrantType + { + get => (string) GetParameter(OpenIddictConstants.Parameters.GrantType); + set => SetParameter(OpenIddictConstants.Parameters.GrantType, value); + } + + /// + /// Gets or sets the "identity_provider" parameter. + /// + public string IdentityProvider + { + get => (string) GetParameter(OpenIddictConstants.Parameters.IdentityProvider); + set => SetParameter(OpenIddictConstants.Parameters.IdentityProvider, value); + } + + /// + /// Gets or sets the "id_token_hint" parameter. + /// + public string IdTokenHint + { + get => (string) GetParameter(OpenIddictConstants.Parameters.IdTokenHint); + set => SetParameter(OpenIddictConstants.Parameters.IdTokenHint, value); + } + + /// + /// Gets or sets the "login_hint" parameter. + /// + public string LoginHint + { + get => (string) GetParameter(OpenIddictConstants.Parameters.LoginHint); + set => SetParameter(OpenIddictConstants.Parameters.LoginHint, value); + } + + /// + /// Gets or sets the "max_age" parameter. + /// + public long? MaxAge + { + get => (long?) GetParameter(OpenIddictConstants.Parameters.MaxAge); + set => SetParameter(OpenIddictConstants.Parameters.MaxAge, value); + } + + /// + /// Gets or sets the "nonce" parameter. + /// + public string Nonce + { + get => (string) GetParameter(OpenIddictConstants.Parameters.Nonce); + set => SetParameter(OpenIddictConstants.Parameters.Nonce, value); + } + + /// + /// Gets or sets the "password" parameter. + /// + public string Password + { + get => (string) GetParameter(OpenIddictConstants.Parameters.Password); + set => SetParameter(OpenIddictConstants.Parameters.Password, value); + } + + /// + /// Gets or sets the "post_logout_redirect_uri" parameter. + /// + public string PostLogoutRedirectUri + { + get => (string) GetParameter(OpenIddictConstants.Parameters.PostLogoutRedirectUri); + set => SetParameter(OpenIddictConstants.Parameters.PostLogoutRedirectUri, value); + } + + /// + /// Gets or sets the "prompt" parameter. + /// + public string Prompt + { + get => (string) GetParameter(OpenIddictConstants.Parameters.Prompt); + set => SetParameter(OpenIddictConstants.Parameters.Prompt, value); + } + + /// + /// Gets or sets the "redirect_uri" parameter. + /// + public string RedirectUri + { + get => (string) GetParameter(OpenIddictConstants.Parameters.RedirectUri); + set => SetParameter(OpenIddictConstants.Parameters.RedirectUri, value); + } + + /// + /// Gets or sets the "refresh_token" parameter. + /// + public string RefreshToken + { + get => (string) GetParameter(OpenIddictConstants.Parameters.RefreshToken); + set => SetParameter(OpenIddictConstants.Parameters.RefreshToken, value); + } + + /// + /// Gets or sets the "request" parameter. + /// + public string Request + { + get => (string) GetParameter(OpenIddictConstants.Parameters.Request); + set => SetParameter(OpenIddictConstants.Parameters.Request, value); + } + + /// + /// Gets or sets the "request_id" parameter. + /// + public string RequestId + { + get => (string) GetParameter(OpenIddictConstants.Parameters.RequestId); + set => SetParameter(OpenIddictConstants.Parameters.RequestId, value); + } + + /// + /// Gets or sets the "request_uri" parameter. + /// + public string RequestUri + { + get => (string) GetParameter(OpenIddictConstants.Parameters.RequestUri); + set => SetParameter(OpenIddictConstants.Parameters.RequestUri, value); + } + + /// + /// Gets or sets the "resource" parameters. + /// + public string[] Resources + { + get => (string[]) GetParameter(OpenIddictConstants.Parameters.Resource); + set => SetParameter(OpenIddictConstants.Parameters.Resource, value); + } + + /// + /// Gets or sets the "response_mode" parameter. + /// + public string ResponseMode + { + get => (string) GetParameter(OpenIddictConstants.Parameters.ResponseMode); + set => SetParameter(OpenIddictConstants.Parameters.ResponseMode, value); + } + + /// + /// Gets or sets the "response_type" parameter. + /// + public string ResponseType + { + get => (string) GetParameter(OpenIddictConstants.Parameters.ResponseType); + set => SetParameter(OpenIddictConstants.Parameters.ResponseType, value); + } + + /// + /// Gets or sets the "scope" parameter. + /// + public string Scope + { + get => (string) GetParameter(OpenIddictConstants.Parameters.Scope); + set => SetParameter(OpenIddictConstants.Parameters.Scope, value); + } + + /// + /// Gets or sets the "state" parameter. + /// + public string State + { + get => (string) GetParameter(OpenIddictConstants.Parameters.State); + set => SetParameter(OpenIddictConstants.Parameters.State, value); + } + + /// + /// Gets or sets the "token" parameter. + /// + public string Token + { + get => (string) GetParameter(OpenIddictConstants.Parameters.Token); + set => SetParameter(OpenIddictConstants.Parameters.Token, value); + } + + /// + /// Gets or sets the "token_type_hint" parameter. + /// + public string TokenTypeHint + { + get => (string) GetParameter(OpenIddictConstants.Parameters.TokenTypeHint); + set => SetParameter(OpenIddictConstants.Parameters.TokenTypeHint, value); + } + + /// + /// Gets or sets the "registration" parameter. + /// + public JObject Registration + { + get => (JObject) GetParameter(OpenIddictConstants.Parameters.Registration); + set => SetParameter(OpenIddictConstants.Parameters.Registration, value); + } + + /// + /// Gets or sets the "ui_locales" parameter. + /// + public string UiLocales + { + get => (string) GetParameter(OpenIddictConstants.Parameters.UiLocales); + set => SetParameter(OpenIddictConstants.Parameters.UiLocales, value); + } + + /// + /// Gets or sets the "username" parameter. + /// + public string Username + { + get => (string) GetParameter(OpenIddictConstants.Parameters.Username); + set => SetParameter(OpenIddictConstants.Parameters.Username, value); + } + } +} diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs new file mode 100644 index 000000000..3c32a771e --- /dev/null +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs @@ -0,0 +1,168 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Generic; +using System.Diagnostics; +using JetBrains.Annotations; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace OpenIddict.Abstractions +{ + /// + /// Represents a generic OpenIddict response. + /// + /// + /// Security notice: developers instantiating this type are responsible of ensuring that the + /// imported parameters are safe and won't cause the resulting message to grow abnormally, + /// which may result in an excessive memory consumption and a potential denial of service. + /// + [DebuggerDisplay("Parameters: {Parameters.Count}")] + [JsonConverter(typeof(OpenIddictConverter))] + public class OpenIddictResponse : OpenIddictMessage + { + /// + /// Initializes a new OpenIddict response. + /// + public OpenIddictResponse() + : base() { } + + /// + /// Initializes a new OpenIddict response. + /// + /// The response parameters. + public OpenIddictResponse([NotNull] IEnumerable> parameters) + : base(parameters) { } + + /// + /// Initializes a new OpenIddict response. + /// + /// The response parameters. + public OpenIddictResponse([NotNull] IEnumerable> parameters) + : base(parameters) { } + + /// + /// Initializes a new OpenIddict response. + /// + /// The response parameters. + public OpenIddictResponse([NotNull] IEnumerable> parameters) + : base(parameters) { } + + /// + /// Initializes a new OpenIddict response. + /// + /// The response parameters. + public OpenIddictResponse([NotNull] IEnumerable> parameters) + : base(parameters) { } + + /// + /// Initializes a new OpenIddict response. + /// + /// The response parameters. + public OpenIddictResponse([NotNull] IEnumerable> parameters) + : base(parameters) { } + + /// + /// Gets or sets the "access_token" parameter. + /// + public string AccessToken + { + get => (string) GetParameter(OpenIddictConstants.Parameters.AccessToken); + set => SetParameter(OpenIddictConstants.Parameters.AccessToken, value); + } + + /// + /// Gets or sets the "code" parameter. + /// + public string Code + { + get => (string) GetParameter(OpenIddictConstants.Parameters.Code); + set => SetParameter(OpenIddictConstants.Parameters.Code, value); + } + + /// + /// Gets or sets the "error" parameter. + /// + public string Error + { + get => (string) GetParameter(OpenIddictConstants.Parameters.Error); + set => SetParameter(OpenIddictConstants.Parameters.Error, value); + } + + /// + /// Gets or sets the "error_description" parameter. + /// + public string ErrorDescription + { + get => (string) GetParameter(OpenIddictConstants.Parameters.ErrorDescription); + set => SetParameter(OpenIddictConstants.Parameters.ErrorDescription, value); + } + + /// + /// Gets or sets the "error_uri" parameter. + /// + public string ErrorUri + { + get => (string) GetParameter(OpenIddictConstants.Parameters.ErrorUri); + set => SetParameter(OpenIddictConstants.Parameters.ErrorUri, value); + } + + /// + /// Gets or sets the "expires_in" parameter. + /// + public long? ExpiresIn + { + get => (long?) GetParameter(OpenIddictConstants.Parameters.ExpiresIn); + set => SetParameter(OpenIddictConstants.Parameters.ExpiresIn, value); + } + + /// + /// Gets or sets the "id_token" parameter. + /// + public string IdToken + { + get => (string) GetParameter(OpenIddictConstants.Parameters.IdToken); + set => SetParameter(OpenIddictConstants.Parameters.IdToken, value); + } + + /// + /// Gets or sets the "refresh_token" parameter. + /// + public string RefreshToken + { + get => (string) GetParameter(OpenIddictConstants.Parameters.RefreshToken); + set => SetParameter(OpenIddictConstants.Parameters.RefreshToken, value); + } + + /// + /// Gets or sets the "scope" parameter. + /// + public string Scope + { + get => (string) GetParameter(OpenIddictConstants.Parameters.Scope); + set => SetParameter(OpenIddictConstants.Parameters.Scope, value); + } + + /// + /// Gets or sets the "state" parameter. + /// + public string State + { + get => (string) GetParameter(OpenIddictConstants.Parameters.State); + set => SetParameter(OpenIddictConstants.Parameters.State, value); + } + + /// + /// Gets or sets the "token_type" parameter. + /// + public string TokenType + { + get => (string) GetParameter(OpenIddictConstants.Parameters.TokenType); + set => SetParameter(OpenIddictConstants.Parameters.TokenType, value); + } + } +} diff --git a/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj b/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj new file mode 100644 index 000000000..2074ec582 --- /dev/null +++ b/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp3.0 + + + + Easy-to-use OpenID Connect stack for ASP.NET Core. + $(PackageTags);aspnetcore;server;validation + + + + + + + + diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index 2ec888ee4..7d64c3925 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -8,7 +8,6 @@ using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Security.Claims; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -164,24 +163,22 @@ public virtual async Task CreateAsync( /// /// Creates a new permanent authorization based on the specified parameters. /// - /// The principal associated with the authorization. + /// The claims associated with the authorization. /// The subject associated with the authorization. /// The client associated with the authorization. /// The authorization type. /// The minimal scopes associated with the authorization. - /// The authentication properties associated with the authorization. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, whose result returns the authorization. /// public virtual Task CreateAsync( - [NotNull] ClaimsPrincipal principal, [NotNull] string subject, - [NotNull] string client, [NotNull] string type, ImmutableArray scopes, - [CanBeNull] ImmutableDictionary properties, CancellationToken cancellationToken = default) + [NotNull] ImmutableDictionary claims, [NotNull] string subject, + [NotNull] string client, [NotNull] string type, ImmutableArray scopes, CancellationToken cancellationToken = default) { - if (principal == null) + if (claims == null) { - throw new ArgumentNullException(nameof(principal)); + throw new ArgumentNullException(nameof(claims)); } if (string.IsNullOrEmpty(subject)) @@ -202,7 +199,6 @@ public virtual Task CreateAsync( var descriptor = new OpenIddictAuthorizationDescriptor { ApplicationId = client, - Principal = principal, Status = OpenIddictConstants.Statuses.Valid, Subject = subject, Type = type @@ -210,12 +206,9 @@ public virtual Task CreateAsync( descriptor.Scopes.UnionWith(scopes); - if (properties != null) + foreach (var claim in claims) { - foreach (var property in properties) - { - descriptor.Properties.Add(property); - } + descriptor.Claims.Add(claim); } return CreateAsync(descriptor, cancellationToken); @@ -1206,7 +1199,7 @@ public virtual async Task> ValidateAsync( break; } - if (scope.Contains(OpenIddictConstants.Separators.Space)) + if (scope.Contains(OpenIddictConstants.Separators.Space[0])) { builder.Add(new ValidationResult("Scopes cannot contain spaces.")); @@ -1225,8 +1218,8 @@ Task IOpenIddictAuthorizationManager.CountAsync(CancellationToken cancella Task IOpenIddictAuthorizationManager.CountAsync(Func, IQueryable> query, CancellationToken cancellationToken) => CountAsync(query, cancellationToken); - async Task IOpenIddictAuthorizationManager.CreateAsync(ClaimsPrincipal principal, string subject, string client, string type, ImmutableArray scopes, ImmutableDictionary properties, CancellationToken cancellationToken) - => await CreateAsync(principal, subject, client, type, scopes, properties, cancellationToken); + async Task IOpenIddictAuthorizationManager.CreateAsync(ImmutableDictionary claims, string subject, string client, string type, ImmutableArray scopes, CancellationToken cancellationToken) + => await CreateAsync(claims, subject, client, type, scopes, cancellationToken); async Task IOpenIddictAuthorizationManager.CreateAsync(OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken) => await CreateAsync(descriptor, cancellationToken); diff --git a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs index cdf115dd9..ccb8e28f1 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs @@ -746,7 +746,7 @@ public virtual async Task> ValidateAsync( builder.Add(new ValidationResult("The scope name cannot be null or empty.")); } - else if (name.Contains(OpenIddictConstants.Separators.Space)) + else if (name.Contains(OpenIddictConstants.Separators.Space[0])) { builder.Add(new ValidationResult("The scope name cannot contain spaces.")); } diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index ace25b496..f22fe6ac9 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -1274,9 +1274,9 @@ await Store.GetIdAsync(token, cancellationToken), StringComparison.Ordinal)) builder.Add(new ValidationResult("The token type cannot be null or empty.")); } - else if (!string.Equals(type, OpenIddictConstants.TokenTypes.AccessToken, StringComparison.OrdinalIgnoreCase) && - !string.Equals(type, OpenIddictConstants.TokenTypes.AuthorizationCode, StringComparison.OrdinalIgnoreCase) && - !string.Equals(type, OpenIddictConstants.TokenTypes.RefreshToken, StringComparison.OrdinalIgnoreCase)) + else if (!string.Equals(type, OpenIddictConstants.TokenUsages.AccessToken, StringComparison.OrdinalIgnoreCase) && + !string.Equals(type, OpenIddictConstants.TokenUsages.AuthorizationCode, StringComparison.OrdinalIgnoreCase) && + !string.Equals(type, OpenIddictConstants.TokenUsages.RefreshToken, StringComparison.OrdinalIgnoreCase)) { builder.Add(new ValidationResult("The specified token type is not supported by the default token manager.")); } diff --git a/src/OpenIddict.Core/OpenIddict.Core.csproj b/src/OpenIddict.Core/OpenIddict.Core.csproj index 57703ecf7..bcba8e065 100644 --- a/src/OpenIddict.Core/OpenIddict.Core.csproj +++ b/src/OpenIddict.Core/OpenIddict.Core.csproj @@ -1,15 +1,12 @@  - - - netstandard2.0 + netstandard2.0;netstandard2.1 OpenIddict's core components, used to manage the applications, authorizations, scopes and tokens stored in the database. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security + $(PackageTags);core @@ -19,9 +16,9 @@ - - - + + + diff --git a/src/OpenIddict.Core/OpenIddictCoreBuilder.cs b/src/OpenIddict.Core/OpenIddictCoreBuilder.cs index 7207b6ca7..baa948f4a 100644 --- a/src/OpenIddict.Core/OpenIddictCoreBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictCoreBuilder.cs @@ -24,14 +24,7 @@ public class OpenIddictCoreBuilder /// /// The services collection. public OpenIddictCoreBuilder([NotNull] IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - Services = services; - } + => Services = services ?? throw new ArgumentNullException(nameof(services)); /// /// Gets the services collection. diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddict.EntityFramework.Models.csproj b/src/OpenIddict.EntityFramework.Models/OpenIddict.EntityFramework.Models.csproj index 6deabadbe..3203218d9 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddict.EntityFramework.Models.csproj +++ b/src/OpenIddict.EntityFramework.Models/OpenIddict.EntityFramework.Models.csproj @@ -1,15 +1,12 @@  - - netstandard2.0 Relational entities for the Entity Framework 6.x stores. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security + $(PackageTags);entityframework;models diff --git a/src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj b/src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj index 2b8eef028..3c6fdd082 100644 --- a/src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj +++ b/src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj @@ -1,15 +1,12 @@  - - - net461 + net461;netstandard2.1 Entity Framework 6.x stores for OpenIddict. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security + $(PackageTags);entityframework diff --git a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs index 2b8df5da0..f15b13de7 100644 --- a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs @@ -25,14 +25,7 @@ public class OpenIddictEntityFrameworkBuilder /// /// The services collection. public OpenIddictEntityFrameworkBuilder([NotNull] IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - Services = services; - } + => Services = services ?? throw new ArgumentNullException(nameof(services)); /// /// Gets the services collection. diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddict.EntityFrameworkCore.Models.csproj b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddict.EntityFrameworkCore.Models.csproj index fe2ed955c..f3bcb157f 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddict.EntityFrameworkCore.Models.csproj +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddict.EntityFrameworkCore.Models.csproj @@ -1,6 +1,4 @@ - - - + netstandard2.0 @@ -8,8 +6,7 @@ Relational entities for the Entity Framework Core stores. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security + $(PackageTags);entityframeworkcore;models diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj b/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj index cdca6b5ee..6f59b8cb0 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj @@ -1,15 +1,12 @@  - - - netstandard2.0 + netstandard2.1 Entity Framework Core stores for OpenIddict. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security + $(PackageTags);entityframeworkcore @@ -19,7 +16,7 @@ - + diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs index 418dd93c5..f3f1144ed 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs @@ -24,14 +24,7 @@ public class OpenIddictEntityFrameworkCoreBuilder /// /// The services collection. public OpenIddictEntityFrameworkCoreBuilder([NotNull] IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - Services = services; - } + => Services = services ?? throw new ArgumentNullException(nameof(services)); /// /// Gets the services collection. diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs index 93d9b75db..c32d2eb3c 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs @@ -6,8 +6,6 @@ using System; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection.Extensions; using OpenIddict.EntityFrameworkCore; using OpenIddict.EntityFrameworkCore.Models; diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs index 8798d1509..cef96369e 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs @@ -16,7 +16,6 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -333,7 +332,7 @@ public virtual Task FindByIdAsync([NotNull] string identifier, Can /// Exposes a compiled query allowing to retrieve all the applications /// associated with the specified post_logout_redirect_uri. /// - private static readonly Func> FindByPostLogoutRedirectUri = + private static readonly Func> FindByPostLogoutRedirectUri = // To optimize the efficiency of the query a bit, only applications whose stringified // PostLogoutRedirectUris contains the specified URL are returned. Once the applications // are retrieved, a second pass is made to ensure only valid elements are returned. @@ -361,10 +360,9 @@ public virtual async Task> FindByPostLogoutRedirect throw new ArgumentException("The address cannot be null or empty.", nameof(address)); } - var applications = await FindByPostLogoutRedirectUri(Context, address).ToListAsync(cancellationToken); - var builder = ImmutableArray.CreateBuilder(applications.Count); + var builder = ImmutableArray.CreateBuilder(); - foreach (var application in applications) + await foreach (var application in FindByPostLogoutRedirectUri(Context, address)) { foreach (var uri in await GetPostLogoutRedirectUrisAsync(application, cancellationToken)) { @@ -379,16 +377,14 @@ public virtual async Task> FindByPostLogoutRedirect } } - return builder.Count == builder.Capacity ? - builder.MoveToImmutable() : - builder.ToImmutable(); + return builder.ToImmutable(); } /// /// Exposes a compiled query allowing to retrieve all the /// applications associated with the specified redirect_uri. /// - private static readonly Func> FindByRedirectUri = + private static readonly Func> FindByRedirectUri = // To optimize the efficiency of the query a bit, only applications whose stringified // RedirectUris property contains the specified URL are returned. Once the applications // are retrieved, a second pass is made to ensure only valid elements are returned. @@ -416,10 +412,9 @@ public virtual async Task> FindByRedirectUriAsync( throw new ArgumentException("The address cannot be null or empty.", nameof(address)); } - var applications = await FindByRedirectUri(Context, address).ToListAsync(cancellationToken); - var builder = ImmutableArray.CreateBuilder(applications.Count); + var builder = ImmutableArray.CreateBuilder(); - foreach (var application in applications) + await foreach (var application in FindByRedirectUri(Context, address)) { foreach (var uri in await GetRedirectUrisAsync(application, cancellationToken)) { @@ -434,9 +429,7 @@ public virtual async Task> FindByRedirectUriAsync( } } - return builder.Count == builder.Capacity ? - builder.MoveToImmutable() : - builder.ToImmutable(); + return builder.ToImmutable(); } /// diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs index dcd3627a3..c86d11ad7 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs @@ -16,7 +16,6 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -253,7 +252,7 @@ where element.Id.Equals(authorization.Id) /// Exposes a compiled query allowing to retrieve the authorizations corresponding /// to the specified subject and associated with the application identifier. /// - private static readonly Func> FindBySubjectAndClient = + private static readonly Func> FindBySubjectAndClient = // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using authorization.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -291,14 +290,21 @@ public virtual async Task> FindAsync( throw new ArgumentException("The client cannot be null or empty.", nameof(client)); } - return ImmutableArray.CreateRange(await FindBySubjectAndClient(Context, - ConvertIdentifierFromString(client), subject).ToListAsync(cancellationToken)); + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var authorization in FindBySubjectAndClient(Context, + ConvertIdentifierFromString(client), subject)) + { + builder.Add(authorization); + } + + return builder.ToImmutable(); } /// /// Exposes a compiled query allowing to retrieve the authorizations matching the specified parameters. /// - private static readonly Func> FindBySubjectClientAndStatus = + private static readonly Func> FindBySubjectClientAndStatus = // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using authorization.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -342,14 +348,21 @@ public virtual async Task> FindAsync( throw new ArgumentException("The status cannot be null or empty.", nameof(status)); } - return ImmutableArray.CreateRange(await FindBySubjectClientAndStatus(Context, - ConvertIdentifierFromString(client), subject, status).ToListAsync(cancellationToken)); + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var authorization in FindBySubjectClientAndStatus(Context, + ConvertIdentifierFromString(client), subject, status)) + { + builder.Add(authorization); + } + + return builder.ToImmutable(); } /// /// Exposes a compiled query allowing to retrieve the authorizations matching the specified parameters. /// - private static readonly Func> FindBySubjectClientStatusAndType = + private static readonly Func> FindBySubjectClientStatusAndType = // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using authorization.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -401,8 +414,15 @@ public virtual async Task> FindAsync( throw new ArgumentException("The type cannot be null or empty.", nameof(type)); } - return ImmutableArray.CreateRange(await FindBySubjectClientStatusAndType(Context, - ConvertIdentifierFromString(client), subject, status, type).ToListAsync(cancellationToken)); + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var authorization in FindBySubjectClientStatusAndType(Context, + ConvertIdentifierFromString(client), subject, status, type)) + { + builder.Add(authorization); + } + + return builder.ToImmutable(); } /// @@ -453,7 +473,7 @@ async Task HasScopesAsync() /// Exposes a compiled query allowing to retrieve the list of /// authorizations corresponding to the specified application identifier. /// - private static readonly Func> FindByApplicationId = + private static readonly Func> FindByApplicationId = // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using authorization.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -483,8 +503,15 @@ public virtual async Task> FindByApplicationIdAsy throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return ImmutableArray.CreateRange(await FindByApplicationId(Context, - ConvertIdentifierFromString(identifier)).ToListAsync(cancellationToken)); + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var authorization in FindByApplicationId(Context, + ConvertIdentifierFromString(identifier))) + { + builder.Add(authorization); + } + + return builder.ToImmutable(); } /// @@ -521,7 +548,7 @@ public virtual Task FindByIdAsync([NotNull] string identifier, C /// Exposes a compiled query allowing to retrieve all the /// authorizations corresponding to the specified subject. /// - private static readonly Func> FindBySubject = + private static readonly Func> FindBySubject = EF.CompileAsyncQuery((TContext context, string subject) => from authorization in context.Set() .Include(authorization => authorization.Application) @@ -546,7 +573,14 @@ public virtual async Task> FindBySubjectAsync( throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } - return ImmutableArray.CreateRange(await FindBySubject(Context, subject).ToListAsync(cancellationToken)); + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var authorization in FindBySubject(Context, subject)) + { + builder.Add(authorization); + } + + return builder.ToImmutable(); } /// diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs index f945e4f87..b1526881e 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs @@ -5,6 +5,7 @@ */ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.Linq; @@ -13,7 +14,6 @@ using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Query; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Newtonsoft.Json; @@ -239,7 +239,7 @@ public virtual Task FindByNameAsync([NotNull] string name, CancellationT /// /// Exposes a compiled query allowing to retrieve a list of scopes using their name. /// - private static readonly Func, AsyncEnumerable> FindByNames = + private static readonly Func, IAsyncEnumerable> FindByNames = EF.CompileAsyncQuery((TContext context, ImmutableArray names) => from scope in context.Set().AsTracking() where names.Contains(scope.Name) @@ -262,13 +262,20 @@ public virtual async Task> FindByNamesAsync( throw new ArgumentException("Scope names cannot be null or empty.", nameof(names)); } - return ImmutableArray.CreateRange(await FindByNames(Context, names).ToListAsync(cancellationToken)); + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var scope in FindByNames(Context, names)) + { + builder.Add(scope); + } + + return builder.ToImmutable(); } /// /// Exposes a compiled query allowing to retrieve all the scopes that contain the specified resource. /// - private static readonly Func> FindByResource = + private static readonly Func> FindByResource = // To optimize the efficiency of the query a bit, only scopes whose stringified // Resources column contains the specified resource are returned. Once the scopes // are retrieved, a second pass is made to ensure only valid elements are returned. @@ -298,7 +305,7 @@ public virtual async Task> FindByResourceAsync( var builder = ImmutableArray.CreateBuilder(); - foreach (var scope in await FindByResource(Context, resource).ToListAsync(cancellationToken)) + await foreach (var scope in FindByResource(Context, resource)) { var resources = await GetResourcesAsync(scope, cancellationToken); if (resources.Contains(resource, StringComparer.Ordinal)) diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs index 27d6767b0..1ff551b10 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs @@ -16,7 +16,6 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -206,7 +205,7 @@ public virtual async Task DeleteAsync([NotNull] TToken token, CancellationToken /// Exposes a compiled query allowing to retrieve the tokens corresponding /// to the specified subject and associated with the application identifier. /// - private static readonly Func> FindBySubjectAndClient = + private static readonly Func> FindBySubjectAndClient = // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using token.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -245,14 +244,21 @@ public virtual async Task> FindAsync([NotNull] string sub throw new ArgumentException("The client cannot be null or empty.", nameof(client)); } - return ImmutableArray.CreateRange(await FindBySubjectAndClient(Context, - ConvertIdentifierFromString(client), subject).ToListAsync(cancellationToken)); + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var token in FindBySubjectAndClient(Context, + ConvertIdentifierFromString(client), subject)) + { + builder.Add(token); + } + + return builder.ToImmutable(); } /// /// Exposes a compiled query allowing to retrieve the tokens matching the specified parameters. /// - private static readonly Func> FindBySubjectClientAndStatus = + private static readonly Func> FindBySubjectClientAndStatus = // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using token.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -297,14 +303,21 @@ public virtual async Task> FindAsync( throw new ArgumentException("The status cannot be null or empty.", nameof(status)); } - return ImmutableArray.CreateRange(await FindBySubjectClientAndStatus(Context, - ConvertIdentifierFromString(client), subject, status).ToListAsync(cancellationToken)); + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var token in FindBySubjectClientAndStatus(Context, + ConvertIdentifierFromString(client), subject, status)) + { + builder.Add(token); + } + + return builder.ToImmutable(); } /// /// Exposes a compiled query allowing to retrieve the tokens matching the specified parameters. /// - private static readonly Func> FindBySubjectClientStatusAndType = + private static readonly Func> FindBySubjectClientStatusAndType = // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be // filtered using token.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -357,15 +370,22 @@ public virtual async Task> FindAsync( throw new ArgumentException("The type cannot be null or empty.", nameof(type)); } - return ImmutableArray.CreateRange(await FindBySubjectClientStatusAndType(Context, - ConvertIdentifierFromString(client), subject, status, type).ToListAsync(cancellationToken)); + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var token in FindBySubjectClientStatusAndType(Context, + ConvertIdentifierFromString(client), subject, status, type)) + { + builder.Add(token); + } + + return builder.ToImmutable(); } /// /// Exposes a compiled query allowing to retrieve the list of /// tokens corresponding to the specified application identifier. /// - private static readonly Func> FindByApplicationId = + private static readonly Func> FindByApplicationId = // Note: due to a bug in Entity Framework Core's query visitor, the tokens can't be // filtered using token.Application.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -395,15 +415,21 @@ public virtual async Task> FindByApplicationIdAsync([NotN throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return ImmutableArray.CreateRange(await FindByApplicationId(Context, - ConvertIdentifierFromString(identifier)).ToListAsync(cancellationToken)); + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var token in FindByApplicationId(Context, ConvertIdentifierFromString(identifier))) + { + builder.Add(token); + } + + return builder.ToImmutable(); } /// /// Exposes a compiled query allowing to retrieve the list of /// tokens corresponding to the specified authorization identifier. /// - private static readonly Func> FindByAuthorizationId = + private static readonly Func> FindByAuthorizationId = // Note: due to a bug in Entity Framework Core's query visitor, the tokens can't be // filtered using token.Authorization.Id.Equals(key). To work around this issue, // this compiled query uses an explicit join before applying the equality check. @@ -433,8 +459,14 @@ public virtual async Task> FindByAuthorizationIdAsync([No throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return ImmutableArray.CreateRange(await FindByAuthorizationId(Context, - ConvertIdentifierFromString(identifier)).ToListAsync(cancellationToken)); + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var token in FindByAuthorizationId(Context, ConvertIdentifierFromString(identifier))) + { + builder.Add(token); + } + + return builder.ToImmutable(); } /// @@ -505,7 +537,7 @@ public virtual Task FindByReferenceIdAsync([NotNull] string identifier, /// Exposes a compiled query allowing to retrieve the /// list of tokens corresponding to the specified subject. /// - private static readonly Func> FindBySubject = + private static readonly Func> FindBySubject = EF.CompileAsyncQuery((TContext context, string subject) => from token in context.Set() .Include(token => token.Application) @@ -530,7 +562,14 @@ public virtual async Task> FindBySubjectAsync([NotNull] s throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } - return ImmutableArray.CreateRange(await FindBySubject(Context, subject).ToListAsync(cancellationToken)); + var builder = ImmutableArray.CreateBuilder(); + + await foreach (var token in FindBySubject(Context, subject)) + { + builder.Add(token); + } + + return builder.ToImmutable(); } /// diff --git a/src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj b/src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj index 4b3b1b879..a3ea0f4fc 100644 --- a/src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj +++ b/src/OpenIddict.MongoDb.Models/OpenIddict.MongoDb.Models.csproj @@ -1,6 +1,4 @@ - - - + netstandard2.0 @@ -10,8 +8,7 @@ Document-oriented entities for the MongoDB stores. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security + $(PackageTags);mongodb;models diff --git a/src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj b/src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj index 9cb05ac78..3ef8d6292 100644 --- a/src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj +++ b/src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj @@ -1,17 +1,14 @@  - - - netstandard2.0 + netstandard2.0;netstandard2.1 false false MongoDB stores for OpenIddict. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security + $(PackageTags);mongodb diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs index bb932d85e..d944d8601 100644 --- a/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbBuilder.cs @@ -24,14 +24,7 @@ public class OpenIddictMongoDbBuilder /// /// The services collection. public OpenIddictMongoDbBuilder([NotNull] IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - Services = services; - } + => Services = services ?? throw new ArgumentNullException(nameof(services)); /// /// Gets the services collection. diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs index 4accb1fe8..81dbead43 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs @@ -5,7 +5,6 @@ */ using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs index c5286161b..5fa49acac 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs @@ -5,7 +5,6 @@ */ using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs index cacd8d1b4..fbb90698f 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs @@ -5,7 +5,6 @@ */ using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; diff --git a/src/OpenIddict.Mvc/OpenIddict.Mvc.csproj b/src/OpenIddict.Mvc/OpenIddict.Mvc.csproj deleted file mode 100644 index 84b356e6b..000000000 --- a/src/OpenIddict.Mvc/OpenIddict.Mvc.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - netstandard2.0 - - - - OpenIddict binders for ASP.NET Core MVC. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security - - - - - - - - - - - - diff --git a/src/OpenIddict.Mvc/OpenIddictMvcBinder.cs b/src/OpenIddict.Mvc/OpenIddictMvcBinder.cs deleted file mode 100644 index ce125a58a..000000000 --- a/src/OpenIddict.Mvc/OpenIddictMvcBinder.cs +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Text; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Primitives; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace OpenIddict.Mvc -{ - /// - /// Represents an ASP.NET Core MVC model binder that is able to bind - /// and instances. - /// - public class OpenIddictMvcBinder : IModelBinder - { - private readonly IOptionsMonitor _options; - - /// - /// Creates a new instance of the class. - /// and instances. - /// - public OpenIddictMvcBinder([NotNull] IOptionsMonitor options) - => _options = options; - - /// - /// Tries to bind a model from the request. - /// - /// The model binding context. - /// A representing the asynchronous operation. - public Task BindModelAsync([NotNull] ModelBindingContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.ModelType == typeof(OpenIdConnectRequest)) - { - var request = context.HttpContext.GetOpenIdConnectRequest(); - if (request == null && !_options.CurrentValue.DisableBindingExceptions) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The OpenID Connect request cannot be retrieved from the ASP.NET context.") - .Append("Make sure that 'app.UseAuthentication()' is called before 'app.UseMvc()' ") - .Append("and that the action route corresponds to the endpoint path registered via ") - .Append("'services.AddOpenIddict().AddServer().Enable[...]Endpoint(...)'.") - .ToString()); - } - - if (request != null) - { - // Add a new validation state entry to prevent the built-in - // model validators from validating the OpenID Connect request. - context.ValidationState.Add(request, new ValidationStateEntry - { - SuppressValidation = true - }); - } - - context.BindingSource = BindingSource.Special; - context.Result = ModelBindingResult.Success(request); - - return Task.CompletedTask; - } - - else if (context.ModelType == typeof(OpenIdConnectResponse)) - { - var response = context.HttpContext.GetOpenIdConnectResponse(); - if (response != null) - { - // Add a new validation state entry to prevent the built-in - // model validators from validating the OpenID Connect response. - context.ValidationState.Add(response, new ValidationStateEntry - { - SuppressValidation = true - }); - } - - context.BindingSource = BindingSource.Special; - context.Result = ModelBindingResult.Success(response); - - return Task.CompletedTask; - } - - throw new InvalidOperationException("The specified model type is not supported by this binder."); - } - } -} diff --git a/src/OpenIddict.Mvc/OpenIddictMvcBinderProvider.cs b/src/OpenIddict.Mvc/OpenIddictMvcBinderProvider.cs deleted file mode 100644 index c5d1905b3..000000000 --- a/src/OpenIddict.Mvc/OpenIddictMvcBinderProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using AspNet.Security.OpenIdConnect.Primitives; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; - -namespace OpenIddict.Mvc -{ - /// - /// Represents an ASP.NET Core MVC model binder provider that is able to provide instances - /// of for the OpenID Connect server primitives. - /// - public class OpenIddictMvcBinderProvider : IModelBinderProvider - { - /// - /// Tries to resolve the model binder corresponding to the given model. - /// - /// The model binding context. - /// The current instance or null if the model is not supported. - public IModelBinder GetBinder([NotNull] ModelBinderProviderContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.Metadata.ModelType == typeof(OpenIdConnectRequest) || - context.Metadata.ModelType == typeof(OpenIdConnectResponse)) - { - return new BinderTypeModelBinder(typeof(OpenIddictMvcBinder)); - } - - return null; - } - } -} diff --git a/src/OpenIddict.Mvc/OpenIddictMvcConfiguration.cs b/src/OpenIddict.Mvc/OpenIddictMvcConfiguration.cs deleted file mode 100644 index 8a344ab7d..000000000 --- a/src/OpenIddict.Mvc/OpenIddictMvcConfiguration.cs +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using AspNet.Security.OpenIdConnect.Primitives; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; -using Microsoft.Extensions.Options; - -namespace OpenIddict.Mvc -{ - /// - /// Contains the methods required to ensure that the OpenIddict MVC configuration is valid. - /// - public class OpenIddictMvcConfiguration : IConfigureOptions - { - /// - /// Registers the OpenIddict MVC components in the MVC options. - /// - /// The options instance to initialize. - public void Configure([NotNull] MvcOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - options.ModelBinderProviders.Insert(0, new OpenIddictMvcBinderProvider()); - - options.ModelMetadataDetailsProviders.Add( - new BindingSourceMetadataProvider(typeof(OpenIdConnectRequest), BindingSource.Special)); - options.ModelMetadataDetailsProviders.Add( - new BindingSourceMetadataProvider(typeof(OpenIdConnectResponse), BindingSource.Special)); - - options.ModelMetadataDetailsProviders.Add( - new SuppressChildValidationMetadataProvider(typeof(OpenIdConnectRequest))); - options.ModelMetadataDetailsProviders.Add( - new SuppressChildValidationMetadataProvider(typeof(OpenIdConnectResponse))); - } - } -} diff --git a/src/OpenIddict.Mvc/OpenIddictMvcExtensions.cs b/src/OpenIddict.Mvc/OpenIddictMvcExtensions.cs deleted file mode 100644 index 140891349..000000000 --- a/src/OpenIddict.Mvc/OpenIddictMvcExtensions.cs +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; -using OpenIddict.Mvc; - -namespace Microsoft.Extensions.DependencyInjection -{ - /// - /// Exposes extensions allowing to register the OpenIddict MVC services. - /// - public static class OpenIddictMvcExtensions - { - /// - /// Registers the ASP.NET Core MVC services used by OpenIddict. - /// - /// The services builder used by OpenIddict to register new services. - /// This extension can be safely called multiple times. - /// The . - public static OpenIddictMvcBuilder UseMvc([NotNull] this OpenIddictServerBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - // Register the options initializer used by the OpenIddict MVC binding/validation components. - // Note: TryAddEnumerable() is used here to ensure the initializer is only registered once. - builder.Services.TryAddEnumerable( - ServiceDescriptor.Singleton, OpenIddictMvcConfiguration>()); - - return new OpenIddictMvcBuilder(builder.Services); - } - - /// - /// Registers the ASP.NET Core MVC model binders used by OpenIddict. - /// - /// The services builder used by OpenIddict to register new services. - /// The configuration delegate used to configure the MVC services. - /// This extension can be safely called multiple times. - /// The . - public static OpenIddictServerBuilder UseMvc( - [NotNull] this OpenIddictServerBuilder builder, - [NotNull] Action configuration) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - configuration(builder.UseMvc()); - - return builder; - } - } -} diff --git a/src/OpenIddict.Mvc/OpenIddictMvcOptions.cs b/src/OpenIddict.Mvc/OpenIddictMvcOptions.cs deleted file mode 100644 index e4e1e820f..000000000 --- a/src/OpenIddict.Mvc/OpenIddictMvcOptions.cs +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using AspNet.Security.OpenIdConnect.Primitives; - -namespace OpenIddict.Mvc -{ - /// - /// Provides various settings needed to configure the OpenIddict MVC integration. - /// - public class OpenIddictMvcOptions - { - /// - /// Gets or sets a boolean indicating whether the OpenIddict MVC binder should throw - /// an exception when it is unable to bind - /// parameters (e.g because the endpoint is not an OpenID Connect endpoint). - /// If exceptions are disabled, the model is automatically set to null. - /// - public bool DisableBindingExceptions { get; set; } - } -} diff --git a/src/OpenIddict.NHibernate.Models/OpenIddict.NHibernate.Models.csproj b/src/OpenIddict.NHibernate.Models/OpenIddict.NHibernate.Models.csproj index c27210f70..5318b8517 100644 --- a/src/OpenIddict.NHibernate.Models/OpenIddict.NHibernate.Models.csproj +++ b/src/OpenIddict.NHibernate.Models/OpenIddict.NHibernate.Models.csproj @@ -1,6 +1,4 @@ - - - + netstandard2.0 @@ -8,8 +6,7 @@ Relational entities for the NHibernate 5.x stores. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security + $(PackageTags);nhibernate;models diff --git a/src/OpenIddict.NHibernate/OpenIddict.NHibernate.csproj b/src/OpenIddict.NHibernate/OpenIddict.NHibernate.csproj index cb2c7c6bf..4050febf2 100644 --- a/src/OpenIddict.NHibernate/OpenIddict.NHibernate.csproj +++ b/src/OpenIddict.NHibernate/OpenIddict.NHibernate.csproj @@ -1,15 +1,12 @@  - - - netstandard2.0 + netstandard2.0;netstandard2.1 NHibernate 5.x stores for OpenIddict. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security + $(PackageTags);nhibernate diff --git a/src/OpenIddict.NHibernate/OpenIddictNHibernateBuilder.cs b/src/OpenIddict.NHibernate/OpenIddictNHibernateBuilder.cs index 5bb79dfe4..491c63515 100644 --- a/src/OpenIddict.NHibernate/OpenIddictNHibernateBuilder.cs +++ b/src/OpenIddict.NHibernate/OpenIddictNHibernateBuilder.cs @@ -24,14 +24,7 @@ public class OpenIddictNHibernateBuilder /// /// The services collection. public OpenIddictNHibernateBuilder([NotNull] IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - Services = services; - } + => Services = services ?? throw new ArgumentNullException(nameof(services)); /// /// Gets the services collection. diff --git a/src/OpenIddict.NHibernate/Resolvers/OpenIddictApplicationStoreResolver.cs b/src/OpenIddict.NHibernate/Resolvers/OpenIddictApplicationStoreResolver.cs index 3d91cdad9..90a6efcad 100644 --- a/src/OpenIddict.NHibernate/Resolvers/OpenIddictApplicationStoreResolver.cs +++ b/src/OpenIddict.NHibernate/Resolvers/OpenIddictApplicationStoreResolver.cs @@ -10,7 +10,6 @@ using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using OpenIddict.Abstractions; -using OpenIddict.Core; using OpenIddict.Extensions; using OpenIddict.NHibernate.Models; diff --git a/src/OpenIddict.NHibernate/Resolvers/OpenIddictAuthorizationStoreResolver.cs b/src/OpenIddict.NHibernate/Resolvers/OpenIddictAuthorizationStoreResolver.cs index 4960e2240..eca5a5a01 100644 --- a/src/OpenIddict.NHibernate/Resolvers/OpenIddictAuthorizationStoreResolver.cs +++ b/src/OpenIddict.NHibernate/Resolvers/OpenIddictAuthorizationStoreResolver.cs @@ -10,7 +10,6 @@ using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using OpenIddict.Abstractions; -using OpenIddict.Core; using OpenIddict.Extensions; using OpenIddict.NHibernate.Models; diff --git a/src/OpenIddict.NHibernate/Resolvers/OpenIddictScopeStoreResolver.cs b/src/OpenIddict.NHibernate/Resolvers/OpenIddictScopeStoreResolver.cs index cdf94b9c0..1d29e5b3e 100644 --- a/src/OpenIddict.NHibernate/Resolvers/OpenIddictScopeStoreResolver.cs +++ b/src/OpenIddict.NHibernate/Resolvers/OpenIddictScopeStoreResolver.cs @@ -10,7 +10,6 @@ using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using OpenIddict.Abstractions; -using OpenIddict.Core; using OpenIddict.Extensions; using OpenIddict.NHibernate.Models; diff --git a/src/OpenIddict.NHibernate/Resolvers/OpenIddictTokenStoreResolver.cs b/src/OpenIddict.NHibernate/Resolvers/OpenIddictTokenStoreResolver.cs index aeb52f4e6..5840e9b48 100644 --- a/src/OpenIddict.NHibernate/Resolvers/OpenIddictTokenStoreResolver.cs +++ b/src/OpenIddict.NHibernate/Resolvers/OpenIddictTokenStoreResolver.cs @@ -10,7 +10,6 @@ using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using OpenIddict.Abstractions; -using OpenIddict.Core; using OpenIddict.Extensions; using OpenIddict.NHibernate.Models; diff --git a/src/OpenIddict.Owin/OpenIddict.Owin.csproj b/src/OpenIddict.Owin/OpenIddict.Owin.csproj new file mode 100644 index 000000000..234dfcc40 --- /dev/null +++ b/src/OpenIddict.Owin/OpenIddict.Owin.csproj @@ -0,0 +1,17 @@ + + + + net461;net472 + + + + Easy-to-use OpenID Connect stack for ASP.NET 4.x/OWIN. + $(PackageTags);aspnet;katana;owin;server;validation + + + + + + + + diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddict.Server.AspNetCore.csproj b/src/OpenIddict.Server.AspNetCore/OpenIddict.Server.AspNetCore.csproj new file mode 100644 index 000000000..e6e0e0746 --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddict.Server.AspNetCore.csproj @@ -0,0 +1,25 @@ + + + + netcoreapp3.0 + + + + ASP.NET Core integration package for the OpenIddict server services. + $(PackageTags);server;aspnetcore + + + + + + + + + + + + + + + + diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs new file mode 100644 index 000000000..51078ed8b --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs @@ -0,0 +1,160 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using JetBrains.Annotations; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Distributed; +using OpenIddict.Server.AspNetCore; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Exposes the necessary methods required to configure + /// the OpenIddict server ASP.NET Core integration. + /// + public class OpenIddictServerAspNetCoreBuilder + { + /// + /// Initializes a new instance of . + /// + /// The services collection. + public OpenIddictServerAspNetCoreBuilder([NotNull] IServiceCollection services) + => Services = services ?? throw new ArgumentNullException(nameof(services)); + + /// + /// Gets the services collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IServiceCollection Services { get; } + + /// + /// Amends the default OpenIddict server ASP.NET Core configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public OpenIddictServerAspNetCoreBuilder Configure([NotNull] Action configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(configuration); + + return this; + } + + /// + /// Disables the transport security requirement (HTTPS) during development. + /// + /// The . + public OpenIddictServerAspNetCoreBuilder DisableTransportSecurityRequirement() + => Configure(options => options.DisableTransportSecurityRequirement = true); + + /// + /// Enables the pass-through mode for the OpenID Connect authorization endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + /// The . + public OpenIddictServerAspNetCoreBuilder EnableAuthorizationEndpointPassthrough() + => Configure(options => options.EnableAuthorizationEndpointPassthrough = true); + + /// + /// Enables error pass-through support, so that the rest of the request processing pipeline is + /// automatically invoked when returning an error from the interactive authorization and logout endpoints. + /// When this option is enabled, special logic must be added to these actions to handle errors, that can be + /// retrieved using + /// + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictServerAspNetCoreBuilder EnableErrorPassthrough() + => Configure(options => options.EnableErrorPassthrough = true); + + /// + /// Enables the pass-through mode for the OpenID Connect token endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + /// The . + public OpenIddictServerAspNetCoreBuilder EnableTokenEndpointPassthrough() + => Configure(options => options.EnableTokenEndpointPassthrough = true); + + /// + /// Enables the pass-through mode for the OpenID Connect userinfo endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + /// The . + public OpenIddictServerAspNetCoreBuilder EnableUserinfoEndpointPassthrough() + => Configure(options => options.EnableUserinfoEndpointPassthrough = true); + + /// + /// Enables request caching, so that both authorization and logout requests + /// are automatically stored in the distributed cache, which allows flowing + /// large payloads across requests. Enabling this option is recommended + /// when using external authentication providers or when large GET or POST + /// OpenID Connect authorization requests support is required. + /// + /// The . + public OpenIddictServerAspNetCoreBuilder EnableRequestCaching() + => Configure(options => options.EnableRequestCaching = true); + + /// + /// Enables status code pages integration support. Once enabled, errors generated + /// by the interactive authorization and logout endpoints can be handled by ASP.NET Core. + /// + /// The . + public OpenIddictServerAspNetCoreBuilder EnableStatusCodePagesIntegration() + => Configure(options => options.EnableStatusCodePagesIntegration = true); + + /// + /// Sets the caching policy used to determine how long the authorization and + /// end session requests should be cached by the distributed cache implementation. + /// Note: the specified policy is only used when request caching is explicitly enabled. + /// + /// The request caching policy. + /// The . + public OpenIddictServerAspNetCoreBuilder SetRequestCachingPolicy([NotNull] DistributedCacheEntryOptions policy) + { + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + return Configure(options => options.RequestCachingPolicy = policy); + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals([CanBeNull] object obj) => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => base.GetHashCode(); + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string ToString() => base.ToString(); + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs new file mode 100644 index 000000000..049f14ffb --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs @@ -0,0 +1,101 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Diagnostics; +using System.Text; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; + +namespace OpenIddict.Server.AspNetCore +{ + /// + /// Contains the methods required to ensure that the OpenIddict server configuration is valid. + /// + public class OpenIddictServerAspNetCoreConfiguration : IConfigureOptions, + IConfigureNamedOptions, + IPostConfigureOptions + { + /// + /// Registers the OpenIddict server handler in the global authentication options. + /// + /// The options instance to initialize. + public void Configure([NotNull] AuthenticationOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + // If a handler was already registered and the type doesn't correspond to the OpenIddict handler, throw an exception. + if (options.SchemeMap.TryGetValue(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, out var builder) && + builder.HandlerType != typeof(OpenIddictServerAspNetCoreHandler)) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The OpenIddict ASP.NET Core server handler cannot be registered as an authentication scheme.") + .Append("This may indicate that an instance of another handler was registered with the same scheme.") + .ToString()); + } + + options.AddScheme( + OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, displayName: null); + } + + public void Configure([NotNull] OpenIddictServerOptions options) + => Debug.Fail("This infrastructure method shouldn't be called"); + + public void Configure([CanBeNull] string name, [NotNull] OpenIddictServerOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + // Register the built-in event handlers used by the OpenIddict ASP.NET Core server components. + foreach (var handler in OpenIddictServerAspNetCoreHandlers.DefaultHandlers) + { + options.DefaultHandlers.Add(handler); + } + } + + /// + /// Ensures that the authentication configuration is in a consistent and valid state. + /// + /// The authentication scheme associated with the handler instance. + /// The options instance to initialize. + public void PostConfigure([CanBeNull] string name, [NotNull] AuthenticationOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + bool TryValidate(string scheme) + { + // If the scheme was not set or if it cannot be found in the map, return true. + if (string.IsNullOrEmpty(scheme) || !options.SchemeMap.TryGetValue(scheme, out var builder)) + { + return true; + } + + return builder.HandlerType != typeof(OpenIddictServerAspNetCoreHandler); + } + + if (!TryValidate(options.DefaultAuthenticateScheme) || !TryValidate(options.DefaultChallengeScheme) || + !TryValidate(options.DefaultForbidScheme) || !TryValidate(options.DefaultScheme) || + !TryValidate(options.DefaultSignInScheme) || !TryValidate(options.DefaultSignOutScheme)) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The OpenIddict ASP.NET Core server cannot be used as the default scheme handler.") + .Append("Make sure that neither DefaultAuthenticateScheme, DefaultChallengeScheme, ") + .Append("DefaultForbidScheme, DefaultSignInScheme, DefaultSignOutScheme nor DefaultScheme ") + .Append("point to an instance of the OpenIddict server handler.") + .ToString()); + } + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs new file mode 100644 index 000000000..a9b55b238 --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +namespace OpenIddict.Server.AspNetCore +{ + /// + /// Exposes common constants used by the OpenIddict ASP.NET Core host. + /// + public static class OpenIddictServerAspNetCoreConstants + { + public static class Cache + { + public const string AuthorizationRequest = "openiddict-authorization-request:"; + public const string LogoutRequest = "openiddict-logout-request:"; + } + + public static class Properties + { + public const string Error = ".error"; + public const string ErrorDescription = ".error_description"; + public const string ErrorUri = ".error_uri"; + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerDefaults.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreDefaults.cs similarity index 71% rename from src/OpenIddict.Server/OpenIddictServerDefaults.cs rename to src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreDefaults.cs index 09e4e675f..19f17b6ce 100644 --- a/src/OpenIddict.Server/OpenIddictServerDefaults.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreDefaults.cs @@ -4,19 +4,18 @@ * the license and the contributors participating to this project. */ -using AspNet.Security.OpenIdConnect.Server; using Microsoft.AspNetCore.Authentication; -namespace OpenIddict.Server +namespace OpenIddict.Server.AspNetCore { /// /// Exposes the default values used by the OpenIddict server handler. /// - public static class OpenIddictServerDefaults + public static class OpenIddictServerAspNetCoreDefaults { /// /// Default value for . /// - public const string AuthenticationScheme = OpenIdConnectServerDefaults.AuthenticationScheme; + public const string AuthenticationScheme = "OpenIddict.Server.AspNetCore"; } } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs new file mode 100644 index 000000000..0e0457227 --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs @@ -0,0 +1,94 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using OpenIddict.Server; +using OpenIddict.Server.AspNetCore; +using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters; +using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Exposes extensions allowing to register the OpenIddict server services. + /// + public static class OpenIddictServerAspNetCoreExtensions + { + /// + /// Registers the OpenIddict server services for ASP.NET Core in the DI container. + /// + /// The services builder used by OpenIddict to register new services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictServerAspNetCoreBuilder UseAspNetCore([NotNull] this OpenIddictServerBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Services.AddAuthentication(); + + builder.Services.TryAddScoped(); + + // Register the built-in event handlers used by the OpenIddict ASP.NET Core server components. + // Note: the order used here is not important, as the actual order is set in the options. + builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); + + // Register the built-in filters used by the default OpenIddict ASP.NET Core server event handlers. + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + + // Register the option initializer used by the OpenIddict ASP.NET Core server integration services. + // Note: TryAddEnumerable() is used here to ensure the initializers are only registered once. + builder.Services.TryAddEnumerable(new[] + { + ServiceDescriptor.Singleton, OpenIddictServerAspNetCoreConfiguration>(), + ServiceDescriptor.Singleton, OpenIddictServerAspNetCoreConfiguration>(), + + ServiceDescriptor.Singleton, OpenIddictServerAspNetCoreConfiguration>() + }); + + return new OpenIddictServerAspNetCoreBuilder(builder.Services); + } + + /// + /// Registers the OpenIddict server services for ASP.NET Core in the DI container. + /// + /// The services builder used by OpenIddict to register new services. + /// The configuration delegate used to configure the server services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictServerBuilder UseAspNetCore( + [NotNull] this OpenIddictServerBuilder builder, + [NotNull] Action configuration) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + configuration(builder.UseAspNetCore()); + + return builder; + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreFeature.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreFeature.cs new file mode 100644 index 000000000..46b8e1d04 --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreFeature.cs @@ -0,0 +1,20 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +namespace OpenIddict.Server.AspNetCore +{ + /// + /// Exposes the current server transaction to the ASP.NET Core host. + /// + public class OpenIddictServerAspNetCoreFeature + { + /// + /// Gets or sets the server transaction that encapsulates all specific + /// information about an individual OpenID Connect server request. + /// + public OpenIddictServerTransaction Transaction { get; set; } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs new file mode 100644 index 000000000..8fb0d5966 --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs @@ -0,0 +1,396 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using Properties = OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants.Properties; + +namespace OpenIddict.Server.AspNetCore +{ + /// + /// Provides the logic necessary to extract, validate and handle OpenID Connect requests. + /// + public class OpenIddictServerAspNetCoreHandler : AuthenticationHandler, + IAuthenticationRequestHandler, + IAuthenticationSignInHandler, + IAuthenticationSignOutHandler + { + private readonly IOpenIddictServerProvider _provider; + + /// + /// Creates a new instance of the class. + /// + public OpenIddictServerAspNetCoreHandler( + [NotNull] IOpenIddictServerProvider provider, + [NotNull] IOptionsMonitor options, + [NotNull] ILoggerFactory logger, + [NotNull] UrlEncoder encoder, + [NotNull] ISystemClock clock) + : base(options, logger, encoder, clock) + => _provider = provider; + + public async Task HandleRequestAsync() + { + // Note: the transaction may be already attached when replaying an ASP.NET Core request + // (e.g when using the built-in status code pages middleware with the re-execute mode). + var transaction = Context.Features.Get()?.Transaction; + if (transaction == null) + { + // Create a new transaction and attach the HTTP request to make it available to the ASP.NET Core handlers. + transaction = await _provider.CreateTransactionAsync(); + transaction.Properties[typeof(HttpRequest).FullName] = new WeakReference(Request); + + // Attach the OpenIddict server transaction to the ASP.NET Core features + // so that it can retrieved while performing sign-in/sign-out operations. + Context.Features.Set(new OpenIddictServerAspNetCoreFeature { Transaction = transaction }); + } + + var context = new ProcessRequestContext(transaction); + await _provider.DispatchAsync(context); + + if (context.IsRequestHandled) + { + return true; + } + + else if (context.IsRequestSkipped) + { + return false; + } + + else if (context.IsRejected) + { + var notification = new ProcessErrorResponseContext(transaction) + { + Response = new OpenIddictResponse + { + Error = context.Error ?? Errors.InvalidRequest, + ErrorDescription = context.ErrorDescription, + ErrorUri = context.ErrorUri + } + }; + + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + return true; + } + + else if (notification.IsRequestSkipped) + { + return false; + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The OpenID Connect response was not correctly processed. This may indicate ") + .Append("that the event handler responsible of processing OpenID Connect responses ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + return false; + } + + protected override async Task HandleAuthenticateAsync() + { + var transaction = Context.Features.Get()?.Transaction; + if (transaction?.Request == null) + { + throw new InvalidOperationException("An identity cannot be extracted from this request."); + } + + switch (transaction.EndpointType) + { + case OpenIddictServerEndpointType.Authorization: + case OpenIddictServerEndpointType.Logout: + { + if (string.IsNullOrEmpty(transaction.Request.IdTokenHint)) + { + return AuthenticateResult.NoResult(); + } + + var notification = new DeserializeIdentityTokenContext(transaction) + { + Token = transaction.Request.IdTokenHint + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The identity token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating identity tokens ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + if (notification.Principal == null) + { + Logger.LogWarning("The identity token extracted from the 'id_token_hint' " + + "parameter was invalid or malformed and was ignored."); + + return AuthenticateResult.NoResult(); + } + + // Note: tickets are returned even if they are considered invalid (e.g expired). + + return AuthenticateResult.Success(new AuthenticationTicket( + notification.Principal, + OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)); + } + + case OpenIddictServerEndpointType.Token when transaction.Request.IsAuthorizationCodeGrantType(): + { + // Note: this method can be called from the ApplyTokenResponse event, + // which may be invoked for a missing authorization code/refresh token. + if (string.IsNullOrEmpty(transaction.Request.Code)) + { + return AuthenticateResult.NoResult(); + } + + var notification = new DeserializeAuthorizationCodeContext(transaction) + { + Token = transaction.Request.Code + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The authorization code was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating authorization codes ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + if (notification.Principal == null) + { + Logger.LogWarning("The authorization code extracted from the token request was invalid and was ignored."); + + return AuthenticateResult.NoResult(); + } + + // Note: tickets are returned even if they are considered invalid (e.g expired). + + return AuthenticateResult.Success(new AuthenticationTicket( + notification.Principal, + OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)); + } + + case OpenIddictServerEndpointType.Token when transaction.Request.IsRefreshTokenGrantType(): + { + if (string.IsNullOrEmpty(transaction.Request.RefreshToken)) + { + return AuthenticateResult.NoResult(); + } + + var notification = new DeserializeRefreshTokenContext(transaction) + { + Token = transaction.Request.RefreshToken + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The refresh token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating refresh tokens ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + if (notification.Principal == null) + { + Logger.LogWarning("The refresh token extracted from the token request was invalid and was ignored."); + + return AuthenticateResult.NoResult(); + } + + // Note: tickets are returned even if they are considered invalid (e.g expired). + + return AuthenticateResult.Success(new AuthenticationTicket( + notification.Principal, + OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)); + } + + default: throw new InvalidOperationException("An identity cannot be extracted from this request."); + } + } + + protected override async Task HandleChallengeAsync([CanBeNull] AuthenticationProperties properties) + { + var transaction = Context.Features.Get()?.Transaction; + if (transaction == null) + { + throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); + } + + var context = new ProcessChallengeResponseContext(transaction) + { + Response = new OpenIddictResponse + { + Error = GetProperty(properties, Properties.Error), + ErrorDescription = GetProperty(properties, Properties.ErrorDescription), + ErrorUri = GetProperty(properties, Properties.ErrorUri) + } + }; + + await _provider.DispatchAsync(context); + + if (context.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + else if (context.IsRejected) + { + var notification = new ProcessErrorResponseContext(transaction) + { + Response = new OpenIddictResponse + { + Error = context.Error ?? Errors.InvalidRequest, + ErrorDescription = context.ErrorDescription, + ErrorUri = context.ErrorUri + } + }; + + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The OpenID Connect response was not correctly processed. This may indicate ") + .Append("that the event handler responsible of processing OpenID Connect responses ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + static string GetProperty(AuthenticationProperties properties, string name) + => properties != null && properties.Items.TryGetValue(name, out string value) ? value : null; + } + + protected override Task HandleForbiddenAsync([CanBeNull] AuthenticationProperties properties) + => HandleChallengeAsync(properties); + + public async Task SignInAsync([NotNull] ClaimsPrincipal user, [CanBeNull] AuthenticationProperties properties) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var transaction = Context.Features.Get()?.Transaction; + if (transaction == null) + { + throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); + } + + var context = new ProcessSigninResponseContext(transaction) + { + Principal = user, + Response = new OpenIddictResponse() + }; + + await _provider.DispatchAsync(context); + + if (context.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + else if (context.IsRejected) + { + var notification = new ProcessErrorResponseContext(transaction) + { + Response = new OpenIddictResponse + { + Error = context.Error ?? Errors.InvalidRequest, + ErrorDescription = context.ErrorDescription, + ErrorUri = context.ErrorUri + } + }; + + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The OpenID Connect response was not correctly processed. This may indicate ") + .Append("that the event handler responsible of processing OpenID Connect responses ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + } + + public async Task SignOutAsync([CanBeNull] AuthenticationProperties properties) + { + var transaction = Context.Features.Get()?.Transaction; + if (transaction == null) + { + throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); + } + + var context = new ProcessSignoutResponseContext(transaction) + { + Response = new OpenIddictResponse() + }; + + await _provider.DispatchAsync(context); + + if (context.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + else if (context.IsRejected) + { + var notification = new ProcessErrorResponseContext(transaction) + { + Response = new OpenIddictResponse + { + Error = context.Error ?? Errors.InvalidRequest, + ErrorDescription = context.ErrorDescription, + ErrorUri = context.ErrorUri + } + }; + + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The OpenID Connect response was not correctly processed. This may indicate ") + .Append("that the event handler responsible of processing OpenID Connect responses ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs new file mode 100644 index 000000000..c0402351c --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs @@ -0,0 +1,189 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore; +using Microsoft.Extensions.Options; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server.AspNetCore +{ + /// + /// Contains a collection of event handler filters commonly used by the ASP.NET Core handlers. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static class OpenIddictServerAspNetCoreHandlerFilters + { + /// + /// Represents a filter that excludes the associated handlers if the + /// pass-through mode was not enabled for the authorization endpoint. + /// + public class RequireAuthorizationEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireAuthorizationEndpointPassthroughEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.EnableAuthorizationEndpointPassthrough); + } + } + + /// + /// Represents a filter that excludes the associated handlers if error pass-through was not enabled. + /// + public class RequireErrorPassthroughEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireErrorPassthroughEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.EnableErrorPassthrough); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no ASP.NET Core request can be found. + /// + public class RequireHttpRequest : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(context.Transaction.GetHttpRequest() != null); + } + } + + /// + /// Represents a filter that excludes the associated handlers if the HTTPS requirement was disabled. + /// + public class RequireTransportSecurityRequirementEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireTransportSecurityRequirementEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(!_options.CurrentValue.DisableTransportSecurityRequirement); + } + } + + /// + /// Represents a filter that excludes the associated handlers if request caching was not enabled. + /// + public class RequireRequestCachingEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireRequestCachingEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.EnableRequestCaching); + } + } + + /// + /// Represents a filter that excludes the associated handlers if status code pages support was not enabled. + /// + public class RequireStatusCodePagesIntegrationEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireStatusCodePagesIntegrationEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.EnableStatusCodePagesIntegration); + } + } + + /// + /// Represents a filter that excludes the associated handlers if the + /// pass-through mode was not enabled for the authorization endpoint. + /// + public class RequireTokenEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireTokenEndpointPassthroughEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.EnableTokenEndpointPassthrough); + } + } + + /// + /// Represents a filter that excludes the associated handlers if the + /// pass-through mode was not enabled for the userinfo endpoint. + /// + public class RequireUserinfoEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireUserinfoEndpointPassthroughEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.EnableUserinfoEndpointPassthrough); + } + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs new file mode 100644 index 000000000..3488a3690 --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs @@ -0,0 +1,793 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; +using Newtonsoft.Json; +using Newtonsoft.Json.Bson; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants; +using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers; + +namespace OpenIddict.Server.AspNetCore +{ + public static partial class OpenIddictServerAspNetCoreHandlers + { + public static class Authentication + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Authorization request extraction: + */ + ExtractGetOrPostRequest.Descriptor, + RestoreCachedRequestParameters.Descriptor, + CacheRequestParameters.Descriptor, + + /* + * Authorization request handling: + */ + EnablePassthroughMode.Descriptor, + + /* + * Authorization response processing: + */ + RemoveCachedRequest.Descriptor, + ProcessFormPostResponse.Descriptor, + ProcessQueryResponse.Descriptor, + ProcessStatusCodePagesErrorResponse.Descriptor, + ProcessPassthroughErrorResponse.Descriptor, + ProcessLocalErrorResponse.Descriptor); + + /// + /// Contains the logic responsible of restoring cached requests from the request_id, if specified. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class RestoreCachedRequestParameters : IOpenIddictServerHandler + { + private readonly IDistributedCache _cache; + + public RestoreCachedRequestParameters() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("A distributed cache instance must be registered when enabling request caching.") + .Append("To register the default in-memory distributed cache implementation, reference the ") + .Append("'Microsoft.Extensions.Caching.Memory' package and call ") + .Append("'services.AddDistributedMemoryCache()' from 'ConfigureServices'.") + .ToString()); + + public RestoreCachedRequestParameters([NotNull] IDistributedCache cache) + => _cache = cache; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ExtractGetOrPostRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ExtractAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a request_id parameter can be found in the authorization request, + // restore the complete authorization request from the distributed cache. + + if (string.IsNullOrEmpty(context.Request.RequestId)) + { + return; + } + + // Note: the cache key is always prefixed with a specific marker + // to avoid collisions with the other types of cached payloads. + var payload = await _cache.GetAsync(Cache.AuthorizationRequest + context.Request.RequestId); + if (payload == null) + { + context.Logger.LogError("The authorization request was rejected because an unknown " + + "or invalid request_id parameter was specified."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'request_id' parameter is invalid."); + + return; + } + + // Restore the authorization request parameters from the serialized payload. + using var reader = new BsonDataReader(new MemoryStream(payload)); + foreach (var parameter in JObject.Load(reader)) + { + // Avoid overriding the current request parameters. + if (context.Request.HasParameter(parameter.Key)) + { + continue; + } + + context.Request.SetParameter(parameter.Key, parameter.Value); + } + } + } + + /// + /// Contains the logic responsible of caching authorization requests, if applicable. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class CacheRequestParameters : IOpenIddictServerHandler + { + private readonly IDistributedCache _cache; + private readonly IOptionsMonitor _options; + + public CacheRequestParameters() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("A distributed cache instance must be registered when enabling request caching.") + .Append("To register the default in-memory distributed cache implementation, reference the ") + .Append("'Microsoft.Extensions.Caching.Memory' package and call ") + .Append("'services.AddDistributedMemoryCache()' from 'ConfigureServices'.") + .ToString()); + + public CacheRequestParameters( + [NotNull] IDistributedCache cache, + [NotNull] IOptionsMonitor options) + { + _cache = cache; + _options = options; + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(RestoreCachedRequestParameters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ExtractAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + // Don't cache the request if the request doesn't include any parameter. + // If a request_id parameter can be found in the authorization request, + // ignore the following logic to prevent an infinite redirect loop. + if (context.Request.GetParameters().IsEmpty || !string.IsNullOrEmpty(context.Request.RequestId)) + { + return; + } + + // Generate a 256-bit request identifier using a crypto-secure random number generator. + var data = new byte[256 / 8]; + RandomNumberGenerator.Fill(data); + + context.Request.RequestId = Base64UrlEncoder.Encode(data); + + // Store the serialized authorization request parameters in the distributed cache. + var stream = new MemoryStream(); + using (var writer = new BsonDataWriter(stream)) + { + writer.CloseOutput = false; + + var serializer = JsonSerializer.CreateDefault(); + serializer.Serialize(writer, context.Request); + } + + // Note: the cache key is always prefixed with a specific marker + // to avoid collisions with the other types of cached payloads. + await _cache.SetAsync(Cache.AuthorizationRequest + context.Request.RequestId, + stream.ToArray(), _options.CurrentValue.RequestCachingPolicy); + + // Create a new GET authorization request containing only the request_id parameter. + var address = QueryHelpers.AddQueryString( + uri: request.Scheme + "://" + request.Host + request.PathBase + request.Path, + name: Parameters.RequestId, + value: context.Request.RequestId); + + request.HttpContext.Response.Redirect(address); + + // Mark the response as handled to skip the rest of the pipeline. + context.HandleRequest(); + } + } + + /// + /// Contains the logic responsible of enabling the pass-through mode for the received request. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class EnablePassthroughMode : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.SkipRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of removing cached authorization requests from the distributed cache. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class RemoveCachedRequest : IOpenIddictServerHandler + { + private readonly IDistributedCache _cache; + + public RemoveCachedRequest() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("A distributed cache instance must be registered when enabling request caching.") + .Append("To register the default in-memory distributed cache implementation, reference the ") + .Append("'Microsoft.Extensions.Caching.Memory' package and call ") + .Append("'services.AddDistributedMemoryCache()' from 'ConfigureServices'.") + .ToString()); + + public RemoveCachedRequest([NotNull] IDistributedCache cache) + => _cache = cache; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessFormPostResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.Request?.RequestId)) + { + return Task.CompletedTask; + } + + // Note: the ApplyAuthorizationResponse event is called for both successful + // and errored authorization responses but discrimination is not necessary here, + // as the authorization request must be removed from the distributed cache in both cases. + + // Note: the cache key is always prefixed with a specific marker + // to avoid collisions with the other types of cached payloads. + return _cache.RemoveAsync(Cache.AuthorizationRequest + context.Request.RequestId); + } + } + + /// + /// Contains the logic responsible of processing authorization responses using the form_post response mode. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessFormPostResponse : IOpenIddictServerHandler + { + private readonly HtmlEncoder _encoder; + + public ProcessFormPostResponse([NotNull] HtmlEncoder encoder) + => _encoder = encoder; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessQueryResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var response = context.Transaction.GetHttpRequest()?.HttpContext.Response; + if (response == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.RedirectUri) || + !string.Equals(context.ResponseMode, ResponseModes.FormPost, StringComparison.Ordinal)) + { + return; + } + + context.Logger.LogInformation("The authorization response was successfully returned to " + + "'{RedirectUri}' using the form post response mode: {Response}.", + context.RedirectUri, context.Response); + + using (var buffer = new MemoryStream()) + using (var writer = new StreamWriter(buffer)) + { + writer.WriteLine(""); + writer.WriteLine(""); + writer.WriteLine(""); + + // While the redirect_uri parameter should be guarded against unknown values, + // it's still safer to encode it to avoid cross-site scripting attacks + // if the authorization server has a relaxed policy concerning redirect URIs. + writer.WriteLine($@"
"); + + // Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters + // with the same name are used by derived drafts like the OAuth 2.0 token exchange specification. + // For consistency, multiple parameters with the same name are also supported by this endpoint. + foreach (var parameter in context.Response.GetFlattenedParameters()) + { + var key = _encoder.Encode(parameter.Key); + var value = _encoder.Encode(parameter.Value); + + writer.WriteLine($@""); + } + + writer.WriteLine(@""); + writer.WriteLine("
"); + writer.WriteLine(""); + writer.WriteLine(""); + writer.WriteLine(""); + writer.Flush(); + + response.StatusCode = 200; + response.ContentLength = buffer.Length; + response.ContentType = "text/html;charset=UTF-8"; + + response.Headers["Cache-Control"] = "no-cache"; + response.Headers["Pragma"] = "no-cache"; + response.Headers["Expires"] = "-1"; + + buffer.Seek(offset: 0, loc: SeekOrigin.Begin); + await buffer.CopyToAsync(response.Body, 4096); + } + + context.HandleRequest(); + } + } + + /// + /// Contains the logic responsible of processing authorization responses using the query response mode. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessQueryResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessFragmentResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var response = context.Transaction.GetHttpRequest()?.HttpContext.Response; + if (response == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.RedirectUri) || + !string.Equals(context.ResponseMode, ResponseModes.Query, StringComparison.Ordinal)) + { + return Task.CompletedTask; + } + + context.Logger.LogInformation("The authorization response was successfully returned to " + + "'{RedirectUri}' using the query response mode: {Response}.", + context.RedirectUri, context.Response); + + var location = context.RedirectUri; + + // Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters + // with the same name are used by derived drafts like the OAuth 2.0 token exchange specification. + // For consistency, multiple parameters with the same name are also supported by this endpoint. + foreach (var parameter in context.Response.GetFlattenedParameters()) + { + location = QueryHelpers.AddQueryString(location, parameter.Key, parameter.Value); + } + + response.Redirect(location); + context.HandleRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of processing authorization responses using the fragment response mode. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessFragmentResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessLocalErrorResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var response = context.Transaction.GetHttpRequest()?.HttpContext.Response; + if (response == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.RedirectUri) || + !string.Equals(context.ResponseMode, ResponseModes.Fragment, StringComparison.Ordinal)) + { + return Task.CompletedTask; + } + + context.Logger.LogInformation("The authorization response was successfully returned to " + + "'{RedirectUri}' using the fragment response mode: {Response}.", + context.RedirectUri, context.Response); + + var builder = new StringBuilder(context.RedirectUri); + + // Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters + // with the same name are used by derived drafts like the OAuth 2.0 token exchange specification. + // For consistency, multiple parameters with the same name are also supported by this endpoint. + foreach (var parameter in context.Response.GetFlattenedParameters()) + { + builder.Append(Contains(builder, '#') ? '&' : '#') + .Append(Uri.EscapeDataString(parameter.Key)) + .Append('=') + .Append(Uri.EscapeDataString(parameter.Value)); + } + + response.Redirect(builder.ToString()); + context.HandleRequest(); + + return Task.CompletedTask; + + static bool Contains(StringBuilder builder, char delimiter) + { + for (var index = 0; index < builder.Length; index++) + { + if (builder[index] == delimiter) + { + return true; + } + } + + return false; + } + } + } + + /// + /// Contains the logic responsible of processing authorization responses that must be handled by another + /// middleware in the pipeline at a later stage (e.g an ASP.NET Core MVC action or a NancyFX module). + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessPassthroughErrorResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessStatusCodePagesErrorResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var response = context.Transaction.GetHttpRequest()?.HttpContext.Response; + if (response == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.Response.Error) || !string.IsNullOrEmpty(context.RedirectUri)) + { + return Task.CompletedTask; + } + + // Apply a 400 status code by default. + response.StatusCode = 400; + + context.SkipRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of processing authorization responses handled by the status code pages middleware. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessStatusCodePagesErrorResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessLocalErrorResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Response == null) + { + throw new InvalidOperationException("This handler cannot be invoked without a response attached."); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var response = context.Transaction.GetHttpRequest()?.HttpContext.Response; + if (response == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.Error) || !string.IsNullOrEmpty(context.RedirectUri)) + { + return Task.CompletedTask; + } + + // Determine if the status code pages middleware has been enabled for this request. + // If it was not registered or enabled, let the default OpenIddict server handlers render + // a default error page instead of delegating the rendering to the status code middleware. + var feature = response.HttpContext.Features.Get(); + if (feature == null || !feature.Enabled) + { + return Task.CompletedTask; + } + + // Replace the default status code to return a 400 response. + response.StatusCode = 400; + + // Mark the request as fully handled to prevent the other OpenIddict server handlers + // from displaying the default error page and to allow the status code pages middleware + // to rewrite the response using the logic defined by the developer when registering it. + context.HandleRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of processing authorization responses that must be returned as plain-text. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessLocalErrorResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var response = context.Transaction.GetHttpRequest()?.HttpContext.Response; + if (response == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.Response.Error) || !string.IsNullOrEmpty(context.RedirectUri)) + { + return; + } + + // Don't return the state originally sent by the client application. + context.Response.State = null; + + // Apply a 400 status code by default. + response.StatusCode = 400; + + context.Logger.LogInformation("The authorization response was successfully returned " + + "as a plain-text document: {Response}.", context.Response); + + using (var buffer = new MemoryStream()) + using (var writer = new StreamWriter(buffer)) + { + foreach (var parameter in context.Response.GetParameters()) + { + // Ignore null or empty parameters, including JSON + // objects that can't be represented as strings. + var value = (string) parameter.Value; + if (string.IsNullOrEmpty(value)) + { + continue; + } + + writer.WriteLine("{0}:{1}", parameter.Key, value); + } + + writer.Flush(); + + response.ContentLength = buffer.Length; + response.ContentType = "text/plain;charset=UTF-8"; + + response.Headers[HeaderNames.CacheControl] = "no-cache"; + response.Headers[HeaderNames.Pragma] = "no-cache"; + response.Headers[HeaderNames.Expires] = "Thu, 01 Jan 1970 00:00:00 GMT"; + + buffer.Seek(offset: 0, loc: SeekOrigin.Begin); + await buffer.CopyToAsync(response.Body, 4096, response.HttpContext.RequestAborted); + } + + context.HandleRequest(); + } + } + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Discovery.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Discovery.cs new file mode 100644 index 000000000..98d53d7db --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Discovery.cs @@ -0,0 +1,108 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore; +using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server.AspNetCore +{ + public static partial class OpenIddictServerAspNetCoreHandlers + { + public static class Discovery + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Configuration request extraction: + */ + ExtractGetRequest.Descriptor, + + /* + * Configuration request handling: + */ + InferIssuerFromHost.Descriptor, + + /* + * Configuration response processing: + */ + ProcessJsonResponse.Descriptor, + + /* + * Cryptography request extraction: + */ + ExtractGetRequest.Descriptor, + + /* + * Cryptography response processing: + */ + ProcessJsonResponse.Descriptor); + + /// + /// Contains the logic responsible of infering the issuer URL from the HTTP request host. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class InferIssuerFromHost : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + // Note: this handler must be invoked after AttachIssuer and before AttachEndpoints. + .UseSingletonHandler() + .SetOrder(OpenIddictServerHandlers.Discovery.AttachIssuer.Descriptor.Order + 500) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + // If the issuer was not populated by another handler (e.g from the server options), + // try to infer it from the request scheme/host/path base (which requires HTTP/1.1). + if (context.Issuer == null) + { + if (!request.Host.HasValue) + { + throw new InvalidOperationException("No host was attached to the HTTP request."); + } + + if (!Uri.TryCreate(request.Scheme + "://" + request.Host + request.PathBase, UriKind.Absolute, out Uri issuer)) + { + throw new InvalidOperationException("The issuer address cannot be inferred from the current request."); + } + + context.Issuer = issuer; + } + + return Task.CompletedTask; + } + } + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs new file mode 100644 index 000000000..e4bc0e684 --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore; +using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers; + +namespace OpenIddict.Server.AspNetCore +{ + public static partial class OpenIddictServerAspNetCoreHandlers + { + public static class Exchange + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Token request extraction: + */ + ExtractPostRequest.Descriptor, + ExtractBasicAuthenticationCredentials.Descriptor, + + /* + * Token request handling: + */ + EnablePassthroughMode.Descriptor, + + /* + * Token response processing: + */ + ProcessJsonResponse.Descriptor); + + /// + /// Contains the logic responsible of enabling the pass-through mode for the received request. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class EnablePassthroughMode : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + context.SkipRequest(); + + return Task.CompletedTask; + } + } + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Serialization.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Serialization.cs new file mode 100644 index 000000000..d72a1190a --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Serialization.cs @@ -0,0 +1,160 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore; +using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers.Serialization; + +namespace OpenIddict.Server.AspNetCore +{ + public static partial class OpenIddictServerAspNetCoreHandlers + { + public static class Serialization + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Token serialization: + */ + InferTokenSerializationIssuerFromHost.Descriptor, + InferTokenSerializationIssuerFromHost.Descriptor, + InferTokenSerializationIssuerFromHost.Descriptor, + InferTokenSerializationIssuerFromHost.Descriptor, + + /* + * Token deserialization: + */ + InferTokenDeserializationIssuerFromHost.Descriptor, + InferTokenDeserializationIssuerFromHost.Descriptor, + InferTokenDeserializationIssuerFromHost.Descriptor, + InferTokenDeserializationIssuerFromHost.Descriptor); + } + + /// + /// Contains the logic responsible of infering the issuer URL from the HTTP request host for token deserialization. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class InferTokenSerializationIssuerFromHost : IOpenIddictServerHandler + where TContext : BaseSerializingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(AttachIdentityTokenSerializationParameters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + // If the issuer was not populated by another handler (e.g from the server options), + // try to infer it from the request scheme/host/path base (which requires HTTP/1.1). + if (context.Issuer == null) + { + if (!request.Host.HasValue) + { + throw new InvalidOperationException("No host was attached to the HTTP request."); + } + + if (!Uri.TryCreate(request.Scheme + "://" + request.Host + request.PathBase, UriKind.Absolute, out Uri issuer)) + { + throw new InvalidOperationException("The issuer address cannot be inferred from the current request."); + } + + context.Issuer = issuer; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of infering the discovery document issuer URL from the HTTP request host. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class InferTokenDeserializationIssuerFromHost : IOpenIddictServerHandler + where TContext : BaseDeserializingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(AttachIdentityTokenDeserializationParameters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + // If the issuer was not populated by another handler (e.g from the server options), + // try to infer it from the request scheme/host/path base (which requires HTTP/1.1). + if (context.TokenValidationParameters != null && context.TokenValidationParameters.ValidIssuer == null) + { + if (!request.Host.HasValue) + { + throw new InvalidOperationException("No host was attached to the HTTP request."); + } + + if (!Uri.TryCreate(request.Scheme + "://" + request.Host + request.PathBase, UriKind.Absolute, out Uri issuer)) + { + throw new InvalidOperationException("The issuer address cannot be inferred from the current request."); + } + + context.TokenValidationParameters.ValidIssuer = issuer.AbsoluteUri; + } + + return Task.CompletedTask; + } + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs new file mode 100644 index 000000000..026cbfa39 --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs @@ -0,0 +1,684 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using Newtonsoft.Json; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server.AspNetCore +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public static partial class OpenIddictServerAspNetCoreHandlers + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Top-level request processing: + */ + InferEndpointType.Descriptor, + ValidateTransportSecurityRequirement.Descriptor, + ValidateHost.Descriptor) + .AddRange(Authentication.DefaultHandlers) + .AddRange(Discovery.DefaultHandlers) + .AddRange(Exchange.DefaultHandlers) + .AddRange(Serialization.DefaultHandlers); + + /// + /// Contains the logic responsible of inferring the endpoint type from the request address. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class InferEndpointType : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MinValue + 50_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + context.EndpointType = + Matches(context.Options.AuthorizationEndpointUris) ? OpenIddictServerEndpointType.Authorization : + Matches(context.Options.ConfigurationEndpointUris) ? OpenIddictServerEndpointType.Configuration : + Matches(context.Options.CryptographyEndpointUris) ? OpenIddictServerEndpointType.Cryptography : + Matches(context.Options.IntrospectionEndpointUris) ? OpenIddictServerEndpointType.Introspection : + Matches(context.Options.LogoutEndpointUris) ? OpenIddictServerEndpointType.Logout : + Matches(context.Options.RevocationEndpointUris) ? OpenIddictServerEndpointType.Revocation : + Matches(context.Options.TokenEndpointUris) ? OpenIddictServerEndpointType.Token : + Matches(context.Options.UserinfoEndpointUris) ? OpenIddictServerEndpointType.Userinfo : + OpenIddictServerEndpointType.Unknown; + + return Task.CompletedTask; + + bool Matches(IList addresses) + { + for (var index = 0; index < addresses.Count; index++) + { + var address = addresses[index]; + if (address.IsAbsoluteUri) + { + if (!string.Equals(address.Scheme, request.Scheme, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var host = HostString.FromUriComponent(address); + if (host != request.Host) + { + continue; + } + + var path = PathString.FromUriComponent(address); + if (path == request.PathBase + request.Path || + path == request.PathBase + request.Path + new PathString("/")) + { + return true; + } + } + + else if (address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { + var path = new PathString(address.OriginalString); + if (path == request.Path || path == request.Path + new PathString("/")) + { + return true; + } + } + } + + return false; + } + } + } + + /// + /// Contains the logic responsible of rejecting OpenID Connect requests that don't use transport security. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ValidateTransportSecurityRequirement : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(InferEndpointType.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + // Don't require that the host be present if the request is not handled by OpenIddict. + if (context.EndpointType == OpenIddictServerEndpointType.Unknown) + { + return Task.CompletedTask; + } + + // Reject authorization requests sent without transport security. + if (!request.IsHttps) + { + context.Reject( + error: Errors.InvalidRequest, + description: "This server only accepts HTTPS requests."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of ensuring the host can be inferred from the request if none was set in the options. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ValidateHost : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateTransportSecurityRequirement.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + // Don't require that the request host be present if the request is not handled + // by an OpenIddict endpoint or if an explicit issuer URL was set in the options. + if (context.Options.Issuer != null || context.EndpointType == OpenIddictServerEndpointType.Unknown) + { + return Task.CompletedTask; + } + + if (!request.Host.HasValue) + { + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'Host' header is missing."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of extracting OpenID Connect requests from GET HTTP requests. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ExtractGetRequest : IOpenIddictServerHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ValidateHost.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (HttpMethods.IsGet(request.Method)) + { + context.Request = new OpenIddictRequest(request.Query); + } + + else + { + context.Logger.LogError("The request was rejected because an invalid " + + "HTTP method was specified: {Method}.", request.Method); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified HTTP method is not valid."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of extracting OpenID Connect requests from GET or POST HTTP requests. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ExtractGetOrPostRequest : IOpenIddictServerHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractGetRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (HttpMethods.IsGet(request.Method)) + { + context.Request = new OpenIddictRequest(request.Query); + } + + else if (HttpMethods.IsPost(request.Method)) + { + // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization + if (string.IsNullOrEmpty(request.ContentType)) + { + context.Logger.LogError("The request was rejected because the mandatory 'Content-Type' header was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'Content-Type' header must be specified."); + + return; + } + + // May have media/type; charset=utf-8, allow partial match. + if (!request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) + { + context.Logger.LogError("The request was rejected because an invalid 'Content-Type' " + + "header was specified: {ContentType}.", request.ContentType); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'Content-Type' header is not valid."); + + return; + } + + context.Request = new OpenIddictRequest(await request.ReadFormAsync(request.HttpContext.RequestAborted)); + } + + else + { + context.Logger.LogError("The request was rejected because an invalid " + + "HTTP method was specified: {Method}.", request.Method); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified HTTP method is not valid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of extracting OpenID Connect requests from POST HTTP requests. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ExtractPostRequest : IOpenIddictServerHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractGetOrPostRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (HttpMethods.IsPost(request.Method)) + { + // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization + if (string.IsNullOrEmpty(request.ContentType)) + { + context.Logger.LogError("The request was rejected because the mandatory 'Content-Type' header was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'Content-Type' header must be specified."); + + return; + } + + // May have media/type; charset=utf-8, allow partial match. + if (!request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) + { + context.Logger.LogError("The request was rejected because an invalid 'Content-Type' " + + "header was specified: {ContentType}.", request.ContentType); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'Content-Type' header is not valid."); + + return; + } + + context.Request = new OpenIddictRequest(await request.ReadFormAsync(request.HttpContext.RequestAborted)); + } + + else + { + context.Logger.LogError("The request was rejected because an invalid " + + "HTTP method was specified: {Method}.", request.Method); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified HTTP method is not valid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting token requests that specify an invalid grant type. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ExtractBasicAuthenticationCredentials : IOpenIddictServerHandler + where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractPostRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + string header = request.Headers[HeaderNames.Authorization]; + if (string.IsNullOrEmpty(header) || !header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) + { + return Task.CompletedTask; + } + + // At this point, reject requests that use multiple client authentication methods. + // See https://tools.ietf.org/html/rfc6749#section-2.3 for more information. + if (!string.IsNullOrEmpty(context.Request.ClientAssertion) || !string.IsNullOrEmpty(context.Request.ClientSecret)) + { + context.Logger.LogError("The request was rejected because multiple client credentials were specified."); + + context.Reject( + error: Errors.InvalidRequest, + description: "Multiple client credentials cannot be specified."); + + return Task.CompletedTask; + } + + try + { + var value = header.Substring("Basic ".Length).Trim(); + var data = Encoding.ASCII.GetString(Convert.FromBase64String(value)); + + var index = data.IndexOf(':'); + if (index < 0) + { + context.Reject( + error: Errors.InvalidRequest, + description: "The specified client credentials are invalid."); + + return Task.CompletedTask; + } + + // Attach the basic authentication credentials to the request message. + context.Request.ClientId = UnescapeDataString(data.Substring(0, index)); + context.Request.ClientSecret = UnescapeDataString(data.Substring(index + 1)); + + return Task.CompletedTask; + } + + catch + { + context.Reject( + error: Errors.InvalidRequest, + description: "The specified client credentials are invalid."); + + return Task.CompletedTask; + } + + static string UnescapeDataString(string data) + { + if (string.IsNullOrEmpty(data)) + { + return null; + } + + return Uri.UnescapeDataString(data.Replace("+", "%20")); + } + } + } + + /// + /// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessJsonResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Response == null) + { + throw new InvalidOperationException("This handler cannot be invoked without a response attached."); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + using (var buffer = new MemoryStream()) + using (var writer = new JsonTextWriter(new StreamWriter(buffer))) + { + var serializer = JsonSerializer.CreateDefault(); + serializer.Serialize(writer, context.Response); + + writer.Flush(); + + if (!string.IsNullOrEmpty(context.Response.Error)) + { + // When client authentication is made using basic authentication, the authorization server MUST return + // a 401 response with a valid WWW-Authenticate header containing the Basic scheme and a non-empty realm. + // A similar error MAY be returned even when basic authentication is not used and MUST also be returned + // when an invalid token is received by the userinfo endpoint using the Bearer authentication scheme. + // To simplify the logic, a 401 response with the Bearer scheme is returned for invalid_token errors + // and a 401 response with the Basic scheme is returned for invalid_client, even if the credentials + // were specified in the request form instead of the HTTP headers, as allowed by the specification. + var scheme = context.Response.Error switch + { + Errors.InvalidClient => Schemes.Basic, + Errors.InvalidToken => Schemes.Bearer, + _ => null + }; + + if (!string.IsNullOrEmpty(scheme)) + { + var issuer = context.Options.Issuer; + if (issuer == null && !Uri.TryCreate(request.Scheme + "://" + request.Host + request.PathBase, UriKind.Absolute, out issuer)) + { + throw new InvalidOperationException("The issuer address cannot be inferred from the current request."); + } + + request.HttpContext.Response.StatusCode = 401; + + request.HttpContext.Response.Headers[HeaderNames.WWWAuthenticate] = new StringBuilder() + .Append(scheme) + .Append(' ') + .Append(Parameters.Realm) + .Append("=\"") + .Append(issuer.AbsoluteUri) + .Append('"') + .ToString(); + } + + else + { + request.HttpContext.Response.StatusCode = 400; + } + } + + request.HttpContext.Response.ContentLength = buffer.Length; + request.HttpContext.Response.ContentType = "application/json;charset=UTF-8"; + + buffer.Seek(offset: 0, loc: SeekOrigin.Begin); + await buffer.CopyToAsync(request.HttpContext.Response.Body, 4096, request.HttpContext.RequestAborted); + } + + context.HandleRequest(); + } + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHelpers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHelpers.cs new file mode 100644 index 000000000..b97bc9c18 --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHelpers.cs @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Http; +using OpenIddict.Abstractions; +using OpenIddict.Server; +using OpenIddict.Server.AspNetCore; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace Microsoft.AspNetCore +{ + /// + /// Exposes companion extensions for the OpenIddict/ASP.NET Core integration. + /// + public static class OpenIddictServerAspNetCoreHelpers + { + /// + /// Retrieves the instance stored in the properties. + /// + /// The transaction instance. + /// The instance or null if it couldn't be found. + public static HttpRequest GetHttpRequest([NotNull] this OpenIddictServerTransaction transaction) + { + if (transaction == null) + { + throw new ArgumentNullException(nameof(transaction)); + } + + if (!transaction.Properties.TryGetValue(typeof(HttpRequest).FullName, out object property)) + { + return null; + } + + if (property is WeakReference reference && reference.TryGetTarget(out HttpRequest request)) + { + return request; + } + + return null; + } + + /// + /// Retrieves the instance stored in . + /// + /// The context instance. + /// The . + public static OpenIddictServerEndpointType GetOpenIddictServerEndpointType([NotNull] this HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return context.Features.Get()?.Transaction?.EndpointType ?? default; + } + + /// + /// Retrieves the instance stored in . + /// + /// The context instance. + /// The instance or null if it couldn't be found. + public static OpenIddictRequest GetOpenIddictServerRequest([NotNull] this HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return context.Features.Get()?.Transaction?.Request; + } + + /// + /// Retrieves the instance stored in . + /// + /// The context instance. + /// The instance or null if it couldn't be found. + public static OpenIddictResponse GetOpenIddictServerResponse([NotNull] this HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return context.Features.Get()?.Transaction?.Response; + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs new file mode 100644 index 000000000..a3c5fd351 --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Distributed; + +namespace OpenIddict.Server.AspNetCore +{ + /// + /// Provides various settings needed to configure the OpenIddict ASP.NET Core server integration. + /// + public class OpenIddictServerAspNetCoreOptions : AuthenticationSchemeOptions + { + /// + /// Gets or sets a boolean indicating whether incoming requests arriving on insecure endpoints should be rejected. + /// By default, this property is set to false to help mitigate man-in-the-middle attacks. + /// + public bool DisableTransportSecurityRequirement { get; set; } + + /// + /// Gets or sets a boolean indicating whether the pass-through mode is enabled for the authorization endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + public bool EnableAuthorizationEndpointPassthrough { get; set; } + + /// + /// Gets or sets a boolean indicating whether OpenIddict should allow the rest of the request processing pipeline + /// to be invoked when returning an error from the interactive authorization and logout endpoints. + /// When this option is enabled, special logic must be added to these actions to handle errors, that can be + /// retrieved using + /// + public bool EnableErrorPassthrough { get; set; } + + /// + /// Gets or sets a boolean indicating whether the pass-through mode is enabled for the token endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + public bool EnableTokenEndpointPassthrough { get; set; } + + /// + /// Gets or sets a boolean indicating whether the pass-through mode is enabled for the userinfo endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + public bool EnableUserinfoEndpointPassthrough { get; set; } + + /// + /// Gets or sets a boolean indicating whether request caching should be enabled. + /// When enabled, both authorization and logout requests are automatically stored + /// in the distributed cache, which allows flowing large payloads across requests. + /// Enabling this option is recommended when using external authentication providers + /// or when large GET or POST OpenID Connect authorization requests support is required. + /// + public bool EnableRequestCaching { get; set; } + + /// + /// Gets or sets a boolean indicating whether integration with the status code pages + /// middleware should be enabled or not. Once enabled, errors generated by the OpenIddict + /// interactive authorization and logout endpoints can be handled by ASP.NET Core. + /// + public bool EnableStatusCodePagesIntegration { get; set; } + + /// + /// Gets or sets the caching policy used to determine how long the authorization + /// and end session requests should be cached by the distributed cache implementation. + /// + public DistributedCacheEntryOptions RequestCachingPolicy { get; set; } = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1), + SlidingExpiration = TimeSpan.FromMinutes(30) + }; + } +} diff --git a/src/OpenIddict.Server.DataProtection/OpenIddict.Server.DataProtection.csproj b/src/OpenIddict.Server.DataProtection/OpenIddict.Server.DataProtection.csproj new file mode 100644 index 000000000..fa2de4efa --- /dev/null +++ b/src/OpenIddict.Server.DataProtection/OpenIddict.Server.DataProtection.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.0;netstandard2.1 + + + + ASP.NET Core Data Protection integration package for the OpenIddict server services. + $(PackageTags);server;dataprotection + + + + + + + + + + + + diff --git a/src/OpenIddict.Mvc/OpenIddictMvcBuilder.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs similarity index 54% rename from src/OpenIddict.Mvc/OpenIddictMvcBuilder.cs rename to src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs index c94a6b06c..3948e2779 100644 --- a/src/OpenIddict.Mvc/OpenIddictMvcBuilder.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs @@ -6,30 +6,24 @@ using System; using System.ComponentModel; -using AspNet.Security.OpenIdConnect.Primitives; using JetBrains.Annotations; -using OpenIddict.Mvc; +using Microsoft.AspNetCore.DataProtection; +using OpenIddict.Server.DataProtection; namespace Microsoft.Extensions.DependencyInjection { /// - /// Exposes the necessary methods required to configure the OpenIddict MVC integration. + /// Exposes the necessary methods required to configure the + /// OpenIddict ASP.NET Core Data Protection integration. /// - public class OpenIddictMvcBuilder + public class OpenIddictServerDataProtectionBuilder { /// - /// Initializes a new instance of . + /// Initializes a new instance of . /// /// The services collection. - public OpenIddictMvcBuilder([NotNull] IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - Services = services; - } + public OpenIddictServerDataProtectionBuilder([NotNull] IServiceCollection services) + => Services = services ?? throw new ArgumentNullException(nameof(services)); /// /// Gets the services collection. @@ -38,12 +32,12 @@ public OpenIddictMvcBuilder([NotNull] IServiceCollection services) public IServiceCollection Services { get; } /// - /// Amends the default OpenIddict MVC configuration. + /// Amends the default OpenIddict server ASP.NET Core Data Protection configuration. /// /// The delegate used to configure the OpenIddict options. /// This extension can be safely called multiple times. - /// The . - public OpenIddictMvcBuilder Configure([NotNull] Action configuration) + /// The . + public OpenIddictServerDataProtectionBuilder Configure([NotNull] Action configuration) { if (configuration == null) { @@ -56,13 +50,28 @@ public OpenIddictMvcBuilder Configure([NotNull] Action con } /// - /// Configures the OpenIddict MVC binder to avoid throwing an exception - /// when it is unable to bind - /// parameters (e.g because the endpoint is not an OpenID Connect endpoint). + /// Configures OpenIddict to use a specific data protection provider + /// instead of relying on the default instance provided by the DI container. + /// + /// The data protection provider used to create token protectors. + /// The . + public OpenIddictServerDataProtectionBuilder UseDataProtectionProvider([NotNull] IDataProtectionProvider provider) + { + if (provider == null) + { + throw new ArgumentNullException(nameof(provider)); + } + + return Configure(options => options.DataProtectionProvider = provider); + } + + /// + /// Configures OpenIddict to use the Data Protection format when + /// issuing new access tokens, refresh tokens and authorization codes. /// - /// The . - public OpenIddictMvcBuilder DisableBindingExceptions() - => Configure(options => options.DisableBindingExceptions = true); + /// The . + public OpenIddictServerDataProtectionBuilder PreferDataProtectionFormat() + => Configure(options => options.PreferDataProtectionFormat = true); /// /// Determines whether the specified object is equal to the current object. diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConfiguration.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConfiguration.cs new file mode 100644 index 000000000..829c3c10d --- /dev/null +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConfiguration.cs @@ -0,0 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using JetBrains.Annotations; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Options; + +namespace OpenIddict.Server.DataProtection +{ + /// + /// Contains the methods required to ensure that the OpenIddict ASP.NET Core Data Protection configuration is valid. + /// + public class OpenIddictServerDataProtectionConfiguration : IConfigureOptions, + IPostConfigureOptions + { + private readonly IDataProtectionProvider _dataProtectionProvider; + + /// + /// Creates a new instance of the class. + /// + /// The ASP.NET Core Data Protection provider. + public OpenIddictServerDataProtectionConfiguration([NotNull] IDataProtectionProvider dataProtectionProvider) + => _dataProtectionProvider = dataProtectionProvider; + + public void Configure([NotNull] OpenIddictServerOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + // Register the built-in event handlers used by the OpenIddict Data Protection server components. + foreach (var handler in OpenIddictServerDataProtectionHandlers.DefaultHandlers) + { + options.DefaultHandlers.Add(handler); + } + } + + /// + /// Populates the default OpenIddict ASP.NET Core Data Protection server options + /// and ensures that the configuration is in a consistent and valid state. + /// + /// The authentication scheme associated with the handler instance. + /// The options instance to initialize. + public void PostConfigure([CanBeNull] string name, [NotNull] OpenIddictServerDataProtectionOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.DataProtectionProvider ??= _dataProtectionProvider; + } + } +} diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs new file mode 100644 index 000000000..79c7bb8a3 --- /dev/null +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs @@ -0,0 +1,82 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using OpenIddict.Server; +using OpenIddict.Server.DataProtection; +using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters; +using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlers; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Exposes extensions allowing to register the OpenIddict ASP.NET Core Data Protection server services. + /// + public static class OpenIddictServerDataProtectionExtensions + { + /// + /// Registers the OpenIddict ASP.NET Core Data Protection server services in the DI container. + /// + /// The services builder used by OpenIddict to register new services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictServerDataProtectionBuilder UseDataProtection([NotNull] this OpenIddictServerBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Services.AddDataProtection(); + + // Register the built-in server event handlers used by the OpenIddict Data Protection components. + // Note: the order used here is not important, as the actual order is set in the options. + builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); + + // Register the built-in filter used by the default OpenIddict Data Protection event handlers. + builder.Services.TryAddSingleton(); + + // Note: TryAddEnumerable() is used here to ensure the initializers are registered only once. + builder.Services.TryAddEnumerable(new[] + { + ServiceDescriptor.Singleton, OpenIddictServerDataProtectionConfiguration>(), + ServiceDescriptor.Singleton, OpenIddictServerDataProtectionConfiguration>() + }); + + return new OpenIddictServerDataProtectionBuilder(builder.Services); + } + + /// + /// Registers the OpenIddict ASP.NET Core Data Protection server services in the DI container. + /// + /// The services builder used by OpenIddict to register new services. + /// The configuration delegate used to configure the server services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictServerBuilder UseDataProtection( + [NotNull] this OpenIddictServerBuilder builder, + [NotNull] Action configuration) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + configuration(builder.UseDataProtection()); + + return builder; + } + } +} diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlerFilters.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlerFilters.cs new file mode 100644 index 000000000..688e29b0e --- /dev/null +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlerFilters.cs @@ -0,0 +1,43 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Options; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server.DataProtection +{ + /// + /// Contains a collection of event handler filters commonly used by the Data Protection handlers. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static class OpenIddictServerDataProtectionHandlerFilters + { + /// + /// Represents a filter that excludes the associated handlers if OpenIddict was not configured to issue Data Protection tokens. + /// + public class RequirePreferDataProtectionFormatEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequirePreferDataProtectionFormatEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.PreferDataProtectionFormat); + } + } + } +} diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Serialization.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Serialization.cs new file mode 100644 index 000000000..11365ccca --- /dev/null +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Serialization.cs @@ -0,0 +1,812 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers.Serialization; + +namespace OpenIddict.Server.DataProtection +{ + public static partial class OpenIddictServerDataProtectionHandlers + { + public static class Serialization + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Access token serialization: + */ + AttachAccessTokenSerializationProtector.Descriptor, + SerializeDataProtectionToken.Descriptor, + + /* + * Authorization code serialization: + */ + AttachAuthorizationCodeSerializationProtector.Descriptor, + SerializeDataProtectionToken.Descriptor, + + /* + * Refresh token serialization: + */ + AttachRefreshTokenSerializationProtector.Descriptor, + SerializeDataProtectionToken.Descriptor, + + /* + * Access token deserialization: + */ + AttachAccessTokenDeserializationProtector.Descriptor, + DeserializeDataProtectionToken.Descriptor, + + /* + * Authorization code deserialization: + */ + AttachAuthorizationCodeDeserializationProtector.Descriptor, + DeserializeDataProtectionToken.Descriptor, + + /* + * Refresh token deserialization: + */ + AttachRefreshTokenDeserializationProtector.Descriptor, + DeserializeDataProtectionToken.Descriptor); + + /// + /// Contains the logic responsible of generating a Data Protection token. + /// + public class SerializeDataProtectionToken : IOpenIddictServerHandler where TContext : BaseSerializingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(SerializeJwtBearerToken.Descriptor.Order - 5000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.Properties.TryGetValue(typeof(IDataProtector).FullName, out var property) || + !(property is IDataProtector protector)) + { + throw new InvalidOperationException(new StringBuilder() + .Append("No suitable data protector was found for the specified token type.") + .Append("This may indicate that the OpenIddict Data Protection services were not correctly registered.") + .ToString()); + } + + var properties = new Dictionary(); + + // Unlike ASP.NET Core Data Protection-based tokens, tokens serialized using the new format + // can't include authentication properties. To ensure tokens can be used with previous versions + // of OpenIddict are issued, well-known claims are manually mapped to their properties equivalents. + + SetProperty(properties, Properties.AccessTokenLifetime, + context.Principal.GetClaim(Claims.Private.AccessTokenLifetime)); + SetProperty(properties, Properties.AuthorizationCodeLifetime, + context.Principal.GetClaim(Claims.Private.AuthorizationCodeLifetime)); + SetProperty(properties, Properties.CodeChallenge, + context.Principal.GetClaim(Claims.Private.CodeChallenge)); + SetProperty(properties, Properties.CodeChallengeMethod, + context.Principal.GetClaim(Claims.Private.CodeChallengeMethod)); + SetProperty(properties, Properties.Expires, + context.Principal.GetExpirationDate()?.ToString("r", CultureInfo.InvariantCulture)); + SetProperty(properties, Properties.IdentityTokenLifetime, + context.Principal.GetClaim(Claims.Private.IdentityTokenLifetime)); + SetProperty(properties, Properties.Issued, + context.Principal.GetCreationDate()?.ToString("r", CultureInfo.InvariantCulture)); + SetProperty(properties, Properties.OriginalRedirectUri, + context.Principal.GetClaim(Claims.Private.OriginalRedirectUri)); + SetProperty(properties, Properties.RefreshTokenLifetime, + context.Principal.GetClaim(Claims.Private.RefreshTokenLifetime)); + + SetArrayProperty(properties, Properties.Audiences, context.Principal.GetAudiences()); + SetArrayProperty(properties, Properties.Presenters, context.Principal.GetPresenters()); + SetArrayProperty(properties, Properties.Scopes, context.Principal.GetScopes()); + + using var buffer = new MemoryStream(); + using var writer = new BinaryWriter(buffer); + + Write(writer, version: 5, context.Principal.Identity.AuthenticationType, context.Principal, properties); + writer.Flush(); + + context.Token = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())); + context.HandleSerialization(); + + return Task.CompletedTask; + + // Note: the following local methods closely matches the logic used by ASP.NET Core's + // authentication stack and MUST NOT be modified to ensure tokens encrypted using + // the OpenID Connect server middleware can be read by OpenIddict (and vice-versa). + + static void Write(BinaryWriter writer, int version, string scheme, + ClaimsPrincipal principal, IReadOnlyDictionary properties) + { + writer.Write(version); + writer.Write(scheme); + + // Write the number of identities contained in the principal. + writer.Write(principal.Identities.Count()); + + foreach (var identity in principal.Identities) + { + WriteIdentity(writer, identity); + } + + WriteProperties(writer, version, properties); + } + + static void WriteIdentity(BinaryWriter writer, ClaimsIdentity identity) + { + writer.Write(identity.AuthenticationType ?? string.Empty); + 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); + } + } + + static 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); + } + } + + static void WriteProperties(BinaryWriter writer, int version, IReadOnlyDictionary properties) + { + writer.Write(version); + writer.Write(properties.Count); + + foreach (var property in properties) + { + writer.Write(property.Key ?? string.Empty); + writer.Write(property.Value ?? string.Empty); + } + } + + static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue) + => writer.Write(string.Equals(value, defaultValue, StringComparison.Ordinal) ? "\0" : value); + + static void SetProperty(IDictionary properties, string name, string value) + { + if (string.IsNullOrEmpty(value)) + { + properties.Remove(name); + } + + else + { + properties[name] = value; + } + } + + static void SetArrayProperty(IDictionary properties, string name, IEnumerable values) + { + var array = new JArray(values); + if (array.Count == 0) + { + properties.Remove(name); + } + + else + { + properties[name] = array.ToString(Formatting.None); + } + } + } + } + + /// + /// Contains the logic responsible of unprotecting a Data Protection token. + /// + public class DeserializeDataProtectionToken : IOpenIddictServerHandler where TContext : BaseDeserializingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler>() + .SetOrder(DeserializeJwtBearerToken.Descriptor.Order - 5000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.Properties.TryGetValue(typeof(IDataProtector).FullName, out var property) || + !(property is IDataProtector protector)) + { + throw new InvalidOperationException(new StringBuilder() + .Append("No suitable data protector was found for the specified token type.") + .Append("This may indicate that the OpenIddict Data Protection services were not correctly registered.") + .ToString()); + } + + try + { + using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(context.Token))); + using var reader = new BinaryReader(buffer); + + var (principal, properties) = Read(reader, version: 5); + if (principal == null) + { + return Task.CompletedTask; + } + + context.Principal = principal; + + // Tokens serialized using the ASP.NET Core Data Protection stack are compound + // of both claims and special authentication properties. To ensure existing tokens + // can be reused, well-known properties are manually mapped to their claims equivalents. + + context.Principal + .SetAudiences(GetArrayProperty(properties, Properties.Audiences)) + .SetCreationDate(GetDateProperty(properties, Properties.Issued)) + .SetExpirationDate(GetDateProperty(properties, Properties.Expires)) + .SetPresenters(GetArrayProperty(properties, Properties.Presenters)) + .SetScopes(GetArrayProperty(properties, Properties.Scopes)) + + .SetClaim(Claims.Private.AccessTokenLifetime, GetProperty(properties, Properties.AccessTokenLifetime)) + .SetClaim(Claims.Private.AuthorizationCodeLifetime, GetProperty(properties, Properties.AuthorizationCodeLifetime)) + .SetClaim(Claims.Private.CodeChallenge, GetProperty(properties, Properties.CodeChallenge)) + .SetClaim(Claims.Private.CodeChallengeMethod, GetProperty(properties, Properties.CodeChallengeMethod)) + .SetClaim(Claims.Private.IdentityTokenLifetime, GetProperty(properties, Properties.IdentityTokenLifetime)) + .SetClaim(Claims.Private.OriginalRedirectUri, GetProperty(properties, Properties.OriginalRedirectUri)) + .SetClaim(Claims.Private.RefreshTokenLifetime, GetProperty(properties, Properties.RefreshTokenLifetime)); + + context.HandleDeserialization(); + + return Task.CompletedTask; + } + + catch (Exception exception) + { + context.Logger.LogTrace(exception, "An exception occured while deserializing a token."); + + return Task.CompletedTask; + } + + static (ClaimsPrincipal principal, ImmutableDictionary properties) Read(BinaryReader reader, int version) + { + if (version != reader.ReadInt32()) + { + return (null, ImmutableDictionary.Create()); + } + + // Read the authentication scheme associated to the ticket. + _ = reader.ReadString(); + + // Read the number of identities stored in the serialized payload. + var count = reader.ReadInt32(); + if (count < 0) + { + return (null, ImmutableDictionary.Create()); + } + + var identities = new ClaimsIdentity[count]; + for (var index = 0; index != count; ++index) + { + identities[index] = ReadIdentity(reader); + } + + var properties = ReadProperties(reader, version); + + return (new ClaimsPrincipal(identities), properties); + } + + static ClaimsIdentity ReadIdentity(BinaryReader reader) + { + var identity = new ClaimsIdentity( + authenticationType: reader.ReadString(), + nameType: ReadWithDefault(reader, ClaimsIdentity.DefaultNameClaimType), + roleType: ReadWithDefault(reader, ClaimsIdentity.DefaultRoleClaimType)); + + // Read the number of claims contained in the serialized identity. + var count = reader.ReadInt32(); + + 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; + } + + static 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; + } + + static ImmutableDictionary ReadProperties(BinaryReader reader, int version) + { + if (version != reader.ReadInt32()) + { + return ImmutableDictionary.Create(); + } + + var properties = ImmutableDictionary.CreateBuilder(StringComparer.Ordinal); + var count = reader.ReadInt32(); + for (var index = 0; index != count; ++index) + { + properties.Add(reader.ReadString(), reader.ReadString()); + } + + return properties.ToImmutable(); + } + + static string ReadWithDefault(BinaryReader reader, string defaultValue) + { + var value = reader.ReadString(); + + if (string.Equals(value, "\0", StringComparison.Ordinal)) + { + return defaultValue; + } + + return value; + } + + static string GetProperty(IReadOnlyDictionary properties, string name) + => properties.TryGetValue(name, out var value) ? value : null; + + static IEnumerable GetArrayProperty(IReadOnlyDictionary properties, string name) + => properties.TryGetValue(name, out var value) ? JArray.Parse(value).Values() : Enumerable.Empty(); + + static DateTimeOffset? GetDateProperty(IReadOnlyDictionary properties, string name) + => properties.TryGetValue(name, out var value) ? (DateTimeOffset?) + DateTimeOffset.ParseExact(value, "r", CultureInfo.InvariantCulture) : null; + } + } + + /// + /// Contains the logic responsible of populating the data protector needed to generate an access token. + /// + public class AttachAccessTokenSerializationProtector : IOpenIddictServerHandler + { + private readonly IOptionsMonitor _options; + + public AttachAccessTokenSerializationProtector([NotNull] IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] SerializeAccessTokenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the protector MUST be created with the same purposes used by the + // OpenID Connect server middleware (aka ASOS) to guarantee compatibility. + var purposes = new List(capacity: 4) + { + "OpenIdConnectServerHandler", + "AccessTokenFormat", + "ASOS" + }; + + if (context.Options.UseReferenceTokens) + { + purposes.Insert(index: 2, "UseReferenceTokens"); + } + + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); + context.Properties[typeof(IDataProtector).FullName] = protector; + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of populating the data protector needed to generate an authorization code. + /// + public class AttachAuthorizationCodeSerializationProtector : IOpenIddictServerHandler + { + private readonly IOptionsMonitor _options; + + public AttachAuthorizationCodeSerializationProtector([NotNull] IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] SerializeAuthorizationCodeContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the protector MUST be created with the same purposes used by the + // OpenID Connect server middleware (aka ASOS) to guarantee compatibility. + var purposes = new List(capacity: 4) + { + "OpenIdConnectServerHandler", + "AuthorizationCodeFormat", + "ASOS" + }; + + if (context.Options.UseReferenceTokens) + { + purposes.Insert(index: 2, "UseReferenceTokens"); + } + + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); + context.Properties[typeof(IDataProtector).FullName] = protector; + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of populating the data protector needed to generate a refresh token. + /// + public class AttachRefreshTokenSerializationProtector : IOpenIddictServerHandler + { + private readonly IOptionsMonitor _options; + + public AttachRefreshTokenSerializationProtector([NotNull] IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] SerializeRefreshTokenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the protector MUST be created with the same purposes used by the + // OpenID Connect server middleware (aka ASOS) to guarantee compatibility. + var purposes = new List(capacity: 4) + { + "OpenIdConnectServerHandler", + "RefreshTokenFormat", + "ASOS" + }; + + if (context.Options.UseReferenceTokens) + { + purposes.Insert(index: 2, "UseReferenceTokens"); + } + + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); + context.Properties[typeof(IDataProtector).FullName] = protector; + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of populating the data protector needed to unprotect an access token. + /// + public class AttachAccessTokenDeserializationProtector : IOpenIddictServerHandler + { + private readonly IOptionsMonitor _options; + + public AttachAccessTokenDeserializationProtector([NotNull] IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] DeserializeAccessTokenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the protector MUST be created with the same purposes used by the + // OpenID Connect server middleware (aka ASOS) to guarantee compatibility. + var purposes = new List(capacity: 4) + { + "OpenIdConnectServerHandler", + "AccessTokenFormat", + "ASOS" + }; + + if (context.Options.UseReferenceTokens) + { + purposes.Insert(index: 2, "UseReferenceTokens"); + } + + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); + context.Properties[typeof(IDataProtector).FullName] = protector; + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of populating the data protector needed to unprotect an authorization code. + /// + public class AttachAuthorizationCodeDeserializationProtector : IOpenIddictServerHandler + { + private readonly IOptionsMonitor _options; + + public AttachAuthorizationCodeDeserializationProtector([NotNull] IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] DeserializeAuthorizationCodeContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the protector MUST be created with the same purposes used by the + // OpenID Connect server middleware (aka ASOS) to guarantee compatibility. + var purposes = new List(capacity: 4) + { + "OpenIdConnectServerHandler", + "AuthorizationCodeFormat", + "ASOS" + }; + + if (context.Options.UseReferenceTokens) + { + purposes.Insert(index: 2, "UseReferenceTokens"); + } + + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); + context.Properties[typeof(IDataProtector).FullName] = protector; + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of populating the data protector needed to unprotect a refresh token. + /// + public class AttachRefreshTokenDeserializationProtector : IOpenIddictServerHandler + { + private readonly IOptionsMonitor _options; + + public AttachRefreshTokenDeserializationProtector([NotNull] IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] DeserializeRefreshTokenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the protector MUST be created with the same purposes used by the + // OpenID Connect server middleware (aka ASOS) to guarantee compatibility. + var purposes = new List(capacity: 4) + { + "OpenIdConnectServerHandler", + "RefreshTokenFormat", + "ASOS" + }; + + if (context.Options.UseReferenceTokens) + { + purposes.Insert(index: 2, "UseReferenceTokens"); + } + + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); + context.Properties[typeof(IDataProtector).FullName] = protector; + + return Task.CompletedTask; + } + } + } + } +} diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs new file mode 100644 index 000000000..6ffa71f47 --- /dev/null +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs @@ -0,0 +1,15 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; + +namespace OpenIddict.Server.DataProtection +{ + public static partial class OpenIddictServerDataProtectionHandlers + { + public static ImmutableArray DefaultHandlers { get; } = Serialization.DefaultHandlers; + } +} diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs new file mode 100644 index 000000000..9bdf16f21 --- /dev/null +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using Microsoft.AspNetCore.DataProtection; + +namespace OpenIddict.Server.DataProtection +{ + /// + /// Provides various settings needed to configure the OpenIddict server handler. + /// + public class OpenIddictServerDataProtectionOptions + { + /// + /// Gets or sets the data protection provider used to create the default + /// data protectors used by the OpenIddict Data Protection server services. + /// When this property is set to null, the data protection provider + /// is directly retrieved from the dependency injection container. + /// + public IDataProtectionProvider DataProtectionProvider { get; set; } + + /// + /// Gets or sets a boolean indicating whether the Data Protection format + /// should be preferred when issuing new access tokens, refresh tokens + /// and authorization codes. This property is set to false by default. + /// + public bool PreferDataProtectionFormat { get; set; } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddict.Server.Owin.csproj b/src/OpenIddict.Server.Owin/OpenIddict.Server.Owin.csproj new file mode 100644 index 000000000..cc31469d8 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddict.Server.Owin.csproj @@ -0,0 +1,24 @@ + + + + net461;net472 + + + + OWIN/ASP.NET 4.x integration package for the OpenIddict server services. + $(PackageTags);server;aspnet;katana;owin + + + + + + + + + + + + + + + diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs new file mode 100644 index 000000000..b55c05fcc --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs @@ -0,0 +1,152 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using JetBrains.Annotations; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Owin; +using OpenIddict.Server.Owin; +using Owin; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Exposes the necessary methods required to configure + /// the OpenIddict server OWIN/Katana integration. + /// + public class OpenIddictServerOwinBuilder + { + /// + /// Initializes a new instance of . + /// + /// The services collection. + public OpenIddictServerOwinBuilder([NotNull] IServiceCollection services) + => Services = services ?? throw new ArgumentNullException(nameof(services)); + + /// + /// Gets the services collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IServiceCollection Services { get; } + + /// + /// Amends the default OpenIddict server OWIN/Katana configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public OpenIddictServerOwinBuilder Configure([NotNull] Action configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(configuration); + + return this; + } + + /// + /// Disables the transport security requirement (HTTPS) during development. + /// + /// The . + public OpenIddictServerOwinBuilder DisableTransportSecurityRequirement() + => Configure(options => options.DisableTransportSecurityRequirement = true); + + /// + /// Enables the pass-through mode for the OpenID Connect authorization endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + /// The . + public OpenIddictServerOwinBuilder EnableAuthorizationEndpointPassthrough() + => Configure(options => options.EnableAuthorizationEndpointPassthrough = true); + + /// + /// Enables error pass-through support, so that the rest of the request processing pipeline is + /// automatically invoked when returning an error from the interactive authorization and logout endpoints. + /// When this option is enabled, special logic must be added to these actions to handle errors, that can be + /// retrieved using + /// + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictServerOwinBuilder EnableErrorPassthrough() + => Configure(options => options.EnableErrorPassthrough = true); + + /// + /// Enables the pass-through mode for the OpenID Connect token endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + /// The . + public OpenIddictServerOwinBuilder EnableTokenEndpointPassthrough() + => Configure(options => options.EnableTokenEndpointPassthrough = true); + + /// + /// Enables the pass-through mode for the OpenID Connect userinfo endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + /// The . + public OpenIddictServerOwinBuilder EnableUserinfoEndpointPassthrough() + => Configure(options => options.EnableUserinfoEndpointPassthrough = true); + + /// + /// Enables request caching, so that both authorization and logout requests + /// are automatically stored in the distributed cache, which allows flowing + /// large payloads across requests. Enabling this option is recommended + /// when using external authentication providers or when large GET or POST + /// OpenID Connect authorization requests support is required. + /// + /// The . + public OpenIddictServerOwinBuilder EnableRequestCaching() + => Configure(options => options.EnableRequestCaching = true); + + /// + /// Sets the caching policy used to determine how long the authorization and + /// end session requests should be cached by the distributed cache implementation. + /// Note: the specified policy is only used when request caching is explicitly enabled. + /// + /// The request caching policy. + /// The . + public OpenIddictServerOwinBuilder SetRequestCachingPolicy([NotNull] DistributedCacheEntryOptions policy) + { + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + return Configure(options => options.RequestCachingPolicy = policy); + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals([CanBeNull] object obj) => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => base.GetHashCode(); + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string ToString() => base.ToString(); + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs new file mode 100644 index 000000000..559e57271 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs @@ -0,0 +1,55 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Diagnostics; +using System.Text; +using JetBrains.Annotations; +using Microsoft.Extensions.Options; +using Microsoft.Owin.Security; + +namespace OpenIddict.Server.Owin +{ + /// + /// Contains the methods required to ensure that the OpenIddict server configuration is valid. + /// + public class OpenIddictServerOwinConfiguration : IConfigureNamedOptions, + IPostConfigureOptions + { + public void Configure([NotNull] OpenIddictServerOptions options) + => Debug.Fail("This infrastructure method shouldn't be called"); + + public void Configure([CanBeNull] string name, [NotNull] OpenIddictServerOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + // Register the built-in event handlers used by the OpenIddict OWIN server components. + foreach (var handler in OpenIddictServerOwinHandlers.DefaultHandlers) + { + options.DefaultHandlers.Add(handler); + } + } + + public void PostConfigure([CanBeNull] string name, [NotNull] OpenIddictServerOwinOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (options.AuthenticationMode == AuthenticationMode.Active) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The OpenIddict OWIN server handler cannot be used as an active authentication handler.") + .Append("Make sure that 'OpenIddictServerOwinOptions.AuthenticationMode' is not set to 'Active'.") + .ToString()); + } + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs new file mode 100644 index 000000000..e1b18c0a5 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +namespace OpenIddict.Server.Owin +{ + /// + /// Exposes common constants used by the OpenIddict OWIN host. + /// + public static class OpenIddictServerOwinConstants + { + public static class Cache + { + public const string AuthorizationRequest = "openiddict-authorization-request:"; + public const string LogoutRequest = "openiddict-logout-request:"; + } + + public static class Properties + { + public const string Error = ".error"; + public const string ErrorDescription = ".error_description"; + public const string ErrorUri = ".error_uri"; + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinDefaults.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinDefaults.cs new file mode 100644 index 000000000..08308c18d --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinDefaults.cs @@ -0,0 +1,21 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using Microsoft.Owin.Security; + +namespace OpenIddict.Server.Owin +{ + /// + /// Exposes the default values used by the OpenIddict server handler. + /// + public static class OpenIddictServerOwinDefaults + { + /// + /// Default value for . + /// + public const string AuthenticationType = "OpenIddict.Server.Owin"; + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs new file mode 100644 index 000000000..f6b5526e3 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using OpenIddict.Server; +using OpenIddict.Server.Owin; +using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlerFilters; +using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlers; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Exposes extensions allowing to register the OpenIddict server services. + /// + public static class OpenIddictServerOwinExtensions + { + /// + /// Registers the OpenIddict server services for OWIN in the DI container. + /// + /// The services builder used by OpenIddict to register new services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictServerOwinBuilder UseOwin([NotNull] this OpenIddictServerBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Services.AddWebEncoders(); + + // Note: unlike regular OWIN middleware, the OpenIddict server middleware is registered + // as a scoped service in the DI container. This allows containers that support middleware + // resolution (like Autofac) to use it without requiring additional configuration. + builder.Services.TryAddScoped(); + + // Register the built-in event handlers used by the OpenIddict OWIN server components. + // Note: the order used here is not important, as the actual order is set in the options. + builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); + + // Register the built-in filters used by the default OpenIddict OWIN server event handlers. + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + + // Register the option initializers used by the OpenIddict OWIN server integration services. + // Note: TryAddEnumerable() is used here to ensure the initializers are only registered once. + builder.Services.TryAddEnumerable(new[] + { + ServiceDescriptor.Singleton, OpenIddictServerOwinConfiguration>(), + ServiceDescriptor.Singleton, OpenIddictServerOwinConfiguration>() + }); + + return new OpenIddictServerOwinBuilder(builder.Services); + } + + /// + /// Registers the OpenIddict server services for OWIN in the DI container. + /// + /// The services builder used by OpenIddict to register new services. + /// The configuration delegate used to configure the server services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictServerBuilder UseOwin( + [NotNull] this OpenIddictServerBuilder builder, + [NotNull] Action configuration) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + configuration(builder.UseOwin()); + + return builder; + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs new file mode 100644 index 000000000..3cafea2c2 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs @@ -0,0 +1,398 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Infrastructure; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using Properties = OpenIddict.Server.Owin.OpenIddictServerOwinConstants.Properties; + +namespace OpenIddict.Server.Owin +{ + /// + /// Provides the entry point necessary to register the OpenIddict server in an OWIN pipeline. + /// + public class OpenIddictServerOwinHandler : AuthenticationHandler + { + private readonly ILogger _logger; + private readonly IOpenIddictServerProvider _provider; + + /// + /// Creates a new instance of the class. + /// + /// The logger used by this instance. + /// The OpenIddict server OWIN provider used by this instance. + public OpenIddictServerOwinHandler( + [NotNull] ILogger logger, + [NotNull] IOpenIddictServerProvider provider) + { + _logger = logger; + _provider = provider; + } + + public override async Task InvokeAsync() + { + // Note: the transaction may be already attached when replaying an OWIN request + // (e.g when using a status code pages middleware re-invoking the OWIN pipeline). + var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName); + if (transaction == null) + { + // Create a new transaction and attach the OWIN request to make it available to the OWIN handlers. + transaction = await _provider.CreateTransactionAsync(); + transaction.Properties[typeof(IOwinRequest).FullName] = new WeakReference(Request); + + // Attach the OpenIddict server transaction to the OWIN shared dictionary + // so that it can retrieved while performing sign-in/sign-out operations. + Context.Set(typeof(OpenIddictServerTransaction).FullName, transaction); + } + + var context = new ProcessRequestContext(transaction); + await _provider.DispatchAsync(context); + + if (context.IsRequestHandled) + { + return true; + } + + else if (context.IsRequestSkipped) + { + return false; + } + + else if (context.IsRejected) + { + var notification = new ProcessErrorResponseContext(transaction) + { + Response = new OpenIddictResponse + { + Error = context.Error ?? Errors.InvalidRequest, + ErrorDescription = context.ErrorDescription, + ErrorUri = context.ErrorUri + } + }; + + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + return true; + } + + else if (notification.IsRequestSkipped) + { + return false; + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The OpenID Connect response was not correctly processed. This may indicate ") + .Append("that the event handler responsible of processing OpenID Connect responses ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + return false; + } + + protected override async Task AuthenticateCoreAsync() + { + var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName); + if (transaction?.Request == null) + { + throw new InvalidOperationException("An identity cannot be extracted from this request."); + } + + switch (transaction.EndpointType) + { + case OpenIddictServerEndpointType.Authorization: + case OpenIddictServerEndpointType.Logout: + { + if (string.IsNullOrEmpty(transaction.Request.IdTokenHint)) + { + return null; + } + + var notification = new DeserializeIdentityTokenContext(transaction) + { + Token = transaction.Request.IdTokenHint + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The identity token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating identity tokens ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + if (notification.Principal == null) + { + _logger.LogWarning("The identity token extracted from the 'id_token_hint' " + + "parameter was invalid or malformed and was ignored."); + + return null; + } + + // Tickets are returned even if they are considered invalid (e.g expired). + + return new AuthenticationTicket((ClaimsIdentity) notification.Principal.Identity, new AuthenticationProperties()); + } + + case OpenIddictServerEndpointType.Token when transaction.Request.IsAuthorizationCodeGrantType(): + { + // Note: this method can be called from the ApplyTokenResponse event, + // which may be invoked for a missing authorization code/refresh token. + if (string.IsNullOrEmpty(transaction.Request.Code)) + { + return null; + } + + var notification = new DeserializeAuthorizationCodeContext(transaction) + { + Token = transaction.Request.Code + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The authorization code was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating authorization codes ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + if (notification.Principal == null) + { + _logger.LogWarning("The authorization code extracted from the token request was invalid and was ignored."); + + return null; + } + + // Tickets are returned even if they are considered invalid (e.g expired). + + return new AuthenticationTicket((ClaimsIdentity) notification.Principal.Identity, new AuthenticationProperties()); + } + + case OpenIddictServerEndpointType.Token when transaction.Request.IsRefreshTokenGrantType(): + { + if (string.IsNullOrEmpty(transaction.Request.RefreshToken)) + { + return null; + } + + var notification = new DeserializeRefreshTokenContext(transaction) + { + Token = transaction.Request.RefreshToken + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The refresh token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating refresh tokens ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + if (notification.Principal == null) + { + _logger.LogWarning("The refresh token extracted from the token request was invalid and was ignored."); + + return null; + } + + // Tickets are returned even if they are considered invalid (e.g expired). + + return new AuthenticationTicket((ClaimsIdentity) notification.Principal.Identity, new AuthenticationProperties()); + } + + default: throw new InvalidOperationException("An identity cannot be extracted from this request."); + } + } + + protected override async Task TeardownCoreAsync() + { + // Note: OWIN authentication handlers cannot reliabily write to the response stream + // from ApplyResponseGrantAsync or ApplyResponseChallengeAsync because these methods + // are susceptible to be invoked from AuthenticationHandler.OnSendingHeaderCallback, + // where calling Write or WriteAsync on the response stream may result in a deadlock + // on hosts using streamed responses. To work around this limitation, this handler + // doesn't implement ApplyResponseGrantAsync but TeardownCoreAsync, which is never called + // by AuthenticationHandler.OnSendingHeaderCallback. In theory, this would prevent + // OpenIddictServerOwinMiddleware from both applying the response grant and allowing + // the next middleware in the pipeline to alter the response stream but in practice, + // OpenIddictServerOwinMiddleware is assumed to be the only middleware allowed to write + // to the response stream when a response grant (sign-in/out or challenge) was applied. + + var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); + if (challenge != null) + { + var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName); + if (transaction == null) + { + throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); + } + + var context = new ProcessChallengeResponseContext(transaction) + { + Response = new OpenIddictResponse + { + Error = GetProperty(challenge.Properties, Properties.Error), + ErrorDescription = GetProperty(challenge.Properties, Properties.ErrorDescription), + ErrorUri = GetProperty(challenge.Properties, Properties.ErrorUri) + } + }; + + await _provider.DispatchAsync(context); + + if (context.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + else if (context.IsRejected) + { + var notification = new ProcessErrorResponseContext(transaction) + { + Response = new OpenIddictResponse + { + Error = context.Error ?? Errors.InvalidRequest, + ErrorDescription = context.ErrorDescription, + ErrorUri = context.ErrorUri + } + }; + + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The OpenID Connect response was not correctly processed. This may indicate ") + .Append("that the event handler responsible of processing OpenID Connect responses ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + static string GetProperty(AuthenticationProperties properties, string name) + => properties != null && properties.Dictionary.TryGetValue(name, out string value) ? value : null; + } + + var signin = Helper.LookupSignIn(Options.AuthenticationType); + if (signin != null) + { + var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName); + if (transaction == null) + { + throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); + } + + var context = new ProcessSigninResponseContext(transaction) + { + Principal = signin.Principal, + Response = new OpenIddictResponse() + }; + + await _provider.DispatchAsync(context); + + if (context.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + else if (context.IsRejected) + { + var notification = new ProcessErrorResponseContext(transaction) + { + Response = new OpenIddictResponse + { + Error = context.Error ?? Errors.InvalidRequest, + ErrorDescription = context.ErrorDescription, + ErrorUri = context.ErrorUri + } + }; + + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The OpenID Connect response was not correctly processed. This may indicate ") + .Append("that the event handler responsible of processing OpenID Connect responses ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + } + + var signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode); + if (signout != null) + { + var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName); + if (transaction == null) + { + throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); + } + + var context = new ProcessSignoutResponseContext(transaction) + { + Response = new OpenIddictResponse() + }; + + await _provider.DispatchAsync(context); + + if (context.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + else if (context.IsRejected) + { + var notification = new ProcessErrorResponseContext(transaction) + { + Response = new OpenIddictResponse + { + Error = context.Error ?? Errors.InvalidRequest, + ErrorDescription = context.ErrorDescription, + ErrorUri = context.ErrorUri + } + }; + + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The OpenID Connect response was not correctly processed. This may indicate ") + .Append("that the event handler responsible of processing OpenID Connect responses ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + } + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs new file mode 100644 index 000000000..2c755e47f --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs @@ -0,0 +1,166 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Options; +using Owin; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server.Owin +{ + /// + /// Contains a collection of event handler filters commonly used by the OWIN handlers. + /// + public static class OpenIddictServerOwinHandlerFilters + { + /// + /// Represents a filter that excludes the associated handlers if the + /// pass-through mode was not enabled for the authorization endpoint. + /// + public class RequireAuthorizationEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireAuthorizationEndpointPassthroughEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.EnableAuthorizationEndpointPassthrough); + } + } + + /// + /// Represents a filter that excludes the associated handlers if error pass-through was not enabled. + /// + public class RequireErrorPassthroughEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireErrorPassthroughEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.EnableErrorPassthrough); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no OWIN request can be found. + /// + public class RequireOwinRequest : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(context.Transaction.GetOwinRequest() != null); + } + } + + /// + /// Represents a filter that excludes the associated handlers if the HTTPS requirement was disabled. + /// + public class RequireTransportSecurityRequirementEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireTransportSecurityRequirementEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(!_options.CurrentValue.DisableTransportSecurityRequirement); + } + } + + /// + /// Represents a filter that excludes the associated handlers if request caching was not enabled. + /// + public class RequireRequestCachingEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireRequestCachingEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.EnableRequestCaching); + } + } + + /// + /// Represents a filter that excludes the associated handlers if the + /// pass-through mode was not enabled for the authorization endpoint. + /// + public class RequireTokenEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireTokenEndpointPassthroughEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.EnableTokenEndpointPassthrough); + } + } + + /// + /// Represents a filter that excludes the associated handlers if the + /// pass-through mode was not enabled for the userinfo endpoint. + /// + public class RequireUserinfoEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireUserinfoEndpointPassthroughEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.EnableUserinfoEndpointPassthrough); + } + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs new file mode 100644 index 000000000..6a38691cf --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs @@ -0,0 +1,724 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Owin.Infrastructure; +using Newtonsoft.Json; +using Newtonsoft.Json.Bson; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using Owin; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers; +using static OpenIddict.Server.Owin.OpenIddictServerOwinConstants; +using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlerFilters; + +namespace OpenIddict.Server.Owin +{ + public static partial class OpenIddictServerOwinHandlers + { + public static class Authentication + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Authorization request extraction: + */ + ExtractGetOrPostRequest.Descriptor, + RestoreCachedRequestParameters.Descriptor, + CacheRequestParameters.Descriptor, + + /* + * Authorization request handling: + */ + EnablePassthroughMode.Descriptor, + + /* + * Authorization response processing: + */ + RemoveCachedRequest.Descriptor, + ProcessFormPostResponse.Descriptor, + ProcessQueryResponse.Descriptor, + ProcessPassthroughErrorResponse.Descriptor, + ProcessLocalErrorResponse.Descriptor); + + /// + /// Contains the logic responsible of restoring cached requests from the request_id, if specified. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class RestoreCachedRequestParameters : IOpenIddictServerHandler + { + private readonly IDistributedCache _cache; + + public RestoreCachedRequestParameters() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("A distributed cache instance must be registered when enabling request caching.") + .Append("To register the default in-memory distributed cache implementation, reference the ") + .Append("'Microsoft.Extensions.Caching.Memory' package and call ") + .Append("'services.AddDistributedMemoryCache()' from 'ConfigureServices'.") + .ToString()); + + public RestoreCachedRequestParameters([NotNull] IDistributedCache cache) + => _cache = cache; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ExtractGetOrPostRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ExtractAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a request_id parameter can be found in the authorization request, + // restore the complete authorization request from the distributed cache. + + if (string.IsNullOrEmpty(context.Request.RequestId)) + { + return; + } + + // Note: the cache key is always prefixed with a specific marker + // to avoid collisions with the other types of cached payloads. + var payload = await _cache.GetAsync(Cache.AuthorizationRequest + context.Request.RequestId); + if (payload == null) + { + context.Logger.LogError("The authorization request was rejected because an unknown " + + "or invalid request_id parameter was specified."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'request_id' parameter is invalid."); + + return; + } + + // Restore the authorization request parameters from the serialized payload. + using var reader = new BsonDataReader(new MemoryStream(payload)); + foreach (var parameter in JObject.Load(reader)) + { + // Avoid overriding the current request parameters. + if (context.Request.HasParameter(parameter.Key)) + { + continue; + } + + context.Request.SetParameter(parameter.Key, parameter.Value); + } + } + } + + /// + /// Contains the logic responsible of caching authorization requests, if applicable. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class CacheRequestParameters : IOpenIddictServerHandler + { + private readonly IDistributedCache _cache; + private readonly IOptionsMonitor _options; + + public CacheRequestParameters() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("A distributed cache instance must be registered when enabling request caching.") + .Append("To register the default in-memory distributed cache implementation, reference the ") + .Append("'Microsoft.Extensions.Caching.Memory' package and call ") + .Append("'services.AddDistributedMemoryCache()' from 'ConfigureServices'.") + .ToString()); + + public CacheRequestParameters( + [NotNull] IDistributedCache cache, + [NotNull] IOptionsMonitor options) + { + _cache = cache; + _options = options; + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(RestoreCachedRequestParameters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ExtractAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + // Don't cache the request if the request doesn't include any parameter. + // If a request_id parameter can be found in the authorization request, + // ignore the following logic to prevent an infinite redirect loop. + if (context.Request.GetParameters().IsEmpty || !string.IsNullOrEmpty(context.Request.RequestId)) + { + return; + } + + // Generate a 256-bit request identifier using a crypto-secure random number generator. + var data = new byte[256 / 8]; + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); + + context.Request.RequestId = Base64UrlEncoder.Encode(data); + + // Store the serialized authorization request parameters in the distributed cache. + var stream = new MemoryStream(); + using (var writer = new BsonDataWriter(stream)) + { + writer.CloseOutput = false; + + var serializer = JsonSerializer.CreateDefault(); + serializer.Serialize(writer, context.Request); + } + + // Note: the cache key is always prefixed with a specific marker + // to avoid collisions with the other types of cached payloads. + await _cache.SetAsync(Cache.AuthorizationRequest + context.Request.RequestId, + stream.ToArray(), _options.CurrentValue.RequestCachingPolicy); + + // Create a new GET authorization request containing only the request_id parameter. + var address = WebUtilities.AddQueryString( + uri: request.Scheme + "://" + request.Host + request.PathBase + request.Path, + name: Parameters.RequestId, + value: context.Request.RequestId); + + request.Context.Response.Redirect(address); + + // Mark the response as handled to skip the rest of the pipeline. + context.HandleRequest(); + } + } + + /// + /// Contains the logic responsible of enabling the pass-through mode for the received request. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class EnablePassthroughMode : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.SkipRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of removing cached authorization requests from the distributed cache. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class RemoveCachedRequest : IOpenIddictServerHandler + { + private readonly IDistributedCache _cache; + + public RemoveCachedRequest() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("A distributed cache instance must be registered when enabling request caching.") + .Append("To register the default in-memory distributed cache implementation, reference the ") + .Append("'Microsoft.Extensions.Caching.Memory' package and call ") + .Append("'services.AddDistributedMemoryCache()' from 'ConfigureServices'.") + .ToString()); + + public RemoveCachedRequest([NotNull] IDistributedCache cache) + => _cache = cache; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessFormPostResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.Request?.RequestId)) + { + return Task.CompletedTask; + } + + // Note: the ApplyAuthorizationResponse event is called for both successful + // and errored authorization responses but discrimination is not necessary here, + // as the authorization request must be removed from the distributed cache in both cases. + + // Note: the cache key is always prefixed with a specific marker + // to avoid collisions with the other types of cached payloads. + return _cache.RemoveAsync(Cache.AuthorizationRequest + context.Request.RequestId); + } + } + + /// + /// Contains the logic responsible of processing authorization responses using the form_post response mode. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ProcessFormPostResponse : IOpenIddictServerHandler + { + private readonly HtmlEncoder _encoder; + + public ProcessFormPostResponse([NotNull] HtmlEncoder encoder) + => _encoder = encoder; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessQueryResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var response = context.Transaction.GetOwinRequest()?.Context.Response; + if (response == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.RedirectUri) || + !string.Equals(context.ResponseMode, ResponseModes.FormPost, StringComparison.Ordinal)) + { + return; + } + + context.Logger.LogInformation("The authorization response was successfully returned to " + + "'{RedirectUri}' using the form post response mode: {Response}.", + context.RedirectUri, context.Response); + + using (var buffer = new MemoryStream()) + using (var writer = new StreamWriter(buffer)) + { + writer.WriteLine(""); + writer.WriteLine(""); + writer.WriteLine(""); + + // While the redirect_uri parameter should be guarded against unknown values + // by OpenIdConnectServerProvider.ValidateAuthorizationRequest, + // it's still safer to encode it to avoid cross-site scripting attacks + // if the authorization server has a relaxed policy concerning redirect URIs. + writer.WriteLine($@"
"); + + // Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters + // with the same name are used by derived drafts like the OAuth 2.0 token exchange specification. + // For consistency, multiple parameters with the same name are also supported by this endpoint. + foreach (var parameter in context.Response.GetFlattenedParameters()) + { + var key = _encoder.Encode(parameter.Key); + var value = _encoder.Encode(parameter.Value); + + writer.WriteLine($@""); + } + + writer.WriteLine(@""); + writer.WriteLine("
"); + writer.WriteLine(""); + writer.WriteLine(""); + writer.WriteLine(""); + writer.Flush(); + + response.StatusCode = 200; + response.ContentLength = buffer.Length; + response.ContentType = "text/html;charset=UTF-8"; + + response.Headers["Cache-Control"] = "no-cache"; + response.Headers["Pragma"] = "no-cache"; + response.Headers["Expires"] = "-1"; + + buffer.Seek(offset: 0, loc: SeekOrigin.Begin); + await buffer.CopyToAsync(response.Body, 4096); + } + + context.HandleRequest(); + } + } + + /// + /// Contains the logic responsible of processing authorization responses using the query response mode. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ProcessQueryResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessFragmentResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var response = context.Transaction.GetOwinRequest()?.Context.Response; + if (response == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.RedirectUri) || + !string.Equals(context.ResponseMode, ResponseModes.Query, StringComparison.Ordinal)) + { + return Task.CompletedTask; + } + + context.Logger.LogInformation("The authorization response was successfully returned to " + + "'{RedirectUri}' using the query response mode: {Response}.", + context.RedirectUri, context.Response); + + var location = context.RedirectUri; + + // Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters + // with the same name are used by derived drafts like the OAuth 2.0 token exchange specification. + // For consistency, multiple parameters with the same name are also supported by this endpoint. + foreach (var parameter in context.Response.GetFlattenedParameters()) + { + location = WebUtilities.AddQueryString(location, parameter.Key, parameter.Value); + } + + response.Redirect(location); + context.HandleRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of processing authorization responses using the fragment response mode. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ProcessFragmentResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessLocalErrorResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var response = context.Transaction.GetOwinRequest()?.Context.Response; + if (response == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.RedirectUri) || + !string.Equals(context.ResponseMode, ResponseModes.Fragment, StringComparison.Ordinal)) + { + return Task.CompletedTask; + } + + context.Logger.LogInformation("The authorization response was successfully returned to " + + "'{RedirectUri}' using the fragment response mode: {Response}.", + context.RedirectUri, context.Response); + + var builder = new StringBuilder(context.RedirectUri); + + // Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters + // with the same name are used by derived drafts like the OAuth 2.0 token exchange specification. + // For consistency, multiple parameters with the same name are also supported by this endpoint. + foreach (var parameter in context.Response.GetFlattenedParameters()) + { + builder.Append(Contains(builder, '#') ? '&' : '#') + .Append(Uri.EscapeDataString(parameter.Key)) + .Append('=') + .Append(Uri.EscapeDataString(parameter.Value)); + } + + response.Redirect(builder.ToString()); + context.HandleRequest(); + + return Task.CompletedTask; + + static bool Contains(StringBuilder builder, char delimiter) + { + for (var index = 0; index < builder.Length; index++) + { + if (builder[index] == delimiter) + { + return true; + } + } + + return false; + } + } + } + + /// + /// Contains the logic responsible of processing authorization responses that must be handled by another + /// middleware in the pipeline at a later stage (e.g an ASP.NET MVC action or a NancyFX module). + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ProcessPassthroughErrorResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessLocalErrorResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var response = context.Transaction.GetOwinRequest()?.Context.Response; + if (response == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.Response.Error) || !string.IsNullOrEmpty(context.RedirectUri)) + { + return Task.CompletedTask; + } + + // Don't return the state originally sent by the client application. + context.Response.State = null; + + // Apply a 400 status code by default. + response.StatusCode = 400; + + context.SkipRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of processing authorization responses that must be returned as plain-text. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ProcessLocalErrorResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var response = context.Transaction.GetOwinRequest()?.Context.Response; + if (response == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.Response.Error) || !string.IsNullOrEmpty(context.RedirectUri)) + { + return; + } + + // Don't return the state originally sent by the client application. + context.Response.State = null; + + // Apply a 400 status code by default. + response.StatusCode = 400; + + context.Logger.LogInformation("The authorization response was successfully returned " + + "as a plain-text document: {Response}.", context.Response); + + using (var buffer = new MemoryStream()) + using (var writer = new StreamWriter(buffer)) + { + foreach (var parameter in context.Response.GetParameters()) + { + // Ignore null or empty parameters, including JSON + // objects that can't be represented as strings. + var value = (string) parameter.Value; + if (string.IsNullOrEmpty(value)) + { + continue; + } + + writer.WriteLine("{0}:{1}", parameter.Key, value); + } + + writer.Flush(); + + response.ContentLength = buffer.Length; + response.ContentType = "text/plain;charset=UTF-8"; + + response.Headers["Cache-Control"] = "no-cache"; + response.Headers["Pragma"] = "no-cache"; + response.Headers["Expires"] = "Thu, 01 Jan 1970 00:00:00 GMT"; + + buffer.Seek(offset: 0, loc: SeekOrigin.Begin); + await buffer.CopyToAsync(response.Body, 4096, response.Context.Request.CallCancelled); + } + + context.HandleRequest(); + } + } + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Discovery.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Discovery.cs new file mode 100644 index 000000000..4d4ffd9ac --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Discovery.cs @@ -0,0 +1,108 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Owin; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlerFilters; + +namespace OpenIddict.Server.Owin +{ + public static partial class OpenIddictServerOwinHandlers + { + public static class Discovery + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Configuration request extraction: + */ + ExtractGetRequest.Descriptor, + + /* + * Configuration request handling: + */ + InferIssuerFromHost.Descriptor, + + /* + * Configuration response processing: + */ + ProcessJsonResponse.Descriptor, + + /* + * Cryptography request extraction: + */ + ExtractGetRequest.Descriptor, + + /* + * Cryptography response processing: + */ + ProcessJsonResponse.Descriptor); + + /// + /// Contains the logic responsible of infering the issuer URL from the HTTP request host. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class InferIssuerFromHost : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + // Note: this handler must be invoked after AttachIssuer and before AttachEndpoints. + .UseSingletonHandler() + .SetOrder(OpenIddictServerHandlers.Discovery.AttachIssuer.Descriptor.Order + 500) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + // If the issuer was not populated by another handler (e.g from the server options), + // try to infer it from the request scheme/host/path base (which requires HTTP/1.1). + if (context.Issuer == null) + { + if (string.IsNullOrEmpty(request.Host.Value)) + { + throw new InvalidOperationException("No host was attached to the HTTP request."); + } + + if (!Uri.TryCreate(request.Scheme + "://" + request.Host + request.PathBase, UriKind.Absolute, out Uri issuer)) + { + throw new InvalidOperationException("The issuer address cannot be inferred from the current request."); + } + + context.Issuer = issuer; + } + + return Task.CompletedTask; + } + } + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs new file mode 100644 index 000000000..8b4b1a6f5 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using JetBrains.Annotations; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers; +using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlerFilters; + +namespace OpenIddict.Server.Owin +{ + public static partial class OpenIddictServerOwinHandlers + { + public static class Exchange + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Token request extraction: + */ + ExtractPostRequest.Descriptor, + ExtractBasicAuthenticationCredentials.Descriptor, + + /* + * Token request handling: + */ + EnablePassthroughMode.Descriptor, + + /* + * Token response processing: + */ + ProcessJsonResponse.Descriptor); + + /// + /// Contains the logic responsible of enabling the pass-through mode for the received request. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class EnablePassthroughMode : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.SkipRequest(); + + return Task.CompletedTask; + } + } + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Serialization.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Serialization.cs new file mode 100644 index 000000000..2f0551937 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Serialization.cs @@ -0,0 +1,160 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Owin; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers.Serialization; +using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlerFilters; + +namespace OpenIddict.Server.Owin +{ + public static partial class OpenIddictServerOwinHandlers + { + public static class Serialization + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Token serialization: + */ + InferIssuerFromHostForTokenSerialization.Descriptor, + InferIssuerFromHostForTokenSerialization.Descriptor, + InferIssuerFromHostForTokenSerialization.Descriptor, + InferIssuerFromHostForTokenSerialization.Descriptor, + + /* + * Token deserialization: + */ + InferIssuerFromHostForTokenDeserialization.Descriptor, + InferIssuerFromHostForTokenDeserialization.Descriptor, + InferIssuerFromHostForTokenDeserialization.Descriptor, + InferIssuerFromHostForTokenDeserialization.Descriptor); + } + + /// + /// Contains the logic responsible of infering the issuer URL from the HTTP request host for token deserialization. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class InferIssuerFromHostForTokenSerialization : IOpenIddictServerHandler + where TContext : BaseSerializingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(AttachIdentityTokenSerializationParameters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + // If the issuer was not populated by another handler (e.g from the server options), + // try to infer it from the request scheme/host/path base (which requires HTTP/1.1). + if (context.Issuer == null) + { + if (string.IsNullOrEmpty(request.Host.Value)) + { + throw new InvalidOperationException("No host was attached to the HTTP request."); + } + + if (!Uri.TryCreate(request.Scheme + "://" + request.Host + request.PathBase, UriKind.Absolute, out Uri issuer)) + { + throw new InvalidOperationException("The issuer address cannot be inferred from the current request."); + } + + context.Issuer = issuer; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of infering the discovery document issuer URL from the HTTP request host. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class InferIssuerFromHostForTokenDeserialization : IOpenIddictServerHandler + where TContext : BaseDeserializingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(AttachIdentityTokenDeserializationParameters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + // If the issuer was not populated by another handler (e.g from the server options), + // try to infer it from the request scheme/host/path base (which requires HTTP/1.1). + if (context.TokenValidationParameters != null && context.TokenValidationParameters.ValidIssuer == null) + { + if (string.IsNullOrEmpty(request.Host.Value)) + { + throw new InvalidOperationException("No host was attached to the HTTP request."); + } + + if (!Uri.TryCreate(request.Scheme + "://" + request.Host + request.PathBase, UriKind.Absolute, out Uri issuer)) + { + throw new InvalidOperationException("The issuer address cannot be inferred from the current request."); + } + + context.TokenValidationParameters.ValidIssuer = issuer.AbsoluteUri; + } + + return Task.CompletedTask; + } + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs new file mode 100644 index 000000000..4cad76bb8 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -0,0 +1,652 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Microsoft.Owin; +using Newtonsoft.Json; +using OpenIddict.Abstractions; +using Owin; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlerFilters; + +namespace OpenIddict.Server.Owin +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public static partial class OpenIddictServerOwinHandlers + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Top-level request processing: + */ + InferEndpointType.Descriptor, + ValidateTransportSecurityRequirement.Descriptor, + ValidateHost.Descriptor) + .AddRange(Authentication.DefaultHandlers) + .AddRange(Discovery.DefaultHandlers) + .AddRange(Exchange.DefaultHandlers) + .AddRange(Serialization.DefaultHandlers); + + /// + /// Contains the logic responsible of inferring the endpoint type from the request address. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class InferEndpointType : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + // Note: this handler must be invoked before any other handler, + // including the built-in handlers defined in OpenIddict.Server. + .SetOrder(int.MinValue + 50_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + context.EndpointType = + Matches(context.Options.AuthorizationEndpointUris) ? OpenIddictServerEndpointType.Authorization : + Matches(context.Options.ConfigurationEndpointUris) ? OpenIddictServerEndpointType.Configuration : + Matches(context.Options.CryptographyEndpointUris) ? OpenIddictServerEndpointType.Cryptography : + Matches(context.Options.IntrospectionEndpointUris) ? OpenIddictServerEndpointType.Introspection : + Matches(context.Options.LogoutEndpointUris) ? OpenIddictServerEndpointType.Logout : + Matches(context.Options.RevocationEndpointUris) ? OpenIddictServerEndpointType.Revocation : + Matches(context.Options.TokenEndpointUris) ? OpenIddictServerEndpointType.Token : + Matches(context.Options.UserinfoEndpointUris) ? OpenIddictServerEndpointType.Userinfo : + OpenIddictServerEndpointType.Unknown; + + return Task.CompletedTask; + + bool Matches(IList addresses) + { + for (var index = 0; index < addresses.Count; index++) + { + var address = addresses[index]; + if (address.IsAbsoluteUri) + { + if (!string.Equals(address.Scheme, request.Scheme, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var host = HostString.FromUriComponent(address); + if (host != request.Host) + { + continue; + } + + var path = PathString.FromUriComponent(address); + if (path == request.PathBase + request.Path || + path == request.PathBase + request.Path + new PathString("/")) + { + return true; + } + } + + else if (address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { + var path = new PathString(address.OriginalString); + if (path == request.Path || path == request.Path + new PathString("/")) + { + return true; + } + } + } + + return false; + } + } + } + + /// + /// Contains the logic responsible of rejecting OpenID Connect requests that don't use transport security. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ValidateTransportSecurityRequirement : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(InferEndpointType.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + // Don't require that the host be present if the request is not handled by OpenIddict. + if (context.EndpointType == OpenIddictServerEndpointType.Unknown) + { + return Task.CompletedTask; + } + + // Reject authorization requests sent without transport security. + if (!request.IsSecure) + { + context.Reject( + error: Errors.InvalidRequest, + description: "This server only accepts HTTPS requests."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of ensuring the host can be inferred from the request if none was set in the options. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ValidateHost : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateTransportSecurityRequirement.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + // Don't require that the request host be present if the request is not handled + // by an OpenIddict endpoint or if an explicit issuer URL was set in the options. + if (context.Options.Issuer != null || context.EndpointType == OpenIddictServerEndpointType.Unknown) + { + return Task.CompletedTask; + } + + if (string.IsNullOrEmpty(request.Host.Value)) + { + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'Host' header is missing."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of extracting OpenID Connect requests from GET HTTP requests. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ExtractGetRequest : IOpenIddictServerHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ValidateHost.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + if (string.Equals(request.Method, "GET", StringComparison.OrdinalIgnoreCase)) + { + context.Request = new OpenIddictRequest(request.Query); + } + + else + { + context.Logger.LogError("The request was rejected because an invalid " + + "HTTP method was specified: {Method}.", request.Method); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified HTTP method is not valid."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of extracting OpenID Connect requests from GET or POST HTTP requests. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ExtractGetOrPostRequest : IOpenIddictServerHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractGetRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + if (string.Equals(request.Method, "GET", StringComparison.OrdinalIgnoreCase)) + { + context.Request = new OpenIddictRequest(request.Query); + } + + else if (string.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase)) + { + // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization + if (string.IsNullOrEmpty(request.ContentType)) + { + context.Logger.LogError("The request was rejected because the mandatory 'Content-Type' header was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'Content-Type' header must be specified."); + + return; + } + + // May have media/type; charset=utf-8, allow partial match. + if (!request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) + { + context.Logger.LogError("The request was rejected because an invalid 'Content-Type' " + + "header was specified: {ContentType}.", request.ContentType); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'Content-Type' header is not valid."); + + return; + } + + context.Request = new OpenIddictRequest(await request.ReadFormAsync()); + } + + else + { + context.Logger.LogError("The request was rejected because an invalid " + + "HTTP method was specified: {Method}.", request.Method); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified HTTP method is not valid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of extracting OpenID Connect requests from POST HTTP requests. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ExtractPostRequest : IOpenIddictServerHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractGetOrPostRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + if (string.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase)) + { + // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization + if (string.IsNullOrEmpty(request.ContentType)) + { + context.Logger.LogError("The request was rejected because the mandatory 'Content-Type' header was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'Content-Type' header must be specified."); + + return; + } + + // May have media/type; charset=utf-8, allow partial match. + if (!request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) + { + context.Logger.LogError("The request was rejected because an invalid 'Content-Type' " + + "header was specified: {ContentType}.", request.ContentType); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'Content-Type' header is not valid."); + + return; + } + + context.Request = new OpenIddictRequest(await request.ReadFormAsync()); + } + + else + { + context.Logger.LogError("The request was rejected because an invalid " + + "HTTP method was specified: {Method}.", request.Method); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified HTTP method is not valid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting token requests that specify an invalid grant type. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ExtractBasicAuthenticationCredentials : IOpenIddictServerHandler + where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractPostRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + var header = request.Headers["Authorization"]; + if (string.IsNullOrEmpty(header) || !header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) + { + return Task.CompletedTask; + } + + // At this point, reject requests that use multiple client authentication methods. + // See https://tools.ietf.org/html/rfc6749#section-2.3 for more information. + if (!string.IsNullOrEmpty(context.Request.ClientAssertion) || !string.IsNullOrEmpty(context.Request.ClientSecret)) + { + context.Logger.LogError("The request was rejected because multiple client credentials were specified."); + + context.Reject( + error: Errors.InvalidRequest, + description: "Multiple client credentials cannot be specified."); + + return Task.CompletedTask; + } + + try + { + var value = header.Substring("Basic ".Length).Trim(); + var data = Encoding.ASCII.GetString(Convert.FromBase64String(value)); + + var index = data.IndexOf(':'); + if (index < 0) + { + context.Reject( + error: Errors.InvalidRequest, + description: "The specified client credentials are invalid."); + + return Task.CompletedTask; + } + + // Attach the basic authentication credentials to the request message. + context.Request.ClientId = UnescapeDataString(data.Substring(0, index)); + context.Request.ClientSecret = UnescapeDataString(data.Substring(index + 1)); + + return Task.CompletedTask; + } + + catch + { + context.Reject( + error: Errors.InvalidRequest, + description: "The specified client credentials are invalid."); + + return Task.CompletedTask; + } + + static string UnescapeDataString(string data) + { + if (string.IsNullOrEmpty(data)) + { + return null; + } + + return Uri.UnescapeDataString(data.Replace("+", "%20")); + } + } + } + + /// + /// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ProcessJsonResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Response == null) + { + throw new InvalidOperationException("This handler cannot be invoked without a response attached."); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var response = context.Transaction.GetOwinRequest()?.Context.Response; + if (response == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + using (var buffer = new MemoryStream()) + using (var writer = new JsonTextWriter(new StreamWriter(buffer))) + { + var serializer = JsonSerializer.CreateDefault(); + serializer.Serialize(writer, context.Response); + + writer.Flush(); + + if (!string.IsNullOrEmpty(context.Response.Error)) + { + // Note: when using basic authentication, returning an invalid_client error MUST result in + // an unauthorized response but returning a 401 status code would invoke the previously + // registered authentication middleware and potentially replace it by a 302 response. + // To work around this OWIN/Katana limitation, a 400 response code is always returned. + response.StatusCode = 400; + } + + response.ContentLength = buffer.Length; + response.ContentType = "application/json;charset=UTF-8"; + + buffer.Seek(offset: 0, loc: SeekOrigin.Begin); + await buffer.CopyToAsync(response.Body, 4096, response.Context.Request.CallCancelled); + } + + context.HandleRequest(); + } + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHelpers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHelpers.cs new file mode 100644 index 000000000..e9255dfb7 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHelpers.cs @@ -0,0 +1,110 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using JetBrains.Annotations; +using Microsoft.Owin; +using OpenIddict.Abstractions; +using OpenIddict.Server; +using OpenIddict.Server.Owin; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace Owin +{ + /// + /// Exposes companion extensions for the OpenIddict/OWIN integration. + /// + public static class OpenIddictServerOwinHelpers + { + /// + /// Registers the OpenIddict server OWIN middleware in the application pipeline. + /// Note: when using a dependency injection container supporting per-request + /// middleware resolution (like Autofac), calling this method is NOT recommended. + /// + /// The application builder used to register middleware instances. + /// The . + public static IAppBuilder UseOpenIddictServer([NotNull] this IAppBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.Use(); + } + + /// + /// Retrieves the instance stored in the properties. + /// + /// The transaction instance. + /// The instance or null if it couldn't be found. + public static IOwinRequest GetOwinRequest([NotNull] this OpenIddictServerTransaction transaction) + { + if (transaction == null) + { + throw new ArgumentNullException(nameof(transaction)); + } + + if (!transaction.Properties.TryGetValue(typeof(IOwinRequest).FullName, out object property)) + { + return null; + } + + if (property is WeakReference reference && reference.TryGetTarget(out IOwinRequest request)) + { + return request; + } + + return null; + } + + /// + /// Retrieves the instance stored in . + /// + /// The context instance. + /// The . + public static OpenIddictServerEndpointType GetOpenIddictServerEndpointType([NotNull] this IOwinContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return context.Get(typeof(OpenIddictServerTransaction).FullName)?.EndpointType ?? default; + } + + /// + /// Retrieves the instance stored in . + /// + /// The context instance. + /// The instance or null if it couldn't be found. + public static OpenIddictRequest GetOpenIddictServerRequest([NotNull] this IOwinContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return context.Get(typeof(OpenIddictServerTransaction).FullName)?.Request; + } + + /// + /// Retrieves the instance stored in . + /// + /// The context instance. + /// The instance or null if it couldn't be found. + public static OpenIddictResponse GetOpenIddictServerResponse([NotNull] this IOwinContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return context.Get(typeof(OpenIddictServerTransaction).FullName)?.Response; + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs new file mode 100644 index 000000000..1f7531fda --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Owin; +using Microsoft.Owin.Security.Infrastructure; + +namespace OpenIddict.Server.Owin +{ + /// + /// Provides the entry point necessary to register the OpenIddict server in an OWIN pipeline. + /// Note: this middleware is intented to be used with dependency injection containers + /// that support middleware resolution, like Autofac. Since it depends on scoped services, + /// it is NOT recommended to instantiate it as a singleton like a regular OWIN middleware. + /// + public class OpenIddictServerOwinMiddleware : AuthenticationMiddleware + { + private readonly ILogger _logger; + private readonly IOpenIddictServerProvider _provider; + + /// + /// Creates a new instance of the class. + /// + /// The next middleware in the pipeline, if applicable. + /// The logger used by this middleware. + /// The OpenIddict server OWIN options. + /// The OpenIddict server provider. + public OpenIddictServerOwinMiddleware( + [CanBeNull] OwinMiddleware next, + [NotNull] ILogger logger, + [NotNull] IOptionsMonitor options, + [NotNull] IOpenIddictServerProvider provider) + : base(next, options.CurrentValue) + { + _logger = logger; + _provider = provider; + } + + /// + /// Creates and returns a new instance. + /// + /// A new instance of the class. + protected override AuthenticationHandler CreateHandler() + => new OpenIddictServerOwinHandler(_logger, _provider); + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddlewareFactory.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddlewareFactory.cs new file mode 100644 index 000000000..124bf2bc6 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddlewareFactory.cs @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Owin; + +namespace OpenIddict.Server.Owin +{ + /// + /// Provides the entry point necessary to instantiate and register the scoped + /// in an OWIN/Katana pipeline. + /// + public class OpenIddictServerOwinMiddlewareFactory : OwinMiddleware + { + /// + /// Creates a new instance of the class. + /// + /// The next middleware in the pipeline, if applicable. + public OpenIddictServerOwinMiddlewareFactory([CanBeNull] OwinMiddleware next) + : base(next) + { + } + + /// + /// Resolves the instance from the OWIN context + /// and creates a new instance of the class, + /// which is used to register in the pipeline. + /// + /// The . + /// + /// A that can be used to monitor the asynchronous operation. + /// + public override Task Invoke([NotNull] IOwinContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var provider = context.Get(typeof(IServiceProvider).FullName); + if (provider == null) + { + throw new InvalidOperationException(new StringBuilder() + .Append("No service provider was found in the OWIN context. For the OpenIddict server ") + .Append("services to work correctly, a per-request 'IServiceProvider' must be attached ") + .AppendLine("to the OWIN environment with the dictionary key 'System.IServiceProvider'.") + .Append("Note: when using a dependency injection container supporting middleware resolution ") + .Append("(like Autofac), the 'app.UseOpenIddictServer()' extension MUST NOT be called.") + .ToString()); + } + + // Note: the Microsoft.Extensions.DependencyInjection container doesn't support resolving services + // with arbitrary parameters, which prevents the server OWIN middleware from being resolved directly + // from the DI container, as the next middleware in the pipeline cannot be specified as a parameter. + // To work around this limitation, the server OWIN middleware is manually instantiated and invoked. + var middleware = new OpenIddictServerOwinMiddleware( + next: Next, + logger: GetRequiredService>(provider), + options: GetRequiredService>(provider), + provider: GetRequiredService(provider)); + + return middleware.Invoke(context); + + static T GetRequiredService(IServiceProvider provider) + => provider.GetService() ?? throw new InvalidOperationException(new StringBuilder() + .AppendLine("The OpenIddict server authentication services cannot be resolved from the DI container.") + .Append("To register the OWIN services, use 'services.AddOpenIddict().AddServer().UseOwin()'.") + .ToString()); + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs new file mode 100644 index 000000000..11c60e307 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Owin; + +namespace OpenIddict.Server.Owin +{ + /// + /// Provides various settings needed to configure the OpenIddict OWIN server integration. + /// + public class OpenIddictServerOwinOptions : AuthenticationOptions + { + /// + /// Creates a new instance of the class. + /// + public OpenIddictServerOwinOptions() + : base(OpenIddictServerOwinDefaults.AuthenticationType) + => AuthenticationMode = AuthenticationMode.Passive; + + /// + /// Gets or sets a boolean indicating whether incoming requests arriving on insecure endpoints should be rejected. + /// By default, this property is set to false to help mitigate man-in-the-middle attacks. + /// + public bool DisableTransportSecurityRequirement { get; set; } + + /// + /// Gets or sets a boolean indicating whether the pass-through mode is enabled for the authorization endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + public bool EnableAuthorizationEndpointPassthrough { get; set; } + + /// + /// Gets or sets a boolean indicating whether OpenIddict should allow the rest of the request processing pipeline + /// to be invoked when returning an error from the interactive authorization and logout endpoints. + /// When this option is enabled, special logic must be added to these actions to handle errors, that can be + /// retrieved using + /// + public bool EnableErrorPassthrough { get; set; } + + /// + /// Gets or sets a boolean indicating whether the pass-through mode is enabled for the token endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + public bool EnableTokenEndpointPassthrough { get; set; } + + /// + /// Gets or sets a boolean indicating whether the pass-through mode is enabled for the userinfo endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + public bool EnableUserinfoEndpointPassthrough { get; set; } + + /// + /// Gets or sets a boolean indicating whether request caching should be enabled. + /// When enabled, both authorization and logout requests are automatically stored + /// in the distributed cache, which allows flowing large payloads across requests. + /// Enabling this option is recommended when using external authentication providers + /// or when large GET or POST OpenID Connect authorization requests support is required. + /// + public bool EnableRequestCaching { get; set; } + + /// + /// Gets or sets the caching policy used to determine how long the authorization + /// and end session requests should be cached by the distributed cache implementation. + /// + public DistributedCacheEntryOptions RequestCachingPolicy { get; set; } = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1), + SlidingExpiration = TimeSpan.FromMinutes(30) + }; + } +} diff --git a/src/OpenIddict.Server/IOpenIddictServerEventDispatcher.cs b/src/OpenIddict.Server/IOpenIddictServerEventDispatcher.cs deleted file mode 100644 index 1d052114f..000000000 --- a/src/OpenIddict.Server/IOpenIddictServerEventDispatcher.cs +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Threading; -using System.Threading.Tasks; -using JetBrains.Annotations; - -namespace OpenIddict.Server -{ - /// - /// Dispatches events by invoking the corresponding handlers. - /// - public interface IOpenIddictServerEventDispatcher - { - /// - /// Publishes a new event. - /// - /// The type of the event to publish. - /// The event to publish. - /// A that can be used to monitor the asynchronous operation. - Task DispatchAsync([NotNull] TEvent notification) where TEvent : class, IOpenIddictServerEvent; - } -} diff --git a/src/OpenIddict.Server/IOpenIddictServerEventHandler.cs b/src/OpenIddict.Server/IOpenIddictServerHandler.cs similarity index 57% rename from src/OpenIddict.Server/IOpenIddictServerEventHandler.cs rename to src/OpenIddict.Server/IOpenIddictServerHandler.cs index b2746180f..af9caaabf 100644 --- a/src/OpenIddict.Server/IOpenIddictServerEventHandler.cs +++ b/src/OpenIddict.Server/IOpenIddictServerHandler.cs @@ -6,23 +6,23 @@ using System.Threading.Tasks; using JetBrains.Annotations; +using static OpenIddict.Server.OpenIddictServerEvents; namespace OpenIddict.Server { /// - /// Represents a handler able to process events. + /// Represents a handler able to process events. /// - /// The type of the events handled by this instance. - public interface IOpenIddictServerEventHandler where TEvent : class, IOpenIddictServerEvent + /// The type of the context associated with events handled by this instance. + public interface IOpenIddictServerHandler where TContext : BaseContext { /// /// Processes the event. /// - /// The event to process. + /// The context associated with the event to process. /// - /// A that can be used to monitor the asynchronous operation, - /// whose result determines whether next handlers in the pipeline are invoked. + /// A that can be used to monitor the asynchronous operation. /// - Task HandleAsync([NotNull] TEvent notification); + Task HandleAsync([NotNull] TContext context); } } diff --git a/src/OpenIddict.Server/IOpenIddictServerEvent.cs b/src/OpenIddict.Server/IOpenIddictServerHandlerFilter.cs similarity index 50% rename from src/OpenIddict.Server/IOpenIddictServerEvent.cs rename to src/OpenIddict.Server/IOpenIddictServerHandlerFilter.cs index 9ed87c85b..03958d316 100644 --- a/src/OpenIddict.Server/IOpenIddictServerEvent.cs +++ b/src/OpenIddict.Server/IOpenIddictServerHandlerFilter.cs @@ -4,10 +4,14 @@ * the license and the contributors participating to this project. */ +using System.Threading.Tasks; +using JetBrains.Annotations; +using static OpenIddict.Server.OpenIddictServerEvents; + namespace OpenIddict.Server { - /// - /// Represents an OpenIddict server event. - /// - public interface IOpenIddictServerEvent { } + public interface IOpenIddictServerHandlerFilter where TContext : BaseContext + { + Task IsActiveAsync([NotNull] TContext context); + } } diff --git a/src/OpenIddict.Server/IOpenIddictServerProvider.cs b/src/OpenIddict.Server/IOpenIddictServerProvider.cs new file mode 100644 index 000000000..1d5428e27 --- /dev/null +++ b/src/OpenIddict.Server/IOpenIddictServerProvider.cs @@ -0,0 +1,18 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Threading.Tasks; +using JetBrains.Annotations; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server +{ + public interface IOpenIddictServerProvider + { + ValueTask CreateTransactionAsync(); + Task DispatchAsync([NotNull] TContext context) where TContext : BaseContext; + } +} \ No newline at end of file diff --git a/src/OpenIddict.Server/OpenIddict.Server.csproj b/src/OpenIddict.Server/OpenIddict.Server.csproj index 3335441be..c2a63ab21 100644 --- a/src/OpenIddict.Server/OpenIddict.Server.csproj +++ b/src/OpenIddict.Server/OpenIddict.Server.csproj @@ -1,15 +1,12 @@  - - - netstandard2.0 + net472;netstandard2.0;netstandard2.1 OpenID Connect server components for OpenIddict. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security + $(PackageTags);server @@ -17,16 +14,21 @@ - - - - - + + - - - + + $(DefineConstants);SUPPORTS_CERTIFICATE_GENERATION + $(DefineConstants);SUPPORTS_DIRECT_KEY_CREATION_WITH_SPECIFIED_SIZE + $(DefineConstants);SUPPORTS_ECDSA + + + + $(DefineConstants);SUPPORTS_CERTIFICATE_HASHING_WITH_SPECIFIED_ALGORITHM + $(DefineConstants);SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + $(DefineConstants);SUPPORTS_TIME_CONSTANT_COMPARISONS + diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index 1793d59d8..3ef0d6689 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -5,21 +5,18 @@ */ using System; -using System.Collections.Generic; using System.ComponentModel; -using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; +using System.Text; using JetBrains.Annotations; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; -using OpenIddict.Extensions; using OpenIddict.Server; namespace Microsoft.Extensions.DependencyInjection @@ -34,14 +31,7 @@ public class OpenIddictServerBuilder ///
/// The services collection. public OpenIddictServerBuilder([NotNull] IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - Services = services; - } + => Services = services ?? throw new ArgumentNullException(nameof(services)); /// /// Gets the services collection. @@ -50,70 +40,79 @@ public OpenIddictServerBuilder([NotNull] IServiceCollection services) public IServiceCollection Services { get; } /// - /// Registers an inline event handler for the specified event type. + /// Registers an event handler using the specified configuration delegate. /// - /// The handler delegate. + /// The event context type. + /// The configuration delegate. /// The . [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictServerBuilder AddEventHandler( - [NotNull] Func> handler) - where TEvent : class, IOpenIddictServerEvent + public OpenIddictServerBuilder AddEventHandler( + [NotNull] Action> configuration) + where TContext : OpenIddictServerEvents.BaseContext { - if (handler == null) + if (configuration == null) { - throw new ArgumentNullException(nameof(handler)); + throw new ArgumentNullException(nameof(configuration)); } - Services.AddSingleton>( - new OpenIddictServerEventHandler(handler)); + var builder = OpenIddictServerHandlerDescriptor.CreateBuilder(); + configuration(builder); - return this; + return AddEventHandler(builder.Build()); } /// - /// Registers an event handler that will be invoked for all the events listed by the implemented interfaces. + /// Registers an event handler using the specified descriptor. /// - /// The type of the handler. - /// The lifetime of the registered service. + /// The handler descriptor. /// The . [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictServerBuilder AddEventHandler(ServiceLifetime lifetime = ServiceLifetime.Scoped) - => AddEventHandler(typeof(THandler), lifetime); + public OpenIddictServerBuilder AddEventHandler([NotNull] OpenIddictServerHandlerDescriptor descriptor) + { + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + // Register the handler in the services collection. + Services.Add(descriptor.ServiceDescriptor); + + return Configure(options => options.CustomHandlers.Add(descriptor)); + } /// - /// Registers an event handler that will be invoked for all the events listed by the implemented interfaces. + /// Removes the event handler that matches the specified descriptor. /// - /// The type of the handler. - /// The lifetime of the registered service. + /// The descriptor corresponding to the handler to remove. /// The . [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictServerBuilder AddEventHandler([NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) + public OpenIddictServerBuilder RemoveEventHandler([NotNull] OpenIddictServerHandlerDescriptor descriptor) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - if (lifetime == ServiceLifetime.Transient) - { - throw new ArgumentException("Handlers cannot be registered as transient services.", nameof(lifetime)); - } - - if (type.IsGenericTypeDefinition) + if (descriptor == null) { - throw new ArgumentException("The specified type is invalid.", nameof(type)); + throw new ArgumentNullException(nameof(descriptor)); } - var services = OpenIddictHelpers.FindGenericBaseTypes(type, typeof(IOpenIddictServerEventHandler<>)).ToArray(); - if (services.Length == 0) - { - throw new ArgumentException("The specified type is invalid.", nameof(type)); - } + Services.RemoveAll(descriptor.ServiceDescriptor.ServiceType); - foreach (var service in services) + Services.PostConfigure(options => { - Services.Add(new ServiceDescriptor(service, type, lifetime)); - } + for (var index = options.CustomHandlers.Count - 1; index >= 0; index--) + { + if (options.CustomHandlers[index].ServiceDescriptor.ServiceType == descriptor.ServiceDescriptor.ServiceType) + { + options.CustomHandlers.RemoveAt(index); + } + } + + for (var index = options.DefaultHandlers.Count - 1; index >= 0; index--) + { + if (options.DefaultHandlers[index].ServiceDescriptor.ServiceType == descriptor.ServiceDescriptor.ServiceType) + { + options.DefaultHandlers.RemoveAt(index); + } + } + }); return this; } @@ -131,7 +130,7 @@ public OpenIddictServerBuilder Configure([NotNull] Action Configure(options => options.AcceptAnonymousClients = true); + /// + /// Registers the used to encrypt the tokens issued by OpenIddict. + /// + /// The encrypting credentials. + /// The . + public OpenIddictServerBuilder AddEncryptionCredentials([NotNull] EncryptingCredentials credentials) + { + if (credentials == null) + { + throw new ArgumentNullException(nameof(credentials)); + } + + return Configure(options => options.EncryptionCredentials.Add(credentials)); + } + + /// + /// Registers a used to encrypt the access tokens issued by OpenIddict. + /// + /// The security key. + /// The . + public OpenIddictServerBuilder AddEncryptionKey([NotNull] SecurityKey key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + // If the encryption key is an asymmetric security key, ensure it has a private key. + if (key is AsymmetricSecurityKey asymmetricSecurityKey && + asymmetricSecurityKey.PrivateKeyStatus == PrivateKeyStatus.DoesNotExist) + { + throw new InvalidOperationException("The asymmetric encryption key doesn't contain the required private key."); + } + + if (IsAlgorithmSupported(key, SecurityAlgorithms.Aes256KW)) + { + return AddEncryptionCredentials(new EncryptingCredentials(key, + SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512)); + } + + if (IsAlgorithmSupported(key, SecurityAlgorithms.RsaOAEP)) + { + return AddEncryptionCredentials(new EncryptingCredentials(key, + SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512)); + } + + throw new InvalidOperationException(new StringBuilder() + .AppendLine("An encryption algorithm cannot be automatically inferred from the encrypting key.") + .Append("Consider using 'options.AddEncryptionCredentials(EncryptingCredentials)' instead.") + .ToString()); + + static bool IsAlgorithmSupported(SecurityKey key, string algorithm) => + key.CryptoProviderFactory.IsSupportedAlgorithm(algorithm, key); + } + /// /// Registers (and generates if necessary) a user-specific development - /// certificate used to sign the JWT tokens issued by OpenIddict. + /// certificate used to encrypt the tokens issued by OpenIddict. /// /// The . - public OpenIddictServerBuilder AddDevelopmentSigningCertificate() - => Configure(options => options.SigningCredentials.AddDevelopmentCertificate()); + public OpenIddictServerBuilder AddDevelopmentEncryptionCertificate() + => AddDevelopmentEncryptionCertificate(new X500DistinguishedName("CN=OpenIddict Server Encryption Certificate")); /// /// Registers (and generates if necessary) a user-specific development - /// certificate used to sign the JWT tokens issued by OpenIddict. + /// certificate used to encrypt the tokens issued by OpenIddict. /// /// The subject name associated with the certificate. /// The . - public OpenIddictServerBuilder AddDevelopmentSigningCertificate([NotNull] X500DistinguishedName subject) + public OpenIddictServerBuilder AddDevelopmentEncryptionCertificate([NotNull] X500DistinguishedName subject) { if (subject == null) { throw new ArgumentNullException(nameof(subject)); } - return Configure(options => options.SigningCredentials.AddDevelopmentCertificate(subject)); + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadWrite); + + // Try to retrieve the development certificate from the specified store. + // If a certificate was found but is not yet or no longer valid, remove it + // from the store before creating and persisting a new encryption certificate. + var certificate = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) + .OfType() + .SingleOrDefault(); + + if (certificate != null && (certificate.NotBefore > DateTime.Now || certificate.NotAfter < DateTime.Now)) + { + store.Remove(certificate); + certificate = null; + } + +#if SUPPORTS_CERTIFICATE_GENERATION + // If no appropriate certificate can be found, generate and persist a new certificate in the specified store. + if (certificate == null) + { + using var algorithm = RSA.Create(keySizeInBits: 2048); + + var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true)); + + certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(2)); + + // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). + // To ensure an exception is not thrown by the property setter, an OS runtime check is used here. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + certificate.FriendlyName = "OpenIddict Server Development Encryption Certificate"; + } + + // Note: CertificateRequest.CreateSelfSigned() doesn't mark the key set associated with the certificate + // as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. + // To work around this issue, the certificate payload is manually exported and imported back + // into a new X509Certificate2 instance specifying the X509KeyStorageFlags.PersistKeySet flag. + var data = certificate.Export(X509ContentType.Pfx, string.Empty); + + try + { + var flags = X509KeyStorageFlags.PersistKeySet; + + // Note: macOS requires marking the certificate private key as exportable. + // If this flag is not set, a CryptographicException is thrown at runtime. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + flags |= X509KeyStorageFlags.Exportable; + } + + certificate = new X509Certificate2(data, string.Empty, flags); + } + + finally + { + Array.Clear(data, 0, data.Length); + } + + store.Add(certificate); + } + + return AddEncryptionCertificate(certificate); +#else + throw new PlatformNotSupportedException("X.509 certificate generation is not supported on this platform."); +#endif } /// - /// Registers a new ephemeral key used to sign the JWT tokens issued by OpenIddict: the key - /// is discarded when the application shuts down and tokens signed using this key are + /// Registers a new ephemeral key used to encrypt the tokens issued by OpenIddict: the key + /// is discarded when the application shuts down and tokens encrypted using this key are /// automatically invalidated. This method should only be used during development. /// On production, using a X.509 certificate stored in the machine store is recommended. /// /// The . - public OpenIddictServerBuilder AddEphemeralSigningKey() - => Configure(options => options.SigningCredentials.AddEphemeralKey()); + public OpenIddictServerBuilder AddEphemeralEncryptionKey() + => AddEphemeralEncryptionKey(SecurityAlgorithms.RsaOAEP); /// - /// Registers a new ephemeral key used to sign the JWT tokens issued by OpenIddict: the key - /// is discarded when the application shuts down and tokens signed using this key are + /// Registers a new ephemeral key used to encrypt the tokens issued by OpenIddict: the key + /// is discarded when the application shuts down and tokens encrypted using this key are /// automatically invalidated. This method should only be used during development. /// On production, using a X.509 certificate stored in the machine store is recommended. /// - /// The algorithm associated with the signing key. + /// The algorithm associated with the encryption key. /// The . - public OpenIddictServerBuilder AddEphemeralSigningKey([NotNull] string algorithm) + public OpenIddictServerBuilder AddEphemeralEncryptionKey([NotNull] string algorithm) { if (string.IsNullOrEmpty(algorithm)) { throw new ArgumentException("The algorithm cannot be null or empty.", nameof(algorithm)); } - return Configure(options => options.SigningCredentials.AddEphemeralKey(algorithm)); - } + switch (algorithm) + { + case SecurityAlgorithms.Aes256KW: + return AddEncryptionCredentials(new EncryptingCredentials(CreateSymmetricSecurityKey(256), + algorithm, SecurityAlgorithms.Aes256CbcHmacSha512)); - /// - /// Registers a used to encrypt the JWT access tokens issued by OpenIddict. - /// - /// The security key. - /// The . - public OpenIddictServerBuilder AddEncryptingKey([NotNull] SecurityKey key) - { - if (key == null) + case SecurityAlgorithms.RsaOAEP: + case SecurityAlgorithms.RsaOaepKeyWrap: + return AddEncryptionCredentials(new EncryptingCredentials(CreateRsaSecurityKey(2048), + algorithm, SecurityAlgorithms.Aes256CbcHmacSha512)); + + default: throw new InvalidOperationException("The specified algorithm is not supported."); + } + + static SymmetricSecurityKey CreateSymmetricSecurityKey(int size) { - throw new ArgumentNullException(nameof(key)); + var data = new byte[size / 8]; + +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + + return new SymmetricSecurityKey(data); } - return Configure(options => options.EncryptingCredentials.AddKey(key)); + static RsaSecurityKey CreateRsaSecurityKey(int size) + { +#if SUPPORTS_DIRECT_KEY_CREATION_WITH_SPECIFIED_SIZE + return new RsaSecurityKey(RSA.Create(size)); +#else + // Note: a 1024-bit key might be returned by RSA.Create() on .NET Desktop/Mono, + // where RSACryptoServiceProvider is still the default implementation and + // where custom implementations can be registered via CryptoConfig. + // To ensure the key size is always acceptable, replace it if necessary. + var algorithm = RSA.Create(); + if (algorithm.KeySize < size) + { + algorithm.KeySize = size; + } + + if (algorithm.KeySize < size && algorithm is RSACryptoServiceProvider) + { + algorithm.Dispose(); + algorithm = new RSACryptoServiceProvider(size); + } + + if (algorithm.KeySize < size) + { + throw new InvalidOperationException("RSA key generation failed."); + } + + return new RsaSecurityKey(algorithm); +#endif + } } /// - /// Registers a that is used to sign the JWT tokens issued by OpenIddict. + /// Registers a that is used to encrypt the tokens issued by OpenIddict. /// - /// The certificate used to sign the security tokens issued by the server. + /// The certificate used to encrypt the security tokens issued by the server. /// The . - public OpenIddictServerBuilder AddSigningCertificate([NotNull] X509Certificate2 certificate) + public OpenIddictServerBuilder AddEncryptionCertificate([NotNull] X509Certificate2 certificate) { if (certificate == null) { throw new ArgumentNullException(nameof(certificate)); } + if (certificate.NotBefore > DateTime.Now) + { + throw new InvalidOperationException("The specified certificate is not yet valid."); + } + + if (certificate.NotAfter < DateTime.Now) + { + throw new InvalidOperationException("The specified certificate is no longer valid."); + } + if (!certificate.HasPrivateKey) { - throw new InvalidOperationException("The certificate doesn't contain the required private key."); + throw new InvalidOperationException("The specified certificate doesn't contain the required private key."); } - return Configure(options => options.SigningCredentials.AddCertificate(certificate)); + return AddEncryptionKey(new X509SecurityKey(certificate)); } /// /// Registers a retrieved from an - /// embedded resource and used to sign the JWT tokens issued by OpenIddict. + /// embedded resource and used to encrypt the tokens issued by OpenIddict. /// /// The assembly containing the certificate. /// The name of the embedded resource. /// The password used to open the certificate. /// The . - public OpenIddictServerBuilder AddSigningCertificate( + public OpenIddictServerBuilder AddEncryptionCertificate( [NotNull] Assembly assembly, [NotNull] string resource, [NotNull] string password) + => AddEncryptionCertificate(assembly, resource, password, X509KeyStorageFlags.MachineKeySet); + + /// + /// Registers a retrieved from an + /// embedded resource and used to encrypt the tokens issued by OpenIddict. + /// + /// The assembly containing the certificate. + /// The name of the embedded resource. + /// The password used to open the certificate. + /// An enumeration of flags indicating how and where to store the private key of the certificate. + /// The . + public OpenIddictServerBuilder AddEncryptionCertificate( + [NotNull] Assembly assembly, [NotNull] string resource, + [NotNull] string password, X509KeyStorageFlags flags) { if (assembly == null) { @@ -249,7 +433,50 @@ public OpenIddictServerBuilder AddSigningCertificate( if (string.IsNullOrEmpty(resource)) { - throw new ArgumentNullException(nameof(resource)); + throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); + } + + if (string.IsNullOrEmpty(password)) + { + throw new ArgumentException("The password cannot be null or empty.", nameof(password)); + } + + using var stream = assembly.GetManifestResourceStream(resource); + if (stream == null) + { + throw new InvalidOperationException("The certificate was not found in the specified assembly."); + } + + return AddEncryptionCertificate(stream, password, flags); + } + + /// + /// Registers a extracted from a + /// stream and used to encrypt the tokens issued by OpenIddict. + /// + /// The stream containing the certificate. + /// The password used to open the certificate. + /// The . + public OpenIddictServerBuilder AddEncryptionCertificate([NotNull] Stream stream, [NotNull] string password) + => AddEncryptionCertificate(stream, password, X509KeyStorageFlags.MachineKeySet); + + /// + /// Registers a extracted from a + /// stream and used to encrypt the tokens issued by OpenIddict. + /// + /// The stream containing the certificate. + /// The password used to open the certificate. + /// + /// An enumeration of flags indicating how and where + /// to store the private key of the certificate. + /// + /// The . + public OpenIddictServerBuilder AddEncryptionCertificate( + [NotNull] Stream stream, [NotNull] string password, X509KeyStorageFlags flags) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); } if (string.IsNullOrEmpty(password)) @@ -257,12 +484,383 @@ public OpenIddictServerBuilder AddSigningCertificate( throw new ArgumentException("The password cannot be null or empty.", nameof(password)); } - return Configure(options => options.SigningCredentials.AddCertificate(assembly, resource, password)); + using var buffer = new MemoryStream(); + stream.CopyTo(buffer); + + return AddEncryptionCertificate(new X509Certificate2(buffer.ToArray(), password, flags)); + } + + /// + /// Registers a retrieved from the X.509 + /// machine store and used to encrypt the tokens issued by OpenIddict. + /// + /// The thumbprint of the certificate used to identify it in the X.509 store. + /// The . + public OpenIddictServerBuilder AddEncryptionCertificate([NotNull] string thumbprint) + { + if (string.IsNullOrEmpty(thumbprint)) + { + throw new ArgumentException("The thumbprint cannot be null or empty.", nameof(thumbprint)); + } + + var certificate = GetCertificate(StoreLocation.CurrentUser, thumbprint) ?? GetCertificate(StoreLocation.LocalMachine, thumbprint); + if (certificate == null) + { + throw new InvalidOperationException("The certificate corresponding to the specified thumbprint was not found."); + } + + return AddEncryptionCertificate(certificate); + + static X509Certificate2 GetCertificate(StoreLocation location, string thumbprint) + { + using var store = new X509Store(StoreName.My, location); + store.Open(OpenFlags.ReadOnly); + + return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) + .OfType() + .SingleOrDefault(); + } + } + + /// + /// Registers a retrieved from the given + /// X.509 store and used to encrypt the tokens issued by OpenIddict. + /// + /// The thumbprint of the certificate used to identify it in the X.509 store. + /// The name of the X.509 store. + /// The location of the X.509 store. + /// The . + public OpenIddictServerBuilder AddEncryptionCertificate( + [NotNull] string thumbprint, StoreName name, StoreLocation location) + { + if (string.IsNullOrEmpty(thumbprint)) + { + throw new ArgumentException("The thumbprint cannot be null or empty.", nameof(thumbprint)); + } + + using var store = new X509Store(name, location); + store.Open(OpenFlags.ReadOnly); + + var certificate = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) + .OfType() + .SingleOrDefault(); + + if (certificate == null) + { + throw new InvalidOperationException("The certificate corresponding to the specified thumbprint was not found."); + } + + return AddEncryptionCertificate(certificate); + } + + /// + /// Registers the used to sign the tokens issued by OpenIddict. + /// Note: using asymmetric keys is recommended on production. + /// + /// The signing credentials. + /// The . + public OpenIddictServerBuilder AddSigningCredentials([NotNull] SigningCredentials credentials) + { + if (credentials == null) + { + throw new ArgumentNullException(nameof(credentials)); + } + + return Configure(options => options.SigningCredentials.Add(credentials)); + } + + /// + /// Registers a used to sign the tokens issued by OpenIddict. + /// Note: using asymmetric keys is recommended on production. + /// + /// The security key. + /// The . + public OpenIddictServerBuilder AddSigningKey([NotNull] SecurityKey key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + // If the signing key is an asymmetric security key, ensure it has a private key. + if (key is AsymmetricSecurityKey asymmetricSecurityKey && + asymmetricSecurityKey.PrivateKeyStatus == PrivateKeyStatus.DoesNotExist) + { + throw new InvalidOperationException("The asymmetric signing key doesn't contain the required private key."); + } + + if (IsAlgorithmSupported(key, SecurityAlgorithms.RsaSha256)) + { + return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.RsaSha256)); + } + + if (IsAlgorithmSupported(key, SecurityAlgorithms.HmacSha256)) + { + return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.HmacSha256)); + } + +#if SUPPORTS_ECDSA + // Note: ECDSA algorithms are bound to specific curves and must be treated separately. + if (IsAlgorithmSupported(key, SecurityAlgorithms.EcdsaSha256)) + { + return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256)); + } + + if (IsAlgorithmSupported(key, SecurityAlgorithms.EcdsaSha384)) + { + return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha384)); + } + + if (IsAlgorithmSupported(key, SecurityAlgorithms.EcdsaSha512)) + { + return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha512)); + } +#else + if (IsAlgorithmSupported(key, SecurityAlgorithms.EcdsaSha256) || + IsAlgorithmSupported(key, SecurityAlgorithms.EcdsaSha384) || + IsAlgorithmSupported(key, SecurityAlgorithms.EcdsaSha512)) + { + throw new PlatformNotSupportedException("ECDSA signing keys are not supported on this platform."); + } +#endif + + throw new InvalidOperationException(new StringBuilder() + .AppendLine("A signature algorithm cannot be automatically inferred from the signing key.") + .Append("Consider using 'options.AddSigningCredentials(SigningCredentials)' instead.") + .ToString()); + + static bool IsAlgorithmSupported(SecurityKey key, string algorithm) => + key.CryptoProviderFactory.IsSupportedAlgorithm(algorithm, key); + } + + /// + /// Registers (and generates if necessary) a user-specific development + /// certificate used to sign the tokens issued by OpenIddict. + /// + /// The . + public OpenIddictServerBuilder AddDevelopmentSigningCertificate() + => AddDevelopmentSigningCertificate(new X500DistinguishedName("CN=OpenIddict Server Signing Certificate")); + + /// + /// Registers (and generates if necessary) a user-specific development + /// certificate used to sign the tokens issued by OpenIddict. + /// + /// The subject name associated with the certificate. + /// The . + public OpenIddictServerBuilder AddDevelopmentSigningCertificate([NotNull] X500DistinguishedName subject) + { + if (subject == null) + { + throw new ArgumentNullException(nameof(subject)); + } + + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadWrite); + + // Try to retrieve the development certificate from the specified store. + // If a certificate was found but is not yet or no longer valid, remove it + // from the store before creating and persisting a new signing certificate. + var certificate = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) + .OfType() + .SingleOrDefault(); + + if (certificate != null && (certificate.NotBefore > DateTime.Now || certificate.NotAfter < DateTime.Now)) + { + store.Remove(certificate); + certificate = null; + } + +#if SUPPORTS_CERTIFICATE_GENERATION + // If no appropriate certificate can be found, generate and persist a new certificate in the specified store. + if (certificate == null) + { + using var algorithm = RSA.Create(keySizeInBits: 2048); + + var request = new CertificateRequest(subject, algorithm, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: true)); + + certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(2)); + + // Note: setting the friendly name is not supported on Unix machines (including Linux and macOS). + // To ensure an exception is not thrown by the property setter, an OS runtime check is used here. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + certificate.FriendlyName = "OpenIddict Server Development Signing Certificate"; + } + + // Note: CertificateRequest.CreateSelfSigned() doesn't mark the key set associated with the certificate + // as "persisted", which eventually prevents X509Store.Add() from correctly storing the private key. + // To work around this issue, the certificate payload is manually exported and imported back + // into a new X509Certificate2 instance specifying the X509KeyStorageFlags.PersistKeySet flag. + var data = certificate.Export(X509ContentType.Pfx, string.Empty); + + try + { + var flags = X509KeyStorageFlags.PersistKeySet; + + // Note: macOS requires marking the certificate private key as exportable. + // If this flag is not set, a CryptographicException is thrown at runtime. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + flags |= X509KeyStorageFlags.Exportable; + } + + certificate = new X509Certificate2(data, string.Empty, flags); + } + + finally + { + Array.Clear(data, 0, data.Length); + } + + store.Add(certificate); + } + + return AddSigningCertificate(certificate); +#else + throw new PlatformNotSupportedException("X.509 certificate generation is not supported on this platform."); +#endif + } + + /// + /// Registers a new ephemeral key used to sign the tokens issued by OpenIddict: the key + /// is discarded when the application shuts down and tokens signed using this key are + /// automatically invalidated. This method should only be used during development. + /// On production, using a X.509 certificate stored in the machine store is recommended. + /// + /// The . + public OpenIddictServerBuilder AddEphemeralSigningKey() + => AddEphemeralSigningKey(SecurityAlgorithms.RsaSha256); + + /// + /// Registers a new ephemeral key used to sign the tokens issued by OpenIddict: the key + /// is discarded when the application shuts down and tokens signed using this key are + /// automatically invalidated. This method should only be used during development. + /// On production, using a X.509 certificate stored in the machine store is recommended. + /// + /// The algorithm associated with the signing key. + /// The . + public OpenIddictServerBuilder AddEphemeralSigningKey([NotNull] string algorithm) + { + if (string.IsNullOrEmpty(algorithm)) + { + throw new ArgumentException("The algorithm cannot be null or empty.", nameof(algorithm)); + } + + switch (algorithm) + { + case SecurityAlgorithms.RsaSha256: + case SecurityAlgorithms.RsaSha384: + case SecurityAlgorithms.RsaSha512: + case SecurityAlgorithms.RsaSha256Signature: + case SecurityAlgorithms.RsaSha384Signature: + case SecurityAlgorithms.RsaSha512Signature: + return AddSigningCredentials(new SigningCredentials(CreateRsaSecurityKey(2048), algorithm)); + +#if SUPPORTS_ECDSA + case SecurityAlgorithms.EcdsaSha256: + case SecurityAlgorithms.EcdsaSha256Signature: + return AddSigningCredentials(new SigningCredentials(new ECDsaSecurityKey( + ECDsa.Create(ECCurve.NamedCurves.nistP256)), algorithm)); + + case SecurityAlgorithms.EcdsaSha384: + case SecurityAlgorithms.EcdsaSha384Signature: + return AddSigningCredentials(new SigningCredentials(new ECDsaSecurityKey( + ECDsa.Create(ECCurve.NamedCurves.nistP384)), algorithm)); + + case SecurityAlgorithms.EcdsaSha512: + case SecurityAlgorithms.EcdsaSha512Signature: + return AddSigningCredentials(new SigningCredentials(new ECDsaSecurityKey( + ECDsa.Create(ECCurve.NamedCurves.nistP521)), algorithm)); +#else + case SecurityAlgorithms.EcdsaSha256: + case SecurityAlgorithms.EcdsaSha384: + case SecurityAlgorithms.EcdsaSha512: + case SecurityAlgorithms.EcdsaSha256Signature: + case SecurityAlgorithms.EcdsaSha384Signature: + case SecurityAlgorithms.EcdsaSha512Signature: + throw new PlatformNotSupportedException("ECDSA signing keys are not supported on this platform."); +#endif + + default: throw new InvalidOperationException("The specified algorithm is not supported."); + } + + static RsaSecurityKey CreateRsaSecurityKey(int size) + { +#if SUPPORTS_DIRECT_KEY_CREATION_WITH_SPECIFIED_SIZE + return new RsaSecurityKey(RSA.Create(size)); +#else + // Note: a 1024-bit key might be returned by RSA.Create() on .NET Desktop/Mono, + // where RSACryptoServiceProvider is still the default implementation and + // where custom implementations can be registered via CryptoConfig. + // To ensure the key size is always acceptable, replace it if necessary. + var algorithm = RSA.Create(); + if (algorithm.KeySize < size) + { + algorithm.KeySize = size; + } + + if (algorithm.KeySize < size && algorithm is RSACryptoServiceProvider) + { + algorithm.Dispose(); + algorithm = new RSACryptoServiceProvider(size); + } + + if (algorithm.KeySize < size) + { + throw new InvalidOperationException("RSA key generation failed."); + } + + return new RsaSecurityKey(algorithm); +#endif + } + } + + /// + /// Registers a that is used to sign the tokens issued by OpenIddict. + /// + /// The certificate used to sign the security tokens issued by the server. + /// The . + public OpenIddictServerBuilder AddSigningCertificate([NotNull] X509Certificate2 certificate) + { + if (certificate == null) + { + throw new ArgumentNullException(nameof(certificate)); + } + + if (certificate.NotBefore > DateTime.Now) + { + throw new InvalidOperationException("The specified certificate is not yet valid."); + } + + if (certificate.NotAfter < DateTime.Now) + { + throw new InvalidOperationException("The specified certificate is no longer valid."); + } + + if (!certificate.HasPrivateKey) + { + throw new InvalidOperationException("The specified certificate doesn't contain the required private key."); + } + + return AddSigningKey(new X509SecurityKey(certificate)); } /// /// Registers a retrieved from an - /// embedded resource and used to sign the JWT tokens issued by OpenIddict. + /// embedded resource and used to sign the tokens issued by OpenIddict. + /// + /// The assembly containing the certificate. + /// The name of the embedded resource. + /// The password used to open the certificate. + /// The . + public OpenIddictServerBuilder AddSigningCertificate( + [NotNull] Assembly assembly, [NotNull] string resource, [NotNull] string password) + => AddSigningCertificate(assembly, resource, password, X509KeyStorageFlags.MachineKeySet); + + /// + /// Registers a retrieved from an + /// embedded resource and used to sign the tokens issued by OpenIddict. /// /// The assembly containing the certificate. /// The name of the embedded resource. @@ -280,7 +878,7 @@ public OpenIddictServerBuilder AddSigningCertificate( if (string.IsNullOrEmpty(resource)) { - throw new ArgumentNullException(nameof(resource)); + throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); } if (string.IsNullOrEmpty(password)) @@ -288,34 +886,28 @@ public OpenIddictServerBuilder AddSigningCertificate( throw new ArgumentException("The password cannot be null or empty.", nameof(password)); } - return Configure(options => options.SigningCredentials.AddCertificate(assembly, resource, password, flags)); + using var stream = assembly.GetManifestResourceStream(resource); + if (stream == null) + { + throw new InvalidOperationException("The certificate was not found in the specified assembly."); + } + + return AddSigningCertificate(stream, password, flags); } /// /// Registers a extracted from a - /// stream and used to sign the JWT tokens issued by OpenIddict. + /// stream and used to sign the tokens issued by OpenIddict. /// /// The stream containing the certificate. /// The password used to open the certificate. /// The . public OpenIddictServerBuilder AddSigningCertificate([NotNull] Stream stream, [NotNull] string password) - { - if (stream == null) - { - throw new ArgumentNullException(nameof(stream)); - } - - if (string.IsNullOrEmpty(password)) - { - throw new ArgumentException("The password cannot be null or empty.", nameof(password)); - } - - return Configure(options => options.SigningCredentials.AddCertificate(stream, password)); - } + => AddSigningCertificate(stream, password, X509KeyStorageFlags.MachineKeySet); /// /// Registers a extracted from a - /// stream and used to sign the JWT tokens issued by OpenIddict. + /// stream and used to sign the tokens issued by OpenIddict. /// /// The stream containing the certificate. /// The password used to open the certificate. @@ -337,12 +929,15 @@ public OpenIddictServerBuilder AddSigningCertificate( throw new ArgumentException("The password cannot be null or empty.", nameof(password)); } - return Configure(options => options.SigningCredentials.AddCertificate(stream, password, flags)); + using var buffer = new MemoryStream(); + stream.CopyTo(buffer); + + return AddSigningCertificate(new X509Certificate2(buffer.ToArray(), password, flags)); } /// /// Registers a retrieved from the X.509 - /// machine store and used to sign the JWT tokens issued by OpenIddict. + /// machine store and used to sign the tokens issued by OpenIddict. /// /// The thumbprint of the certificate used to identify it in the X.509 store. /// The . @@ -350,15 +945,31 @@ public OpenIddictServerBuilder AddSigningCertificate([NotNull] string thumbprint { if (string.IsNullOrEmpty(thumbprint)) { - throw new ArgumentNullException(nameof(thumbprint)); + throw new ArgumentException("The thumbprint cannot be null or empty.", nameof(thumbprint)); + } + + var certificate = GetCertificate(StoreLocation.CurrentUser, thumbprint) ?? GetCertificate(StoreLocation.LocalMachine, thumbprint); + if (certificate == null) + { + throw new InvalidOperationException("The certificate corresponding to the specified thumbprint was not found."); } - return Configure(options => options.SigningCredentials.AddCertificate(thumbprint)); + return AddSigningCertificate(certificate); + + static X509Certificate2 GetCertificate(StoreLocation location, string thumbprint) + { + using var store = new X509Store(StoreName.My, location); + store.Open(OpenFlags.ReadOnly); + + return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) + .OfType() + .SingleOrDefault(); + } } /// /// Registers a retrieved from the given - /// X.509 store and used to sign the JWT tokens issued by OpenIddict. + /// X.509 store and used to sign the tokens issued by OpenIddict. /// /// The thumbprint of the certificate used to identify it in the X.509 store. /// The name of the X.509 store. @@ -369,31 +980,27 @@ public OpenIddictServerBuilder AddSigningCertificate( { if (string.IsNullOrEmpty(thumbprint)) { - throw new ArgumentNullException(nameof(thumbprint)); + throw new ArgumentException("The thumbprint cannot be null or empty.", nameof(thumbprint)); } - return Configure(options => options.SigningCredentials.AddCertificate(thumbprint, name, location)); - } + using var store = new X509Store(name, location); + store.Open(OpenFlags.ReadOnly); - /// - /// Registers a used to sign the JWT tokens issued by OpenIddict. - /// Note: using asymmetric keys is recommended on production. - /// - /// The security key. - /// The . - public OpenIddictServerBuilder AddSigningKey([NotNull] SecurityKey key) - { - if (key == null) + var certificate = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false) + .OfType() + .SingleOrDefault(); + + if (certificate == null) { - throw new ArgumentNullException(nameof(key)); + throw new InvalidOperationException("The certificate corresponding to the specified thumbprint was not found."); } - return Configure(options => options.SigningCredentials.AddKey(key)); + return AddSigningCertificate(certificate); } /// /// Enables authorization code flow support. For more information - /// about this specific OAuth2/OpenID Connect flow, visit + /// about this specific OAuth 2.0/OpenID Connect flow, visit /// https://tools.ietf.org/html/rfc6749#section-4.1 and /// http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth. /// @@ -403,7 +1010,7 @@ public OpenIddictServerBuilder AllowAuthorizationCodeFlow() /// /// Enables client credentials flow support. For more information about this - /// specific OAuth2 flow, visit https://tools.ietf.org/html/rfc6749#section-4.4. + /// specific OAuth 2.0 flow, visit https://tools.ietf.org/html/rfc6749#section-4.4. /// /// The . public OpenIddictServerBuilder AllowClientCredentialsFlow() @@ -426,7 +1033,7 @@ public OpenIddictServerBuilder AllowCustomFlow([NotNull] string type) /// /// Enables implicit flow support. For more information - /// about this specific OAuth2/OpenID Connect flow, visit + /// about this specific OAuth 2.0/OpenID Connect flow, visit /// https://tools.ietf.org/html/rfc6749#section-4.2 and /// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth. /// @@ -436,7 +1043,7 @@ public OpenIddictServerBuilder AllowImplicitFlow() /// /// Enables password flow support. For more information about this specific - /// OAuth2 flow, visit https://tools.ietf.org/html/rfc6749#section-4.3. + /// OAuth 2.0 flow, visit https://tools.ietf.org/html/rfc6749#section-4.3. /// /// The . public OpenIddictServerBuilder AllowPasswordFlow() @@ -444,170 +1051,474 @@ public OpenIddictServerBuilder AllowPasswordFlow() /// /// Enables refresh token flow support. For more information about this - /// specific OAuth2 flow, visit https://tools.ietf.org/html/rfc6749#section-6. + /// specific OAuth 2.0 flow, visit https://tools.ietf.org/html/rfc6749#section-6. /// /// The . public OpenIddictServerBuilder AllowRefreshTokenFlow() => Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.RefreshToken)); /// - /// Disables authorization storage so that ad-hoc authorizations are - /// not created when an authorization code or refresh token is issued - /// and can't be revoked to prevent associated tokens from being used. - /// Using this option is generally NOT recommended. + /// Sets the relative or absolute URLs associated to the authorization endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder DisableAuthorizationStorage() - => Configure(options => options.DisableAuthorizationStorage = true); + public OpenIddictServerBuilder SetAuthorizationEndpointUris([NotNull] params string[] addresses) + { + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + return SetAuthorizationEndpointUris(addresses.Select(address => new Uri(address, UriKind.RelativeOrAbsolute)).ToArray()); + } + + /// + /// Sets the relative or absolute URLs associated to the authorization endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. + /// + /// The addresses associated to the endpoint. + /// The . + public OpenIddictServerBuilder SetAuthorizationEndpointUris([NotNull] params Uri[] addresses) + { + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + if (addresses.Any(address => !address.IsWellFormedOriginalString())) + { + throw new ArgumentException("One of the specified addresses is not valid.", nameof(addresses)); + } + + if (addresses.Any(address => !address.IsAbsoluteUri && !address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException("Relative URLs must start with a '/'.", nameof(addresses)); + } + + return Configure(options => + { + options.AuthorizationEndpointUris.Clear(); + + foreach (var address in addresses) + { + options.AuthorizationEndpointUris.Add(address); + } + }); + } /// - /// Disables the configuration endpoint. + /// Sets the relative or absolute URLs associated to the configuration endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder DisableConfigurationEndpoint() - => Configure(options => options.ConfigurationEndpointPath = PathString.Empty); + public OpenIddictServerBuilder SetConfigurationEndpointUris([NotNull] params string[] addresses) + { + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + return SetConfigurationEndpointUris(addresses.Select(address => new Uri(address, UriKind.RelativeOrAbsolute)).ToArray()); + } /// - /// Disables the cryptography endpoint. + /// Sets the relative or absolute URLs associated to the configuration endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder DisableCryptographyEndpoint() - => Configure(options => options.CryptographyEndpointPath = PathString.Empty); + public OpenIddictServerBuilder SetConfigurationEndpointUris([NotNull] params Uri[] addresses) + { + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + if (addresses.Any(address => !address.IsWellFormedOriginalString())) + { + throw new ArgumentException("One of the specified addresses is not valid.", nameof(addresses)); + } + + if (addresses.Any(address => !address.IsAbsoluteUri && !address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException("Relative URLs must start with a '/'.", nameof(addresses)); + } + + return Configure(options => + { + options.ConfigurationEndpointUris.Clear(); + + foreach (var address in addresses) + { + options.ConfigurationEndpointUris.Add(address); + } + }); + } /// - /// Disables the HTTPS requirement during development. + /// Sets the relative or absolute URLs associated to the cryptography endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder DisableHttpsRequirement() - => Configure(options => options.AllowInsecureHttp = true); + public OpenIddictServerBuilder SetCryptographyEndpointUris([NotNull] params string[] addresses) + { + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + return SetCryptographyEndpointUris(addresses.Select(address => new Uri(address, UriKind.RelativeOrAbsolute)).ToArray()); + } /// - /// Disables sliding expiration. When using this option, refresh tokens - /// are issued with a fixed expiration date: when they expire, a complete - /// authorization flow must be started to retrieve a new refresh token. + /// Sets the relative or absolute URLs associated to the cryptography endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder DisableSlidingExpiration() - => Configure(options => options.UseSlidingExpiration = false); + public OpenIddictServerBuilder SetCryptographyEndpointUris([NotNull] params Uri[] addresses) + { + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + if (addresses.Any(address => !address.IsWellFormedOriginalString())) + { + throw new ArgumentException("One of the specified addresses is not valid.", nameof(addresses)); + } + + if (addresses.Any(address => !address.IsAbsoluteUri && !address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException("Relative URLs must start with a '/'.", nameof(addresses)); + } + + return Configure(options => + { + options.CryptographyEndpointUris.Clear(); + + foreach (var address in addresses) + { + options.CryptographyEndpointUris.Add(address); + } + }); + } /// - /// Disables token storage, so that authorization code and - /// refresh tokens are never stored and cannot be revoked. - /// Using this option is generally NOT recommended. + /// Sets the relative or absolute URLs associated to the introspection endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder DisableTokenStorage() - => Configure(options => options.DisableTokenStorage = true); + public OpenIddictServerBuilder SetIntrospectionEndpointUris([NotNull] params string[] addresses) + { + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + return SetIntrospectionEndpointUris(addresses.Select(address => new Uri(address, UriKind.RelativeOrAbsolute)).ToArray()); + } + + /// + /// Sets the relative or absolute URLs associated to the introspection endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. + /// + /// The addresses associated to the endpoint. + /// The . + public OpenIddictServerBuilder SetIntrospectionEndpointUris([NotNull] params Uri[] addresses) + { + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + if (addresses.Any(address => !address.IsWellFormedOriginalString())) + { + throw new ArgumentException("One of the specified addresses is not valid.", nameof(addresses)); + } + + if (addresses.Any(address => !address.IsAbsoluteUri && !address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException("Relative URLs must start with a '/'.", nameof(addresses)); + } + + return Configure(options => + { + options.IntrospectionEndpointUris.Clear(); + + foreach (var address in addresses) + { + options.IntrospectionEndpointUris.Add(address); + } + }); + } /// - /// Enables the authorization endpoint. + /// Sets the relative or absolute URLs associated to the logout endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// - /// The relative path of the authorization endpoint. + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder EnableAuthorizationEndpoint(PathString path) + public OpenIddictServerBuilder SetLogoutEndpointUris([NotNull] params string[] addresses) { - if (!path.HasValue) + if (addresses == null) { - throw new ArgumentException("The path cannot be empty.", nameof(path)); + throw new ArgumentNullException(nameof(addresses)); } - return Configure(options => options.AuthorizationEndpointPath = path); + return SetLogoutEndpointUris(addresses.Select(address => new Uri(address, UriKind.RelativeOrAbsolute)).ToArray()); } /// - /// Enables the introspection endpoint. + /// Sets the relative or absolute URLs associated to the logout endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// - /// The relative path of the logout endpoint. + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder EnableIntrospectionEndpoint(PathString path) + public OpenIddictServerBuilder SetLogoutEndpointUris([NotNull] params Uri[] addresses) { - if (!path.HasValue) + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + if (addresses.Any(address => !address.IsWellFormedOriginalString())) { - throw new ArgumentException("The path cannot be empty.", nameof(path)); + throw new ArgumentException("One of the specified addresses is not valid.", nameof(addresses)); } - return Configure(options => options.IntrospectionEndpointPath = path); + if (addresses.Any(address => !address.IsAbsoluteUri && !address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException("Relative URLs must start with a '/'.", nameof(addresses)); + } + + return Configure(options => + { + options.LogoutEndpointUris.Clear(); + + foreach (var address in addresses) + { + options.LogoutEndpointUris.Add(address); + } + }); } /// - /// Enables the logout endpoint. + /// Sets the relative or absolute URLs associated to the revocation endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// - /// The relative path of the logout endpoint. + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder EnableLogoutEndpoint(PathString path) + public OpenIddictServerBuilder SetRevocationEndpointUris([NotNull] params string[] addresses) { - if (!path.HasValue) + if (addresses == null) { - throw new ArgumentException("The path cannot be empty.", nameof(path)); + throw new ArgumentNullException(nameof(addresses)); } - return Configure(options => options.LogoutEndpointPath = path); + return SetRevocationEndpointUris(addresses.Select(address => new Uri(address, UriKind.RelativeOrAbsolute)).ToArray()); } /// - /// Enables request caching, so that both authorization and logout requests - /// are automatically stored in the distributed cache, which allows flowing - /// large payloads across requests. Enabling this option is recommended - /// when using external authentication providers or when large GET or POST - /// OpenID Connect authorization requests support is required. + /// Sets the relative or absolute URLs associated to the revocation endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder EnableRequestCaching() - => Configure(options => options.EnableRequestCaching = true); + public OpenIddictServerBuilder SetRevocationEndpointUris([NotNull] params Uri[] addresses) + { + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + if (addresses.Any(address => !address.IsWellFormedOriginalString())) + { + throw new ArgumentException("One of the specified addresses is not valid.", nameof(addresses)); + } + + if (addresses.Any(address => !address.IsAbsoluteUri && !address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException("Relative URLs must start with a '/'.", nameof(addresses)); + } + + return Configure(options => + { + options.RevocationEndpointUris.Clear(); + + foreach (var address in addresses) + { + options.RevocationEndpointUris.Add(address); + } + }); + } /// - /// Enables the revocation endpoint. + /// Sets the relative or absolute URLs associated to the token endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// - /// The relative path of the revocation endpoint. + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder EnableRevocationEndpoint( PathString path) + public OpenIddictServerBuilder SetTokenEndpointUris([NotNull] params string[] addresses) { - if (!path.HasValue) + if (addresses == null) { - throw new ArgumentException("The path cannot be empty.", nameof(path)); + throw new ArgumentNullException(nameof(addresses)); } - return Configure(options => options.RevocationEndpointPath = path); + return SetTokenEndpointUris(addresses.Select(address => new Uri(address, UriKind.RelativeOrAbsolute)).ToArray()); } /// - /// Allows processing authorization and token requests that specify scopes that have not - /// been registered using or the scope manager. + /// Sets the relative or absolute URLs associated to the token endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder DisableScopeValidation() - => Configure(options => options.DisableScopeValidation = true); + public OpenIddictServerBuilder SetTokenEndpointUris([NotNull] params Uri[] addresses) + { + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + if (addresses.Any(address => !address.IsWellFormedOriginalString())) + { + throw new ArgumentException("One of the specified addresses is not valid.", nameof(addresses)); + } + + if (addresses.Any(address => !address.IsAbsoluteUri && !address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException("Relative URLs must start with a '/'.", nameof(addresses)); + } + + return Configure(options => + { + options.TokenEndpointUris.Clear(); + + foreach (var address in addresses) + { + options.TokenEndpointUris.Add(address); + } + }); + } /// - /// Enables the token endpoint. + /// Sets the relative or absolute URLs associated to the userinfo endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// - /// The relative path of the token endpoint. + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder EnableTokenEndpoint(PathString path) + public OpenIddictServerBuilder SetUserinfoEndpointUris([NotNull] params string[] addresses) { - if (!path.HasValue) + if (addresses == null) { - throw new ArgumentException("The path cannot be empty.", nameof(path)); + throw new ArgumentNullException(nameof(addresses)); } - return Configure(options => options.TokenEndpointPath = path); + return SetUserinfoEndpointUris(addresses.Select(address => new Uri(address, UriKind.RelativeOrAbsolute)).ToArray()); } /// - /// Enables the userinfo endpoint. + /// Sets the relative or absolute URLs associated to the userinfo endpoint. + /// If an empty array is specified, the endpoint will be considered disabled. + /// Note: only the first address will be returned as part of the discovery document. /// - /// The relative path of the userinfo endpoint. + /// The addresses associated to the endpoint. /// The . - public OpenIddictServerBuilder EnableUserinfoEndpoint(PathString path) + public OpenIddictServerBuilder SetUserinfoEndpointUris([NotNull] params Uri[] addresses) { - if (!path.HasValue) + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + if (addresses.Any(address => !address.IsWellFormedOriginalString())) + { + throw new ArgumentException("One of the specified addresses is not valid.", nameof(addresses)); + } + + if (addresses.Any(address => !address.IsAbsoluteUri && !address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase))) { - throw new ArgumentException("The path cannot be empty.", nameof(path)); + throw new ArgumentException("Relative URLs must start with a '/'.", nameof(addresses)); } - return Configure(options => options.UserinfoEndpointPath = path); + return Configure(options => + { + options.UserinfoEndpointUris.Clear(); + + foreach (var address in addresses) + { + options.UserinfoEndpointUris.Add(address); + } + }); } + /// + /// Disables authorization storage so that ad-hoc authorizations are + /// not created when an authorization code or refresh token is issued + /// and can't be revoked to prevent associated tokens from being used. + /// Using this option is generally NOT recommended. + /// + /// The . + public OpenIddictServerBuilder DisableAuthorizationStorage() + => Configure(options => options.DisableAuthorizationStorage = true); + + /// + /// Disables sliding expiration. When using this option, refresh tokens + /// are issued with a fixed expiration date: when they expire, a complete + /// authorization flow must be started to retrieve a new refresh token. + /// + /// The . + public OpenIddictServerBuilder DisableSlidingExpiration() + => Configure(options => options.UseSlidingExpiration = false); + + /// + /// Disables token storage, so that authorization code and + /// refresh tokens are never stored and cannot be revoked. + /// Using this option is generally NOT recommended. + /// + /// The . + public OpenIddictServerBuilder DisableTokenStorage() + => Configure(options => options.DisableTokenStorage = true); + + /// + /// Allows processing authorization and token requests that specify scopes that have not + /// been registered using or the scope manager. + /// + /// The . + public OpenIddictServerBuilder DisableScopeValidation() + => Configure(options => options.DisableScopeValidation = true); + + /// + /// Enables the degraded mode. When the degraded mode is enabled, all the security checks that + /// depend on the OpenIddict core managers are disabled. This option MUST be enabled with extreme + /// caution and custom handlers MUST be registered to properly validate OpenID Connect requests. + /// + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictServerBuilder EnableDegradedMode() + => Configure(options => options.EnableDegradedMode = true); + /// /// Disables endpoint permissions enforcement. Calling this method is NOT recommended, /// unless all the clients are first-party applications you own, control and fully trust. @@ -674,16 +1585,6 @@ public OpenIddictServerBuilder RegisterScopes([NotNull] params string[] scopes) return Configure(options => options.Scopes.UnionWith(scopes)); } - /// - /// Configures OpenIddict to force client applications to use Proof Key for Code Exchange - /// (PKCE) when requesting an authorization code (e.g when using the code or hybrid flows). - /// When enforced, authorization requests that lack the code_challenge or - /// code_challenge_method parameters will be automatically rejected by OpenIddict. - /// - /// The . - public OpenIddictServerBuilder RequireProofKeyForCodeExchange() - => Configure(options => options.RequireProofKeyForCodeExchange = true); - /// /// Sets the access token lifetime, after which client applications must retrieve /// a new access token by making a grant_type=refresh_token token request @@ -729,23 +1630,6 @@ public OpenIddictServerBuilder SetIdentityTokenLifetime([CanBeNull] TimeSpan? li public OpenIddictServerBuilder SetRefreshTokenLifetime([CanBeNull] TimeSpan? lifetime) => Configure(options => options.RefreshTokenLifetime = lifetime); - /// - /// Sets the caching policy used to determine how long the authorization and - /// end session requests should be cached by the distributed cache implementation. - /// Note: the specified policy is only used when request caching is explicitly enabled. - /// - /// The request caching policy. - /// The . - public OpenIddictServerBuilder SetRequestCachingPolicy([NotNull] DistributedCacheEntryOptions policy) - { - if (policy == null) - { - throw new ArgumentNullException(nameof(policy)); - } - - return Configure(options => options.RequestCachingPolicy = policy); - } - /// /// Sets the issuer address, which is used as the base address /// for the endpoint URIs returned from the discovery endpoint. @@ -762,35 +1646,6 @@ public OpenIddictServerBuilder SetIssuer([NotNull] Uri address) return Configure(options => options.Issuer = address); } - /// - /// Configures OpenIddict to use a specific data protection provider - /// instead of relying on the default instance provided by the DI container. - /// - /// The data protection provider used to create token protectors. - /// The . - public OpenIddictServerBuilder UseDataProtectionProvider([NotNull] IDataProtectionProvider provider) - { - if (provider == null) - { - throw new ArgumentNullException(nameof(provider)); - } - - return Configure(options => options.DataProtectionProvider = provider); - } - - /// - /// Sets JSON Web Token (JWT) as the default token format for access tokens. - /// Note: JWT tokens cannot be used with the OpenIddict validation handler. - /// To validate JWT tokens, use the JWT handler shipping with ASP.NET Core. - /// - /// The . - public OpenIddictServerBuilder UseJsonWebTokens() - => Configure(options => options.AccessTokenHandler = new JwtSecurityTokenHandler - { - InboundClaimTypeMap = new Dictionary(), - OutboundClaimTypeMap = new Dictionary() - }); - /// /// Configures OpenIddict to use reference tokens, so that authorization codes, /// access tokens and refresh tokens are stored as ciphertext in the database diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs index fd4e3b53b..98407a561 100644 --- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs +++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs @@ -5,220 +5,62 @@ */ using System; +using System.Diagnostics; using System.Linq; using System.Text; -using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; -using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlerFilters; namespace OpenIddict.Server { /// /// Contains the methods required to ensure that the OpenIddict server configuration is valid. /// - public class OpenIddictServerConfiguration : IConfigureOptions, - IPostConfigureOptions, - IPostConfigureOptions + public class OpenIddictServerConfiguration : IPostConfigureOptions { - private readonly IDistributedCache _cache; - private readonly IDataProtectionProvider _dataProtectionProvider; - - /// - /// Creates a new instance of the class. - /// Note: this API supports the OpenIddict infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future minor releases. - /// - public OpenIddictServerConfiguration( - [NotNull] IDistributedCache cache, - [NotNull] IDataProtectionProvider dataProtectionProvider) - { - _cache = cache; - _dataProtectionProvider = dataProtectionProvider; - } - - /// - /// Registers the OpenIddict server handler in the global authentication options. - /// - /// The options instance to initialize. - public void Configure([NotNull] AuthenticationOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - // If a handler was already registered and the type doesn't correspond to the OpenIddict handler, throw an exception. - if (options.SchemeMap.TryGetValue(OpenIddictServerDefaults.AuthenticationScheme, out var builder) && - builder.HandlerType != typeof(OpenIddictServerHandler)) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The OpenIddict server handler cannot be registered as an authentication scheme.") - .AppendLine("This may indicate that an instance of the OpenID Connect server was registered.") - .Append("Make sure that 'services.AddAuthentication().AddOpenIdConnectServer()' is not used.") - .ToString()); - } - - options.AddScheme(OpenIddictServerDefaults.AuthenticationScheme, displayName: null); - } - /// - /// Ensures that the authentication configuration is in a consistent and valid state. - /// - /// The authentication scheme associated with the handler instance. - /// The options instance to initialize. - public void PostConfigure([CanBeNull] string name, [NotNull] AuthenticationOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - bool TryValidate(string scheme) - { - // If the scheme was not set or if it cannot be found in the map, return true. - if (string.IsNullOrEmpty(scheme) || !options.SchemeMap.TryGetValue(scheme, out var builder)) - { - return true; - } - - return builder.HandlerType != typeof(OpenIddictServerHandler); - } - - if (!TryValidate(options.DefaultAuthenticateScheme) || !TryValidate(options.DefaultChallengeScheme) || - !TryValidate(options.DefaultForbidScheme) || !TryValidate(options.DefaultScheme) || - !TryValidate(options.DefaultSignInScheme) || !TryValidate(options.DefaultSignOutScheme)) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The OpenIddict server handler cannot be used as the default scheme handler.") - .Append("Make sure that neither DefaultAuthenticateScheme, DefaultChallengeScheme, ") - .Append("DefaultForbidScheme, DefaultSignInScheme, DefaultSignOutScheme nor DefaultScheme ") - .Append("point to an instance of the OpenIddict server handler.") - .ToString()); - } - } - - /// - /// Populates the default OpenID Connect server options and ensures + /// Populates the default OpenIddict server options and ensures /// that the configuration is in a consistent and valid state. /// /// The authentication scheme associated with the handler instance. /// The options instance to initialize. - public void PostConfigure([NotNull] string name, [NotNull] OpenIddictServerOptions options) + public void PostConfigure([CanBeNull] string name, [NotNull] OpenIddictServerOptions options) { if (options == null) { throw new ArgumentNullException(nameof(options)); } - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException("The options instance name cannot be null or empty.", nameof(name)); - } - - if (options.RandomNumberGenerator == null) - { - throw new InvalidOperationException("A random number generator must be registered."); - } - - if (options.ProviderType == null || options.ProviderType != typeof(OpenIddictServerProvider)) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("OpenIddict can only be used with its built-in server provider.") - .AppendLine("This error may indicate that 'OpenIddictServerOptions.ProviderType' was manually set.") - .Append("To execute custom request handling logic, consider registering an event handler using ") - .Append("the generic 'services.AddOpenIddict().AddServer().AddEventHandler()' method.") - .ToString()); - } - - // When no distributed cache has been registered in the options, - // try to resolve it from the dependency injection container. - if (options.Cache == null) - { - options.Cache = _cache; - } - - // If OpenIddict was configured to use reference tokens, replace the default access tokens/ - // authorization codes/refresh tokens formats using a specific data protector to ensure - // that encrypted tokens stored in the database cannot be treated as valid tokens if the - // reference tokens option is later turned off by the developer. - if (options.UseReferenceTokens) - { - // Note: a default data protection provider is always registered by - // the OpenID Connect server handler when none is explicitly set but - // this initializer is registered to be invoked before ASOS' initializer. - // To ensure the provider property is never null, it's manually set here. - if (options.DataProtectionProvider == null) - { - options.DataProtectionProvider = _dataProtectionProvider; - } - - if (options.AccessTokenFormat == null) - { - var protector = options.DataProtectionProvider.CreateProtector( - nameof(OpenIdConnectServerHandler), - nameof(options.AccessTokenFormat), - nameof(options.UseReferenceTokens), name); - - options.AccessTokenFormat = new TicketDataFormat(protector); - } - - if (options.AuthorizationCodeFormat == null) - { - var protector = options.DataProtectionProvider.CreateProtector( - nameof(OpenIdConnectServerHandler), - nameof(options.AuthorizationCodeFormat), - nameof(options.UseReferenceTokens), name); - - options.AuthorizationCodeFormat = new TicketDataFormat(protector); - } - - if (options.RefreshTokenFormat == null) - { - var protector = options.DataProtectionProvider.CreateProtector( - nameof(OpenIdConnectServerHandler), - nameof(options.RefreshTokenFormat), - nameof(options.UseReferenceTokens), name); - - options.RefreshTokenFormat = new TicketDataFormat(protector); - } - } - // Ensure at least one flow has been enabled. if (options.GrantTypes.Count == 0) { - throw new InvalidOperationException("At least one OAuth2/OpenID Connect flow must be enabled."); + throw new InvalidOperationException("At least one OAuth 2.0/OpenID Connect flow must be enabled."); } // Ensure the authorization endpoint has been enabled when // the authorization code or implicit grants are supported. - if (!options.AuthorizationEndpointPath.HasValue && (options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.AuthorizationCode) || - options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.Implicit))) + if (options.AuthorizationEndpointUris.Count == 0 && (options.GrantTypes.Contains(GrantTypes.AuthorizationCode) || + options.GrantTypes.Contains(GrantTypes.Implicit))) { throw new InvalidOperationException("The authorization endpoint must be enabled to use the authorization code and implicit flows."); } // Ensure the token endpoint has been enabled when the authorization code, // client credentials, password or refresh token grants are supported. - if (!options.TokenEndpointPath.HasValue && (options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.AuthorizationCode) || - options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.ClientCredentials) || - options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.Password) || - options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.RefreshToken))) + if (options.TokenEndpointUris.Count == 0 && (options.GrantTypes.Contains(GrantTypes.AuthorizationCode) || + options.GrantTypes.Contains(GrantTypes.ClientCredentials) || + options.GrantTypes.Contains(GrantTypes.Password) || + options.GrantTypes.Contains(GrantTypes.RefreshToken))) { throw new InvalidOperationException( "The token endpoint must be enabled to use the authorization code, client credentials, password and refresh token flows."); } - if (options.EnableRequestCaching && options.RequestCachingPolicy == null) - { - throw new InvalidOperationException("A caching policy must be specified when enabling request caching."); - } - - if (options.RevocationEndpointPath.HasValue && options.DisableTokenStorage) + if (options.DisableTokenStorage && options.RevocationEndpointUris.Count != 0) { throw new InvalidOperationException("The revocation endpoint cannot be enabled when token storage is disabled."); } @@ -239,32 +81,116 @@ public void PostConfigure([NotNull] string name, [NotNull] OpenIddictServerOptio "Sliding expiration must be disabled when turning off token storage if rolling tokens are not used."); } - if (options.AccessTokenHandler != null && options.SigningCredentials.Count == 0) + if (options.EncryptionCredentials.Count == 0) { throw new InvalidOperationException(new StringBuilder() - .AppendLine("At least one signing key must be registered when using JWT as the access token format.") - .Append("Consider registering a certificate using 'services.AddOpenIddict().AddServer().AddSigningCertificate()' ") - .Append("or 'services.AddOpenIddict().AddServer().AddDevelopmentSigningCertificate()' or call ") - .Append("'services.AddOpenIddict().AddServer().AddEphemeralSigningKey()' to use an ephemeral key.") + .AppendLine("At least one encryption key must be registered in the OpenIddict server options.") + .Append("Consider registering a certificate using 'services.AddOpenIddict().AddServer().AddEncryptionCertificate()' ") + .Append("or 'services.AddOpenIddict().AddServer().AddDevelopmentEncryptionCertificate()' or call ") + .Append("'services.AddOpenIddict().AddServer().AddEphemeralEncryptionKey()' to use an ephemeral key.") .ToString()); } - // Ensure at least one asymmetric signing certificate/key was registered if the implicit flow was enabled. - if (!options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey) && - options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.Implicit)) + if (!options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey)) { throw new InvalidOperationException(new StringBuilder() - .AppendLine("At least one asymmetric signing key must be registered when enabling the implicit flow.") + .AppendLine("At least one asymmetric signing key must be registered in the OpenIddict server options.") .Append("Consider registering a certificate using 'services.AddOpenIddict().AddServer().AddSigningCertificate()' ") .Append("or 'services.AddOpenIddict().AddServer().AddDevelopmentSigningCertificate()' or call ") .Append("'services.AddOpenIddict().AddServer().AddEphemeralSigningKey()' to use an ephemeral key.") .ToString()); } + // If the degraded mode was enabled, ensure custom validation handlers + // have been registered for the endpoints that require manual validation. + if (options.EnableDegradedMode) + { + if (options.AuthorizationEndpointUris.Count != 0 && !options.CustomHandlers.Any( + descriptor => descriptor.ContextType == typeof(ValidateAuthorizationRequestContext) && + descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) + { + throw new InvalidOperationException(new StringBuilder() + .Append("No custom authorization request validation handler was found. When enabling the degraded mode, ") + .Append("a custom 'IOpenIddictServerHandler' must be implemented ") + .Append("to validate authorization requests (e.g to ensure the client_id and redirect_uri are valid).") + .ToString()); + } + + if (options.TokenEndpointUris.Count != 0 && !options.CustomHandlers.Any( + descriptor => descriptor.ContextType == typeof(ValidateTokenRequestContext) && + descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) + { + throw new InvalidOperationException(new StringBuilder() + .Append("No custom token request validation handler was found. When enabling the degraded mode, ") + .Append("a custom 'IOpenIddictServerHandler' must be implemented ") + .Append("to validate token requests (e.g to ensure the client_id and client_secret are valid).") + .ToString()); + } + } + // Automatically add the offline_access scope if the refresh token grant has been enabled. - if (options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.RefreshToken)) + if (options.GrantTypes.Contains(GrantTypes.RefreshToken)) + { + options.Scopes.Add(Scopes.OfflineAccess); + } + + foreach (var key in options.EncryptionCredentials + .Select(credentials => credentials.Key) + .Concat(options.SigningCredentials.Select(credentials => credentials.Key))) { - options.Scopes.Add(OpenIddictConstants.Scopes.OfflineAccess); + if (!string.IsNullOrEmpty(key.KeyId)) + { + continue; + } + + key.KeyId = GetKeyIdentifier(key); + } + + static string GetKeyIdentifier(SecurityKey key) + { + // When no key identifier can be retrieved from the security keys, a value is automatically + // inferred from the hexadecimal representation of the certificate thumbprint (SHA-1) + // when the key is bound to a X.509 certificate or from the public part of the signing key. + + if (key is X509SecurityKey x509SecurityKey) + { + return x509SecurityKey.Certificate.Thumbprint; + } + + if (key is RsaSecurityKey rsaSecurityKey) + { + // Note: if the RSA parameters are not attached to the signing key, + // extract them by calling ExportParameters on the RSA instance. + var parameters = rsaSecurityKey.Parameters; + if (parameters.Modulus == null) + { + parameters = rsaSecurityKey.Rsa.ExportParameters(includePrivateParameters: false); + + Debug.Assert(parameters.Modulus != null, + "A null modulus shouldn't be returned by RSA.ExportParameters()."); + } + + // Only use the 40 first chars of the base64url-encoded modulus. + var identifier = Base64UrlEncoder.Encode(parameters.Modulus); + return identifier.Substring(0, Math.Min(identifier.Length, 40)).ToUpperInvariant(); + } + +#if SUPPORTS_ECDSA + if (key is ECDsaSecurityKey ecsdaSecurityKey) + { + // Extract the ECDSA parameters from the signing credentials. + var parameters = ecsdaSecurityKey.ECDsa.ExportParameters(includePrivateParameters: false); + + Debug.Assert(parameters.Q.X != null, + "Invalid coordinates shouldn't be returned by ECDsa.ExportParameters()."); + + // Only use the 40 first chars of the base64url-encoded X coordinate. + var identifier = Base64UrlEncoder.Encode(parameters.Q.X); + return identifier.Substring(0, Math.Min(identifier.Length, 40)).ToUpperInvariant(); + } +#endif + + return null; } } } diff --git a/src/OpenIddict.Server/OpenIddictServerEndpointType.cs b/src/OpenIddict.Server/OpenIddictServerEndpointType.cs new file mode 100644 index 000000000..f89db7ed9 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEndpointType.cs @@ -0,0 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +namespace OpenIddict.Server +{ + /// + /// Represents the type of an OpenIddict server endpoint. + /// + public enum OpenIddictServerEndpointType + { + /// + /// Unknown endpoint. + /// + Unknown = 0, + + /// + /// Authorization endpoint. + /// + Authorization = 1, + + /// + /// Token endpoint. + /// + Token = 2, + + /// + /// Logout endpoint. + /// + Logout = 3, + + /// + /// Configuration endpoint. + /// + Configuration = 4, + + /// + /// Cryptography endpoint. + /// + Cryptography = 5, + + /// + /// Userinfo endpoint. + /// + Userinfo = 6, + + /// + /// Introspection endpoint. + /// + Introspection = 7, + + /// + /// Revocation endpoint. + /// + Revocation = 8 + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvent.cs b/src/OpenIddict.Server/OpenIddictServerEvent.cs deleted file mode 100644 index 73836bdf5..000000000 --- a/src/OpenIddict.Server/OpenIddictServerEvent.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using JetBrains.Annotations; - -namespace OpenIddict.Server -{ - /// - /// Represents an OpenIddict server event. - /// - /// The type of the context instance associated with the event. - public class OpenIddictServerEvent : IOpenIddictServerEvent where TContext : class - { - /// - /// Creates a new instance of . - /// - /// The context instance associated with the event. - public OpenIddictServerEvent([NotNull] TContext context) - => Context = context ?? throw new ArgumentNullException(nameof(context)); - - /// - /// Gets the context instance associated with the event. - /// - public TContext Context { get; } - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerEventDispatcher.cs b/src/OpenIddict.Server/OpenIddictServerEventDispatcher.cs deleted file mode 100644 index a002e3f8e..000000000 --- a/src/OpenIddict.Server/OpenIddictServerEventDispatcher.cs +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; - -namespace OpenIddict.Server -{ - /// - /// Dispatches events by invoking the corresponding notification handlers. - /// - public class OpenIddictServerEventDispatcher : IOpenIddictServerEventDispatcher - { - private readonly IServiceProvider _provider; - - /// - /// Creates a new instance of the class. - /// - public OpenIddictServerEventDispatcher([NotNull] IServiceProvider provider) - => _provider = provider; - - /// - /// Publishes a new event. - /// - /// The type of the event to publish. - /// The event to publish. - /// A that can be used to monitor the asynchronous operation. - public async Task DispatchAsync([NotNull] TEvent notification) where TEvent : class, IOpenIddictServerEvent - { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - foreach (var handler in _provider.GetServices>()) - { - switch (await handler.HandleAsync(notification)) - { - case OpenIddictServerEventState.Unhandled: continue; - case OpenIddictServerEventState.Handled: return; - - default: throw new InvalidOperationException("The specified event state is not valid."); - } - } - } - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerEventHandler.cs b/src/OpenIddict.Server/OpenIddictServerEventHandler.cs deleted file mode 100644 index b0d9e4900..000000000 --- a/src/OpenIddict.Server/OpenIddictServerEventHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Threading.Tasks; -using JetBrains.Annotations; - -namespace OpenIddict.Server -{ - /// - /// Represents a handler able to process events. - /// - /// The type of the events handled by this instance. - public class OpenIddictServerEventHandler : IOpenIddictServerEventHandler - where TEvent : class, IOpenIddictServerEvent - { - private readonly Func> _handler; - - /// - /// Creates a new event using the specified handler delegate. - /// - /// The event handler delegate. - public OpenIddictServerEventHandler([NotNull] Func> handler) - => _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - - /// - /// Processes the event. - /// - /// The event to process. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result determines whether next handlers in the pipeline are invoked. - /// - public Task HandleAsync(TEvent notification) - => _handler(notification ?? throw new ArgumentNullException(nameof(notification))); - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerEventState.cs b/src/OpenIddict.Server/OpenIddictServerEventState.cs deleted file mode 100644 index 9873e49c2..000000000 --- a/src/OpenIddict.Server/OpenIddictServerEventState.cs +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -namespace OpenIddict.Server -{ - /// - /// Represents the state of an event triggered by the OpenIddict - /// server components and processed by user-defined handlers. - /// - public enum OpenIddictServerEventState - { - /// - /// Marks the event as unhandled, allowing the event service to invoke the - /// other event handlers registered in the dependency injection container. - /// Using this value is recommended for event handlers that don't produce - /// an immediate response (i.e that don't call context.HandleResponse(), - /// context.SkipHandler(), context.Validate() or context.Reject()). - /// - Unhandled = 0, - - /// - /// Marks the event as fully handled, preventing the event service from invoking - /// other event handlers registered in the dependency injection container. - /// Using this value is recommended for event handlers that produce an - /// immediate response (i.e that call context.HandleResponse(), - /// context.SkipHandler(), context.Validate() or context.Reject()). - /// - Handled = 1 - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs new file mode 100644 index 000000000..fda22c030 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs @@ -0,0 +1,150 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Security.Claims; +using JetBrains.Annotations; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerEvents + { + /// + /// Represents an event called for each request to the authorization endpoint to give the user code + /// a chance to manually extract the authorization request from the ambient HTTP context. + /// + public class ExtractAuthorizationRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractAuthorizationRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each request to the authorization endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public class ValidateAuthorizationRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateAuthorizationRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + // Infer the redirect_uri from the value specified by the client application. + => RedirectUri = Request?.RedirectUri; + + /// + /// Gets the client_id specified by the client application. + /// + public string ClientId => Request.ClientId; + + /// + /// Gets the redirect_uri specified by the client application. + /// If it's not provided by the client, it must be set by + /// the user code by calling . + /// + public string RedirectUri { get; private set; } + + /// + /// Populates the property with the specified redirect_uri. + /// + /// The redirect_uri to use when redirecting the user agent. + public void SetRedirectUri(string address) + { + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentException("The redirect_uri cannot be null or empty.", nameof(address)); + } + + // Don't allow validation to alter the redirect_uri parameter extracted + // from the request if the address was explicitly provided by the client. + if (!string.IsNullOrEmpty(Request.RedirectUri) && + !string.Equals(Request.RedirectUri, address, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "The authorization request cannot be validated because a different " + + "redirect_uri was specified by the client application."); + } + + RedirectUri = address; + } + } + + /// + /// Represents an event called for each validated authorization request + /// to allow the user code to decide how the request should be handled. + /// + public class HandleAuthorizationRequestContext : BaseValidatingTicketContext + { + /// + /// Creates a new instance of the class. + /// + public HandleAuthorizationRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called before the authorization response is returned to the caller. + /// + public class ApplyAuthorizationResponseContext : BaseRequestContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyAuthorizationResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the security principal. + /// + public ClaimsPrincipal Principal { get; set; } + + /// + /// Gets the access code expected to + /// be returned to the client application. + /// Depending on the flow, it may be null. + /// + public string AccessToken => Response.AccessToken; + + /// + /// Gets the authorization code expected to + /// be returned to the client application. + /// Depending on the flow, it may be null. + /// + public string AuthorizationCode => Response.Code; + + /// + /// Gets the error code returned to the client application. + /// When the response indicates a successful response, + /// this property returns null. + /// + public string Error => Response.Error; + + /// + /// Gets or sets the callback URL the user agent will be redirected to, if applicable. + /// Note: manually changing the value of this property is generally not recommended + /// and extreme caution must be taken to ensure the user agent is not redirected to + /// an untrusted address, which would result in an "open redirection" vulnerability. + /// + public string RedirectUri { get; set; } + + /// + /// Gets or sets the response mode used to redirect the user agent, if applicable. + /// Note: manually changing the value of this property is generally not recommended. + /// + public string ResponseMode { get; set; } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs new file mode 100644 index 000000000..fee0e9ec0 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs @@ -0,0 +1,276 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerEvents + { + /// + /// Represents an event called for each request to the configuration endpoint to give the user code + /// a chance to manually extract the configuration request from the ambient HTTP context. + /// + public class ExtractConfigurationRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractConfigurationRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each request to the configuration endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public class ValidateConfigurationRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateConfigurationRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each validated configuration request + /// to allow the user code to decide how the request should be handled. + /// + public class HandleConfigurationRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public HandleConfigurationRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the additional parameters returned to the client application. + /// + public IDictionary Metadata { get; } = + new Dictionary(StringComparer.Ordinal); + + /// + /// Gets or sets the authorization endpoint address. + /// + public Uri AuthorizationEndpoint { get; set; } + + /// + /// Gets or sets the JWKS endpoint address. + /// + public Uri CryptographyEndpoint { get; set; } + + /// + /// Gets or sets the introspection endpoint address. + /// + public Uri IntrospectionEndpoint { get; set; } + + /// + /// Gets or sets the logout endpoint address. + /// + public Uri LogoutEndpoint { get; set; } + + /// + /// Gets or sets the revocation endpoint address. + /// + public Uri RevocationEndpoint { get; set; } + + /// + /// Gets or sets the token endpoint address. + /// + public Uri TokenEndpoint { get; set; } + + /// + /// Gets or sets the userinfo endpoint address. + /// + public Uri UserinfoEndpoint { get; set; } + + /// + /// Gets or sets the issuer address. + /// + public Uri Issuer { get; set; } + + /// + /// Gets the list of claims supported by the authorization server. + /// + public ISet Claims { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// Gets a list of the code challenge methods + /// supported by the authorization server. + /// + public ISet CodeChallengeMethods { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// Gets the list of grant types + /// supported by the authorization server. + /// + public ISet GrantTypes { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// Gets a list of signing algorithms supported by the + /// authorization server for signing the identity tokens. + /// + public ISet IdTokenSigningAlgorithms { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// Gets a list of client authentication methods supported by + /// the introspection endpoint provided by the authorization server. + /// + public ISet IntrospectionEndpointAuthenticationMethods { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// Gets the list of response modes + /// supported by the authorization server. + /// + public ISet ResponseModes { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// Gets the list of response types + /// supported by the authorization server. + /// + public ISet ResponseTypes { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// Gets a list of client authentication methods supported by + /// the revocation endpoint provided by the authorization server. + /// + public ISet RevocationEndpointAuthenticationMethods { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// Gets the list of scope values + /// supported by the authorization server. + /// + public ISet Scopes { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// Gets the list of subject types + /// supported by the authorization server. + /// + public ISet SubjectTypes { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// Gets a list of client authentication methods supported by + /// the token endpoint provided by the authorization server. + /// + public ISet TokenEndpointAuthenticationMethods { get; } = + new HashSet(StringComparer.Ordinal); + } + + /// + /// Represents an event called before the configuration response is returned to the caller. + /// + public class ApplyConfigurationResponseContext : BaseRequestContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyConfigurationResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the error code returned to the client application. + /// When the response indicates a successful response, + /// this property returns null. + /// + public string Error => Response.Error; + } + + /// + /// Represents an event called for each request to the cryptography endpoint to give the user code + /// a chance to manually extract the cryptography request from the ambient HTTP context. + /// + public class ExtractCryptographyRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractCryptographyRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each request to the cryptography endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public class ValidateCryptographyRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateCryptographyRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each validated cryptography request + /// to allow the user code to decide how the request should be handled. + /// + public class HandleCryptographyRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public HandleCryptographyRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the list of JSON Web Keys exposed by the JWKS endpoint. + /// + public IList Keys { get; } = new List(); + } + + /// + /// Represents an event called before the cryptography response is returned to the caller. + /// + public class ApplyCryptographyResponseContext : BaseRequestContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyCryptographyResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the error code returned to the client application. + /// When the response indicates a successful response, + /// this property returns null. + /// + public string Error => Response.Error; + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs new file mode 100644 index 000000000..c136383a7 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs @@ -0,0 +1,94 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Security.Claims; +using JetBrains.Annotations; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerEvents + { + /// + /// Represents an event called for each request to the token endpoint to give the user code + /// a chance to manually extract the token request from the ambient HTTP context. + /// + public class ExtractTokenRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractTokenRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each request to the token endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public class ValidateTokenRequestContext : BaseValidatingClientContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateTokenRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the security principal extracted from the authorization + /// code or the refresh token, if applicable to the current token request. + /// + public ClaimsPrincipal Principal { get; set; } + } + + /// + /// Represents an event called for each validated token request + /// to allow the user code to decide how the request should be handled. + /// + public class HandleTokenRequestContext : BaseValidatingTicketContext + { + /// + /// Creates a new instance of the class. + /// + public HandleTokenRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called before the token response is returned to the caller. + /// + public class ApplyTokenResponseContext : BaseRequestContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyTokenResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the security principal used to forge the token response. + /// + public ClaimsPrincipal Principal { get; set; } + + /// + /// Gets the error code returned to the client application. + /// When the response indicates a successful response, + /// this property returns null. + /// + public string Error => Response.Error; + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs new file mode 100644 index 000000000..37ebe6850 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs @@ -0,0 +1,174 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using OpenIddict.Abstractions; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerEvents + { + /// + /// Represents an event called for each request to the introspection endpoint to give the user code + /// a chance to manually extract the introspection request from the ambient HTTP context. + /// + public class ExtractIntrospectionRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractIntrospectionRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each request to the introspection endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public class ValidateIntrospectionRequestContext : BaseValidatingClientContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateIntrospectionRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the optional token_type_hint parameter extracted from the + /// introspection request, or null if it cannot be found. + /// + public string TokenTypeHint => Request.TokenTypeHint; + } + + /// + /// Represents an event called for each validated introspection request + /// to allow the user code to decide how the request should be handled. + /// + public class HandleIntrospectionRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public HandleIntrospectionRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the additional claims returned to the caller. + /// + public IDictionary Claims { get; } = + new Dictionary(StringComparer.Ordinal); + + /// + /// Gets or sets the flag indicating + /// whether the token is active or inactive. + /// + public bool Active { get; set; } + + /// + /// Gets the list of audiences returned to the caller + /// as part of the "aud" claim, if applicable. + /// + public ISet Audiences { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// Gets or sets the "client_id" claim + /// returned to the caller, if applicable. + /// + public string ClientId { get; set; } + + /// + /// Gets or sets the "exp" claim + /// returned to the caller, if applicable. + /// + public DateTimeOffset? ExpiresAt { get; set; } + + /// + /// Gets or sets the "iat" claim + /// returned to the caller, if applicable. + /// + public DateTimeOffset? IssuedAt { get; set; } + + /// + /// Gets or sets the "iss" claim + /// returned to the caller, if applicable. + /// + public string Issuer { get; set; } + + /// + /// Gets or sets the "nbf" claim + /// returned to the caller, if applicable. + /// + public DateTimeOffset? NotBefore { get; set; } + + /// + /// Gets the list of scopes returned to the caller + /// as part of the "scope" claim, if applicable. + /// + public ISet Scopes { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// Gets or sets the "sub" claim + /// returned to the caller, if applicable. + /// + public string Subject { get; set; } + + /// + /// Gets or sets the "jti" claim + /// returned to the caller, if applicable. + /// + public string TokenId { get; set; } + + /// + /// Gets or sets the "token_type" claim + /// returned to the caller, if applicable. + /// + public string TokenType { get; set; } + + /// + /// Gets or sets the "token_usage" claim + /// returned to the caller, if applicable. + /// + public string TokenUsage { get; set; } + + /// + /// Gets or sets the "username" claim + /// returned to the caller, if applicable. + /// + public string Username { get; set; } + } + + /// + /// Represents an event called before the introspection response is returned to the caller. + /// + public class ApplyIntrospectionResponseContext : BaseRequestContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyIntrospectionResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the error code returned to the client application. + /// When the response indicates a successful response, + /// this property returns null. + /// + public string Error => Response.Error; + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs new file mode 100644 index 000000000..0dc7af6f3 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs @@ -0,0 +1,99 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerEvents + { + /// + /// Represents an event called for each request to the revocation endpoint to give the user code + /// a chance to manually extract the revocation request from the ambient HTTP context. + /// + public class ExtractRevocationRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractRevocationRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each request to the revocation endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public class ValidateRevocationRequestContext : BaseValidatingClientContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateRevocationRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the optional token_type_hint parameter extracted from the + /// revocation request, or null if it cannot be found. + /// + public string TokenTypeHint => Request.TokenTypeHint; + } + + /// + /// Represents an event called for each validated revocation request + /// to allow the user code to decide how the request should be handled. + /// + public class HandleRevocationRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public HandleRevocationRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the authentication ticket. + /// + public IDictionary Claims { get; } + = new Dictionary(StringComparer.Ordinal); + + /// + /// Gets or sets a boolean indicating whether + /// the token was successfully revoked. + /// + public bool Revoked { get; set; } + } + + /// + /// Represents an event called before the revocation response is returned to the caller. + /// + public class ApplyRevocationResponseContext : BaseRequestContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyRevocationResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the error code returned to the client application. + /// When the response indicates a successful response, + /// this property returns null. + /// + public string Error => Response.Error; + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs new file mode 100644 index 000000000..bbd749fa4 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs @@ -0,0 +1,236 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Security.Claims; +using JetBrains.Annotations; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerEvents + { + /// + /// Represents an abstract base class used for certain event contexts. + /// + public abstract class BaseSerializingContext : BaseContext + { + /// + /// Creates a new instance of the class. + /// + public BaseSerializingContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the security principal containing the claims to serialize. + /// + public ClaimsPrincipal Principal { get; set; } + + /// + /// Gets or sets the encrypting credentials used to encrypt the token. + /// + public EncryptingCredentials EncryptingCredentials { get; set; } + + /// + /// Gets or sets the signing credentials used to sign the token. + /// + public SigningCredentials SigningCredentials { get; set; } + + /// + /// Gets or sets the security token handler used to serialize the token. + /// + public JsonWebTokenHandler SecurityTokenHandler { get; set; } + + /// + /// Gets or sets the issuer address. + /// + public Uri Issuer { get; set; } + + /// + /// Gets or sets the token returned to the client application. + /// + public string Token { get; set; } + + /// + /// Gets or sets the token usage. + /// + public string TokenUsage { get; set; } + + /// + /// Gets a boolean indicating whether the + /// method was called. + /// + public bool IsHandled { get; private set; } + + /// + /// Marks the serialization process as handled by the application code. + /// + public void HandleSerialization() => IsHandled = true; + } + + /// + /// Represents an abstract base class used for certain event contexts. + /// + public abstract class BaseDeserializingContext : BaseContext + { + /// + /// Creates a new instance of the class. + /// + public BaseDeserializingContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the security principal containing the deserialized claims. + /// + public ClaimsPrincipal Principal { get; set; } + + /// + /// Gets or sets the validation parameters used to verify the authenticity of access tokens. + /// Note: this property is only used when is not null. + /// + public TokenValidationParameters TokenValidationParameters { get; set; } = new TokenValidationParameters(); + + /// + /// Gets or sets the security token handler used to + /// deserialize the authentication ticket. + /// + public JsonWebTokenHandler SecurityTokenHandler { get; set; } + + /// + /// Gets or sets the token used by the client application. + /// + public string Token { get; set; } + + /// + /// Gets or sets the token usage. + /// + public string TokenUsage { get; set; } + + /// + /// Gets a boolean indicating whether the + /// method was called. + /// + public bool IsHandled { get; private set; } + + /// + /// Marks the deserialization process as handled by the application code. + /// + public void HandleDeserialization() => IsHandled = true; + } + + /// + /// Represents an event called when serializing an access token. + /// + public class SerializeAccessTokenContext : BaseSerializingContext + { + /// + /// Creates a new instance of the class. + /// + public SerializeAccessTokenContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + => TokenUsage = TokenUsages.AccessToken; + } + + /// + /// Represents an event called when serializing an authorization code. + /// + public class SerializeAuthorizationCodeContext : BaseSerializingContext + { + /// + /// Creates a new instance of the class. + /// + public SerializeAuthorizationCodeContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + => TokenUsage = TokenUsages.AuthorizationCode; + } + + /// + /// Represents an event called when serializing an identity token. + /// + public class SerializeIdentityTokenContext : BaseSerializingContext + { + /// + /// Creates a new instance of the class. + /// + public SerializeIdentityTokenContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + => TokenUsage = TokenUsages.IdToken; + } + + /// + /// Represents an event called when serializing a refresh token. + /// + public class SerializeRefreshTokenContext : BaseSerializingContext + { + /// + /// Creates a new instance of the class. + /// + public SerializeRefreshTokenContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + => TokenUsage = TokenUsages.RefreshToken; + } + + /// + /// Represents an event called when deserializing an access token. + /// + public class DeserializeAccessTokenContext : BaseDeserializingContext + { + /// + /// Creates a new instance of the class. + /// + public DeserializeAccessTokenContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + => TokenUsage = TokenUsages.AccessToken; + } + + /// + /// Represents an event called when deserializing an authorization code. + /// + public class DeserializeAuthorizationCodeContext : BaseDeserializingContext + { + /// + /// Creates a new instance of the class. + /// + public DeserializeAuthorizationCodeContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + => TokenUsage = TokenUsages.AuthorizationCode; + } + + /// + /// Represents an event called when deserializing an identity token. + /// + public class DeserializeIdentityTokenContext : BaseDeserializingContext + { + /// + /// Creates a new instance of the class. + /// + public DeserializeIdentityTokenContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + => TokenUsage = TokenUsages.IdToken; + } + + /// + /// Represents an event called when deserializing a refresh token. + /// + public class DeserializeRefreshTokenContext : BaseDeserializingContext + { + /// + /// Creates a new instance of the class. + /// + public DeserializeRefreshTokenContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + => TokenUsage = TokenUsages.RefreshToken; + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs new file mode 100644 index 000000000..dd1ec80d1 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs @@ -0,0 +1,117 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using JetBrains.Annotations; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerEvents + { + /// + /// Represents an event called for each request to the logout endpoint to give the user code + /// a chance to manually extract the logout request from the ambient HTTP context. + /// + public class ExtractLogoutRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractLogoutRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each request to the logout endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public class ValidateLogoutRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateLogoutRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + // Infer the post_logout_redirect_uri from the value specified by the client application. + => PostLogoutRedirectUri = Request?.PostLogoutRedirectUri; + + /// + /// Gets the post_logout_redirect_uri specified by the client application. + /// + public string PostLogoutRedirectUri { get; private set; } + + /// + /// Populates the property with the specified redirect_uri. + /// + /// The post_logout_redirect_uri to use when redirecting the user agent. + public void SetPostLogoutRedirectUri(string address) + { + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentException("The post_logout_redirect_uri cannot be null or empty.", nameof(address)); + } + + // Don't allow validation to alter the post_logout_redirect_uri parameter extracted + // from the request if the address was explicitly provided by the client application. + if (!string.IsNullOrEmpty(Request.PostLogoutRedirectUri) && + !string.Equals(Request.PostLogoutRedirectUri, address, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "The end session request cannot be validated because a different " + + "post_logout_redirect_uri was specified by the client application."); + } + + PostLogoutRedirectUri = address; + } + } + + /// + /// Represents an event called for each validated logout request + /// to allow the user code to decide how the request should be handled. + /// + public class HandleLogoutRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public HandleLogoutRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called before the logout response is returned to the caller. + /// + public class ApplyLogoutResponseContext : BaseRequestContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyLogoutResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the error code returned to the client application. + /// When the response indicates a successful response, + /// this property returns null. + /// + public string Error => Response.Error; + + /// + /// Gets or sets the callback URL the user agent will be redirected to, if applicable. + /// Note: manually changing the value of this property is generally not recommended + /// and extreme caution must be taken to ensure the user agent is not redirected to + /// an untrusted address, which would result in an "open redirection" vulnerability. + /// + public string PostLogoutRedirectUri { get; set; } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs new file mode 100644 index 000000000..f2a5788b8 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs @@ -0,0 +1,183 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerEvents + { + /// + /// Represents an event called for each request to the userinfo endpoint to give the user code + /// a chance to manually extract the userinfo request from the ambient HTTP context. + /// + public class ExtractUserinfoRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractUserinfoRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each request to the userinfo endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public class ValidateUserinfoRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateUserinfoRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each validated userinfo request + /// to allow the user code to decide how the request should be handled. + /// + public class HandleUserinfoRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public HandleUserinfoRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the additional claims returned to the client application. + /// + public IDictionary Claims { get; } = + new Dictionary(StringComparer.Ordinal); + + /// + /// Gets or sets the value used for the "address" claim. + /// Note: this value should only be populated if the "address" + /// scope was requested and accepted by the resource owner. + /// + public JObject Address { get; set; } + + /// + /// Gets or sets the values used for the "aud" claim. + /// + public ISet Audiences { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// Gets or sets the value used for the "birthdate" claim. + /// Note: this value should only be populated if the "profile" + /// scope was requested and accepted by the resource owner. + /// + public string BirthDate { get; set; } + + /// + /// Gets or sets the value used for the "email" claim. + /// Note: this value should only be populated if the "email" + /// scope was requested and accepted by the resource owner. + /// + public string Email { get; set; } + + /// + /// Gets or sets the value used for the "email_verified" claim. + /// Note: this value should only be populated if the "email" + /// scope was requested and accepted by the resource owner. + /// + public bool? EmailVerified { get; set; } + + /// + /// Gets or sets the value used for the "family_name" claim. + /// Note: this value should only be populated if the "profile" + /// scope was requested and accepted by the resource owner. + /// + public string FamilyName { get; set; } + + /// + /// Gets or sets the value used for the "given_name" claim. + /// Note: this value should only be populated if the "profile" + /// scope was requested and accepted by the resource owner. + /// + public string GivenName { get; set; } + + /// + /// Gets or sets the value used for the "iss" claim. + /// + public string Issuer { get; set; } + + /// + /// Gets or sets the value used for the "phone_number" claim. + /// Note: this value should only be populated if the "phone" + /// scope was requested and accepted by the resource owner. + /// + public string PhoneNumber { get; set; } + + /// + /// Gets or sets the value used for the "phone_number_verified" claim. + /// Note: this value should only be populated if the "phone" + /// scope was requested and accepted by the resource owner. + /// + public bool? PhoneNumberVerified { get; set; } + + /// + /// Gets or sets the value used for the "preferred_username" claim. + /// Note: this value should only be populated if the "profile" + /// scope was requested and accepted by the resource owner. + /// + public string PreferredUsername { get; set; } + + /// + /// Gets or sets the value used for the "profile" claim. + /// Note: this value should only be populated if the "profile" + /// scope was requested and accepted by the resource owner. + /// + public string Profile { get; set; } + + /// + /// Gets or sets the unique value + /// used for the mandatory "sub" claim. + /// + public string Subject { get; set; } + + /// + /// Gets or sets the value used for the "website" claim. + /// Note: this value should only be populated if the "profile" + /// scope was requested and accepted by the resource owner. + /// + public string Website { get; set; } + } + + /// + /// Represents an event called before the userinfo response is returned to the caller. + /// + public class ApplyUserinfoResponseContext : BaseRequestContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyUserinfoResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the error code returned to the client application. + /// When the response indicates a successful response, + /// this property returns null. + /// + public string Error => Response.Error; + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs index 6cf70e9a8..363c4c7c8 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -4,568 +4,359 @@ * the license and the contributors participating to this project. */ -using AspNet.Security.OpenIdConnect.Server; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Security.Claims; using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using OpenIddict.Abstractions; namespace OpenIddict.Server { - /// - /// Contains common events used by the OpenIddict server handler. - /// - public static class OpenIddictServerEvents + public static partial class OpenIddictServerEvents { /// - /// Represents an event called for each HTTP request to determine if - /// it should be handled by the OpenID Connect server middleware. + /// Represents an abstract base class used for certain event contexts. /// - public sealed class MatchEndpoint : OpenIddictServerEvent + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class BaseContext { /// - /// Creates a new instance of . + /// Creates a new instance of the class. /// - /// The context instance associated with the notification. - public MatchEndpoint([NotNull] MatchEndpointContext context) : base(context) { } - } + protected BaseContext([NotNull] OpenIddictServerTransaction transaction) + => Transaction = transaction ?? throw new ArgumentNullException(nameof(transaction)); - /// - /// Represents an event called for each request to the authorization endpoint to give the user code - /// a chance to manually extract the authorization request from the ambient HTTP context. - /// - public sealed class ExtractAuthorizationRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets the environment associated with the current request being processed. /// - /// The context instance associated with the notification. - public ExtractAuthorizationRequest([NotNull] ExtractAuthorizationRequestContext context) : base(context) { } - } + public OpenIddictServerTransaction Transaction { get; } - /// - /// Represents an event called for each request to the configuration endpoint to give the user code - /// a chance to manually extract the configuration request from the ambient HTTP context. - /// - public sealed class ExtractConfigurationRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets or sets the endpoint type that handled the request, if applicable. /// - /// The context instance associated with the notification. - public ExtractConfigurationRequest([NotNull] ExtractConfigurationRequestContext context) : base(context) { } - } - - /// - /// Represents an event called for each request to the cryptography endpoint to give the user code - /// a chance to manually extract the cryptography request from the ambient HTTP context. - /// + public OpenIddictServerEndpointType EndpointType + { + get => Transaction.EndpointType; + set => Transaction.EndpointType = value; + } - public sealed class ExtractCryptographyRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets the logger responsible of logging processed operations. /// - /// The context instance associated with the notification. - public ExtractCryptographyRequest([NotNull] ExtractCryptographyRequestContext context) : base(context) { } - } + public ILogger Logger => Transaction.Logger; - /// - /// Represents an event called for each request to the introspection endpoint to give the user code - /// a chance to manually extract the introspection request from the ambient HTTP context. - /// - public sealed class ExtractIntrospectionRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets the OpenIddict server options. /// - /// The context instance associated with the notification. - public ExtractIntrospectionRequest([NotNull] ExtractIntrospectionRequestContext context) : base(context) { } - } + public OpenIddictServerOptions Options => Transaction.Options; - /// - /// Represents an event called for each request to the logout endpoint to give the user code - /// a chance to manually extract the logout request from the ambient HTTP context. - /// - public sealed class ExtractLogoutRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets the dictionary containing the properties associated with this event. /// - /// The context instance associated with the notification. - public ExtractLogoutRequest([NotNull] ExtractLogoutRequestContext context) : base(context) { } - } + public IDictionary Properties { get; } + = new Dictionary(StringComparer.OrdinalIgnoreCase); - /// - /// Represents an event called for each request to the revocation endpoint to give the user code - /// a chance to manually extract the revocation request from the ambient HTTP context. - /// - public sealed class ExtractRevocationRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets or sets the OpenIddict request or null if it couldn't be extracted. /// - /// The context instance associated with the notification. - public ExtractRevocationRequest([NotNull] ExtractRevocationRequestContext context) : base(context) { } - } + public OpenIddictRequest Request + { + get => Transaction.Request; + set => Transaction.Request = value; + } - /// - /// Represents an event called for each request to the token endpoint to give the user code - /// a chance to manually extract the token request from the ambient HTTP context. - /// - public sealed class ExtractTokenRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets or sets the OpenIddict response, if applicable. /// - /// The context instance associated with the notification. - public ExtractTokenRequest([NotNull] ExtractTokenRequestContext context) : base(context) { } + public OpenIddictResponse Response + { + get => Transaction.Response; + set => Transaction.Response = value; + } } - /// - /// Represents an event called for each request to the userinfo endpoint to give the user code - /// a chance to manually extract the userinfo request from the ambient HTTP context. - /// - public sealed class ExtractUserinfoRequest : OpenIddictServerEvent + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class BaseRequestContext : BaseContext { /// - /// Creates a new instance of . + /// Creates a new instance of the class. /// - /// The context instance associated with the notification. - public ExtractUserinfoRequest([NotNull] ExtractUserinfoRequestContext context) : base(context) { } - } + protected BaseRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } - /// - /// Represents an event called for each request to the authorization endpoint - /// to determine if the request is valid and should continue to be processed. - /// - public sealed class ValidateAuthorizationRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets a boolean indicating whether the request was fully handled. /// - /// The context instance associated with the notification. - public ValidateAuthorizationRequest([NotNull] ValidateAuthorizationRequestContext context) : base(context) { } - } + public bool IsRequestHandled { get; private set; } - /// - /// Represents an event called for each request to the configuration endpoint - /// to determine if the request is valid and should continue to be processed. - /// - public sealed class ValidateConfigurationRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets a boolean indicating whether the request processing was skipped. /// - /// The context instance associated with the notification. - public ValidateConfigurationRequest([NotNull] ValidateConfigurationRequestContext context) : base(context) { } - } + public bool IsRequestSkipped { get; private set; } - /// - /// Represents an event called for each request to the cryptography endpoint - /// to determine if the request is valid and should continue to be processed. - /// - public sealed class ValidateCryptographyRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Marks the request as fully handled. Once declared handled, + /// a request shouldn't be processed further by the underlying host. /// - /// The context instance associated with the notification. - public ValidateCryptographyRequest([NotNull] ValidateCryptographyRequestContext context) : base(context) { } - } + public void HandleRequest() => IsRequestHandled = true; - /// - /// Represents an event called for each request to the introspection endpoint - /// to determine if the request is valid and should continue to be processed. - /// - public sealed class ValidateIntrospectionRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Marks the request as skipped. Once declared skipped, a request + /// shouldn't be processed further by OpenIddict but should be allowed + /// to go through the next components in the processing pipeline + /// (if this pattern is supported by the underlying host). /// - /// The context instance associated with the notification. - public ValidateIntrospectionRequest([NotNull] ValidateIntrospectionRequestContext context) : base(context) { } + public void SkipRequest() => IsRequestSkipped = true; } /// - /// Represents an event called for each request to the logout endpoint - /// to determine if the request is valid and should continue to be processed. + /// Represents an abstract base class used for certain event contexts. /// - public sealed class ValidateLogoutRequest : OpenIddictServerEvent + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class BaseValidatingClientContext : BaseValidatingContext { /// - /// Creates a new instance of . + /// Creates a new instance of the class. /// - /// The context instance associated with the notification. - public ValidateLogoutRequest([NotNull] ValidateLogoutRequestContext context) : base(context) { } - } + protected BaseValidatingClientContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } - /// - /// Represents an event called for each request to the revocation endpoint - /// to determine if the request is valid and should continue to be processed. - /// - public sealed class ValidateRevocationRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets the "client_id" parameter for the current request. + /// The authorization server application is responsible for + /// validating this value to ensure it identifies a registered client. /// - /// The context instance associated with the notification. - public ValidateRevocationRequest([NotNull] ValidateRevocationRequestContext context) : base(context) { } - } + public string ClientId => (string) Request[OpenIddictConstants.Parameters.ClientId]; - /// - /// Represents an event called for each request to the token endpoint - /// to determine if the request is valid and should continue to be processed. - /// - public sealed class ValidateTokenRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets the "client_secret" parameter for the current request. + /// The authorization server application is responsible for + /// validating this value to ensure it identifies a registered client. /// - /// The context instance associated with the notification. - public ValidateTokenRequest([NotNull] ValidateTokenRequestContext context) : base(context) { } + public string ClientSecret => (string) Request[OpenIddictConstants.Parameters.ClientSecret]; } /// - /// Represents an event called for each request to the userinfo endpoint - /// to determine if the request is valid and should continue to be processed. + /// Represents an abstract base class used for certain event contexts. /// - public sealed class ValidateUserinfoRequest : OpenIddictServerEvent + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class BaseValidatingContext : BaseRequestContext { /// - /// Creates a new instance of . + /// Creates a new instance of the class. /// - /// The context instance associated with the notification. - public ValidateUserinfoRequest([NotNull] ValidateUserinfoRequestContext context) : base(context) { } - } + protected BaseValidatingContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } - /// - /// Represents an event called for each validated authorization request - /// to allow the user code to decide how the request should be handled. - /// - public sealed class HandleAuthorizationRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets a boolean indicating whether the request will be rejected. /// - /// The context instance associated with the notification. - public HandleAuthorizationRequest([NotNull] HandleAuthorizationRequestContext context) : base(context) { } - } + public bool IsRejected { get; protected set; } - /// - /// Represents an event called for each validated configuration request - /// to allow the user code to decide how the request should be handled. - /// - public sealed class HandleConfigurationRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets or sets the "error" parameter returned to the client application. /// - /// The context instance associated with the notification. - public HandleConfigurationRequest([NotNull] HandleConfigurationRequestContext context) : base(context) { } - } + public string Error { get; private set; } - /// - /// Represents an event called for each validated cryptography request - /// to allow the user code to decide how the request should be handled. - /// - public sealed class HandleCryptographyRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets or sets the "error_description" parameter returned to the client application. /// - /// The context instance associated with the notification. - public HandleCryptographyRequest([NotNull] HandleCryptographyRequestContext context) : base(context) { } - } + public string ErrorDescription { get; private set; } - /// - /// Represents an event called for each validated introspection request - /// to allow the user code to decide how the request should be handled. - /// - public sealed class HandleIntrospectionRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets or sets the "error_uri" parameter returned to the client application. /// - /// The context instance associated with the notification. - public HandleIntrospectionRequest([NotNull] HandleIntrospectionRequestContext context) : base(context) { } - } + public string ErrorUri { get; private set; } - /// - /// Represents an event called for each validated logout request - /// to allow the user code to decide how the request should be handled. - /// - public sealed class HandleLogoutRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Rejects the request. /// - /// The context instance associated with the notification. - public HandleLogoutRequest([NotNull] HandleLogoutRequestContext context) : base(context) { } - } + public virtual void Reject() => IsRejected = true; - /// - /// Represents an event called for each validated revocation request - /// to allow the user code to decide how the request should be handled. - /// - public sealed class HandleRevocationRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Rejects the request. /// - /// The context instance associated with the notification. - public HandleRevocationRequest([NotNull] HandleRevocationRequestContext context) : base(context) { } - } + /// The "error" parameter returned to the client application. + public virtual void Reject(string error) + { + Error = error; - /// - /// Represents an event called for each validated token request - /// to allow the user code to decide how the request should be handled. - /// - public sealed class HandleTokenRequest : OpenIddictServerEvent - { - /// - /// Creates a new instance of . - /// - /// The context instance associated with the notification. - public HandleTokenRequest([NotNull] HandleTokenRequestContext context) : base(context) { } - } + Reject(); + } - /// - /// Represents an event called for each validated userinfo request - /// to allow the user code to decide how the request should be handled. - /// - public sealed class HandleUserinfoRequest : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Rejects the request. /// - /// The context instance associated with the notification. - public HandleUserinfoRequest([NotNull] HandleUserinfoRequestContext context) : base(context) { } - } + /// The "error" parameter returned to the client application. + /// The "error_description" parameter returned to the client application. + public virtual void Reject(string error, string description) + { + Error = error; + ErrorDescription = description; - /// - /// Represents an event called when processing a challenge response. - /// - public sealed class ProcessChallengeResponse : OpenIddictServerEvent - { - /// - /// Creates a new instance of . - /// - /// The context instance associated with the notification. - public ProcessChallengeResponse([NotNull] ProcessChallengeResponseContext context) : base(context) { } - } + Reject(); + } - /// - /// Represents an event called when processing a sign-in response. - /// - public sealed class ProcessSigninResponse : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Rejects the request. /// - /// The context instance associated with the notification. - public ProcessSigninResponse([NotNull] ProcessSigninResponseContext context) : base(context) { } - } + /// The "error" parameter returned to the client application. + /// The "error_description" parameter returned to the client application. + /// The "error_uri" parameter returned to the client application. + public virtual void Reject(string error, string description, string uri) + { + Error = error; + ErrorDescription = description; + ErrorUri = uri; - /// - /// Represents an event called when processing a sign-out response. - /// - public sealed class ProcessSignoutResponse : OpenIddictServerEvent - { - /// - /// Creates a new instance of . - /// - /// The context instance associated with the notification. - public ProcessSignoutResponse([NotNull] ProcessSignoutResponseContext context) : base(context) { } - } - - /// - /// Represents an event called before the authorization response is returned to the caller. - /// - public sealed class ApplyAuthorizationResponse : OpenIddictServerEvent - { - /// - /// Creates a new instance of . - /// - /// The context instance associated with the notification. - public ApplyAuthorizationResponse([NotNull] ApplyAuthorizationResponseContext context) : base(context) { } - } - - /// - /// Represents an event called before the configuration response is returned to the caller. - /// - public sealed class ApplyConfigurationResponse : OpenIddictServerEvent - { - /// - /// Creates a new instance of . - /// - /// The context instance associated with the notification. - public ApplyConfigurationResponse([NotNull] ApplyConfigurationResponseContext context) : base(context) { } + Reject(); + } } /// - /// Represents an event called before the cryptography response is returned to the caller. + /// Represents an abstract base class used for certain event contexts. /// - public sealed class ApplyCryptographyResponse : OpenIddictServerEvent + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class BaseValidatingTicketContext : BaseValidatingContext { /// - /// Creates a new instance of . + /// Creates a new instance of the class. /// - /// The context instance associated with the notification. - public ApplyCryptographyResponse([NotNull] ApplyCryptographyResponseContext context) : base(context) { } - } + protected BaseValidatingTicketContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } - /// - /// Represents an event called before the introspection response is returned to the caller. - /// - public sealed class ApplyIntrospectionResponse : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets or sets the security principal. /// - /// The context instance associated with the notification. - public ApplyIntrospectionResponse([NotNull] ApplyIntrospectionResponseContext context) : base(context) { } - } + public ClaimsPrincipal Principal { get; set; } - /// - /// Represents an event called before the logout response is returned to the caller. - /// - public sealed class ApplyLogoutResponse : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets the client identifier, or null if the client application is unknown. /// - /// The context instance associated with the notification. - public ApplyLogoutResponse([NotNull] ApplyLogoutResponseContext context) : base(context) { } - } + public string ClientId => Request.ClientId; - /// - /// Represents an event called before the revocation response is returned to the caller. - /// - public sealed class ApplyRevocationResponse : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets a boolean indicating whether the + /// method was called. /// - /// The context instance associated with the notification. - public ApplyRevocationResponse([NotNull] ApplyRevocationResponseContext context) : base(context) { } - } + public bool IsHandled { get; private set; } - /// - /// Represents an event called before the token response is returned to the caller. - /// - public sealed class ApplyTokenResponse : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Marks the authentication process as handled by the application code. /// - /// The context instance associated with the notification. - public ApplyTokenResponse([NotNull] ApplyTokenResponseContext context) : base(context) { } + public void HandleAuthentication() => IsHandled = true; } /// - /// Represents an event called before the userinfo response is returned to the caller. + /// Represents an event called when processing an incoming request. /// - public sealed class ApplyUserinfoResponse : OpenIddictServerEvent + public class ProcessRequestContext : BaseValidatingContext { /// - /// Creates a new instance of . + /// Creates a new instance of the class. /// - /// The context instance associated with the notification. - public ApplyUserinfoResponse([NotNull] ApplyUserinfoResponseContext context) : base(context) { } + public ProcessRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } } /// - /// Represents an event called when serializing an authorization code. + /// Represents an event called when processing an errored response. /// - public sealed class SerializeAuthorizationCode : OpenIddictServerEvent + public class ProcessErrorResponseContext : BaseRequestContext { /// - /// Creates a new instance of . + /// Creates a new instance of the class. /// - /// The context instance associated with the notification. - public SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context) : base(context) { } + public ProcessErrorResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } } /// - /// Represents an event called when serializing an access token. + /// Represents an event called when processing a challenge response. /// - public sealed class SerializeAccessToken : OpenIddictServerEvent + public class ProcessChallengeResponseContext : BaseValidatingContext { /// - /// Creates a new instance of . + /// Creates a new instance of the class. /// - /// The context instance associated with the notification. - public SerializeAccessToken([NotNull] SerializeAccessTokenContext context) : base(context) { } + public ProcessChallengeResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } } /// - /// Represents an event called when serializing an identity token. + /// Represents an event called when processing a sign-in response. /// - public sealed class SerializeIdentityToken : OpenIddictServerEvent + public class ProcessSigninResponseContext : BaseValidatingTicketContext { /// - /// Creates a new instance of . + /// Creates a new instance of the class. /// - /// The context instance associated with the notification. - public SerializeIdentityToken([NotNull] SerializeIdentityTokenContext context) : base(context) { } - } + public ProcessSigninResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } - /// - /// Represents an event called when serializing a refresh token. - /// - public sealed class SerializeRefreshToken : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets or sets a boolean indicating whether an access token + /// should be returned to the client application. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. /// - /// The context instance associated with the notification. - public SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) : base(context) { } - } + public bool IncludeAccessToken { get; set; } - /// - /// Represents an event called when deserializing an authorization code. - /// - public sealed class DeserializeAuthorizationCode : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets or sets a boolean indicating whether an authorization code + /// should be returned to the client application. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. /// - /// The context instance associated with the notification. - public DeserializeAuthorizationCode([NotNull] DeserializeAuthorizationCodeContext context) : base(context) { } - } + public bool IncludeAuthorizationCode { get; set; } - /// - /// Represents an event called when deserializing an access token. - /// - public sealed class DeserializeAccessToken : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets or sets a boolean indicating whether an identity token + /// should be returned to the client application. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. /// - /// The context instance associated with the notification. - public DeserializeAccessToken([NotNull] DeserializeAccessTokenContext context) : base(context) { } - } + public bool IncludeIdentityToken { get; set; } - /// - /// Represents an event called when deserializing an identity token. - /// - public sealed class DeserializeIdentityToken : OpenIddictServerEvent - { /// - /// Creates a new instance of . + /// Gets or sets a boolean indicating whether a refresh token + /// should be returned to the client application. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. /// - /// The context instance associated with the notification. - public DeserializeIdentityToken([NotNull] DeserializeIdentityTokenContext context) : base(context) { } + public bool IncludeRefreshToken { get; set; } } /// - /// Represents an event called when deserializing a refresh token. + /// Represents an event called when processing a sign-out response. /// - public sealed class DeserializeRefreshToken : OpenIddictServerEvent + public class ProcessSignoutResponseContext : BaseValidatingContext { /// - /// Creates a new instance of . + /// Creates a new instance of the class. /// - /// The context instance associated with the notification. - public DeserializeRefreshToken([NotNull] DeserializeRefreshTokenContext context) : base(context) { } + public ProcessSignoutResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } } } } diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index 2260a9370..e1a05a6c8 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -5,15 +5,13 @@ */ using System; -using System.Text; -using AspNet.Security.OpenIdConnect.Server; +using System.Linq; using JetBrains.Annotations; -using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using OpenIddict.Abstractions; using OpenIddict.Server; +using static OpenIddict.Server.OpenIddictServerHandlerFilters; +using static OpenIddict.Server.OpenIddictServerHandlers; namespace Microsoft.Extensions.DependencyInjection { @@ -35,40 +33,31 @@ public static OpenIddictServerBuilder AddServer([NotNull] this OpenIddictBuilder throw new ArgumentNullException(nameof(builder)); } - builder.Services.AddAuthentication(); - builder.Services.AddDistributedMemoryCache(); builder.Services.AddLogging(); - builder.Services.AddMemoryCache(); builder.Services.AddOptions(); - builder.Services.TryAddScoped(); - builder.Services.TryAddScoped(); - builder.Services.TryAddScoped(provider => - { - InvalidOperationException CreateException() => new InvalidOperationException(new StringBuilder() - .AppendLine("The core services must be registered when enabling the OpenIddict server handler.") - .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") - .Append("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") - .ToString()); + builder.Services.TryAddScoped(); - return new OpenIddictServerProvider( - provider.GetRequiredService>(), - provider.GetRequiredService(), - provider.GetService() ?? throw CreateException(), - provider.GetService() ?? throw CreateException(), - provider.GetService() ?? throw CreateException(), - provider.GetService() ?? throw CreateException()); - }); + // Register the built-in server event handlers used by the OpenIddict server components. + // Note: the order used here is not important, as the actual order is set in the options. + builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); - // Register the options initializers used by the OpenID Connect server handler and OpenIddict. - // Note: TryAddEnumerable() is used here to ensure the initializers are only registered once. - builder.Services.TryAddEnumerable(new[] - { - ServiceDescriptor.Singleton, OpenIddictServerConfiguration>(), - ServiceDescriptor.Singleton, OpenIddictServerConfiguration>(), - ServiceDescriptor.Singleton, OpenIddictServerConfiguration>(), - ServiceDescriptor.Singleton, OpenIdConnectServerInitializer>() - }); + // Register the built-in filters used by the default OpenIddict server event handlers. + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + + // Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< + IPostConfigureOptions, OpenIddictServerConfiguration>()); return new OpenIddictServerBuilder(builder.Services); } @@ -79,7 +68,7 @@ public static OpenIddictServerBuilder AddServer([NotNull] this OpenIddictBuilder /// The services builder used by OpenIddict to register new services. /// The configuration delegate used to configure the server services. /// This extension can be safely called multiple times. - /// The . + /// The . public static OpenIddictBuilder AddServer( [NotNull] this OpenIddictBuilder builder, [NotNull] Action configuration) diff --git a/src/OpenIddict.Server/OpenIddictServerHandler.cs b/src/OpenIddict.Server/OpenIddictServerHandler.cs index c6061e735..bdbf53a42 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandler.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandler.cs @@ -4,30 +4,36 @@ * the license and the contributors participating to this project. */ -using System.Text.Encodings.Web; -using AspNet.Security.OpenIdConnect.Server; +using System; +using System.Threading.Tasks; using JetBrains.Annotations; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; +using static OpenIddict.Server.OpenIddictServerEvents; namespace OpenIddict.Server { /// - /// Provides the logic necessary to extract, validate and handle OpenID Connect requests. + /// Represents a handler able to process events. /// - public class OpenIddictServerHandler : OpenIdConnectServerHandler + /// The type of the events handled by this instance. + public class OpenIddictServerHandler : IOpenIddictServerHandler where TContext : BaseContext { + private readonly Func _handler; + + /// + /// Creates a new event using the specified handler delegate. + /// + /// The event handler delegate. + public OpenIddictServerHandler([NotNull] Func handler) + => _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + /// - /// Creates a new instance of the class. + /// Processes the event. /// - public OpenIddictServerHandler( - [NotNull] IOptionsMonitor options, - [NotNull] ILoggerFactory logger, - [NotNull] UrlEncoder encoder, - [NotNull] ISystemClock clock) - : base(options, logger, encoder, clock) - { - } + /// The event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] TContext context) + => _handler(context ?? throw new ArgumentNullException(nameof(context))); } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerDescriptor.cs b/src/OpenIddict.Server/OpenIddictServerHandlerDescriptor.cs new file mode 100644 index 000000000..588aa4bd1 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerHandlerDescriptor.cs @@ -0,0 +1,199 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server +{ + /// + /// Represents an immutable descriptor of an OpenIddict server event handler. + /// + [DebuggerDisplay("{ServiceDescriptor?.ServiceType}")] + public class OpenIddictServerHandlerDescriptor + { + /// + /// Creates a new instance of the class. + /// + private OpenIddictServerHandlerDescriptor() { } + + /// + /// Gets the context type associated with the event. + /// + public Type ContextType { get; private set; } + + /// + /// Gets the list of filters responsible of excluding the handler + /// from the activated handlers if it doesn't meet the criteria. + /// + public ImmutableArray FilterTypes { get; private set; } = ImmutableArray.Create(); + + /// + /// Gets the order assigned to the handler. + /// + public int Order { get; private set; } + + /// + /// Gets the service descriptor associated with the handler. + /// + public ServiceDescriptor ServiceDescriptor { get; private set; } + + /// + /// Creates a builder allowing to initialize an immutable descriptor. + /// + /// The event context type. + /// A new descriptor builder. + public static Builder CreateBuilder() where TContext : BaseContext + => new Builder(); + + /// + /// Contains methods allowing to build a descriptor instance. + /// + /// The event context type. + public class Builder where TContext : BaseContext + { + private ServiceDescriptor _descriptor; + private readonly List _filterTypes = new List(); + private int _order; + + /// + /// Adds the type of a handler filter to the filters list. + /// + /// The event handler filter type. + /// The builder instance, so that calls can be easily chained. + public Builder AddFilter([NotNull] Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (!typeof(IOpenIddictServerHandlerFilter<>).MakeGenericType(typeof(TContext)).IsAssignableFrom(type)) + { + throw new InvalidOperationException("The specified service type is not valid."); + } + + _filterTypes.Add(type); + + return this; + } + + /// + /// Adds the type of a handler filter to the filters list. + /// + /// The event handler filter type. + /// The builder instance, so that calls can be easily chained. + public Builder AddFilter() + where TFilter : IOpenIddictServerHandlerFilter + => AddFilter(typeof(TFilter)); + + /// + /// Sets the service descriptor. + /// + /// The service descriptor. + /// The builder instance, so that calls can be easily chained. + public Builder SetServiceDescriptor([NotNull] ServiceDescriptor descriptor) + { + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + var type = descriptor.ServiceType; + if (!typeof(IOpenIddictServerHandler<>).MakeGenericType(typeof(TContext)).IsAssignableFrom(type)) + { + throw new InvalidOperationException("The specified service type is not valid."); + } + + _descriptor = descriptor; + + return this; + } + + /// + /// Sets the order in which the event handler will be invoked. + /// + /// The handler order. + /// The builder instance, so that calls can be easily chained. + public Builder SetOrder(int order) + { + _order = order; + + return this; + } + + /// + /// Configures the descriptor to use the specified inline handler. + /// + /// The handler instance. + /// The builder instance, so that calls can be easily chained. + public Builder UseInlineHandler([NotNull] Func handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + return UseSingletonHandler(new OpenIddictServerHandler(handler)); + } + + /// + /// Configures the descriptor to use the specified scoped handler. + /// + /// The handler type. + /// The builder instance, so that calls can be easily chained. + public Builder UseScopedHandler() + where THandler : IOpenIddictServerHandler + => SetServiceDescriptor(new ServiceDescriptor( + typeof(THandler), typeof(THandler), ServiceLifetime.Scoped)); + + /// + /// Configures the descriptor to use the specified singleton handler. + /// + /// The handler type. + /// The builder instance, so that calls can be easily chained. + public Builder UseSingletonHandler() + where THandler : IOpenIddictServerHandler + => SetServiceDescriptor(new ServiceDescriptor( + typeof(THandler), typeof(THandler), ServiceLifetime.Singleton)); + + /// + /// Configures the descriptor to use the specified singleton handler. + /// + /// The handler type. + /// The handler instance. + /// The builder instance, so that calls can be easily chained. + public Builder UseSingletonHandler([NotNull] THandler handler) + where THandler : IOpenIddictServerHandler + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + return SetServiceDescriptor(new ServiceDescriptor(typeof(THandler), handler)); + } + + /// + /// Build a new descriptor instance, based on the parameters that were previously set. + /// + /// The builder instance, so that calls can be easily chained. + public OpenIddictServerHandlerDescriptor Build() => new OpenIddictServerHandlerDescriptor + { + ContextType = typeof(TContext), + FilterTypes = _filterTypes.ToImmutableArray(), + Order = _order, + ServiceDescriptor = _descriptor ?? throw new InvalidOperationException("No service descriptor was set.") + }; + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs new file mode 100644 index 000000000..ff8044167 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs @@ -0,0 +1,194 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using JetBrains.Annotations; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server +{ + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static class OpenIddictServerHandlerFilters + { + /// + /// Represents a filter that excludes the associated handlers if no access token is returned. + /// + public class RequireAccessTokenIncluded : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] ProcessSigninResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(context.IncludeAccessToken); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no authorization code is returned. + /// + public class RequireAuthorizationCodeIncluded : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] ProcessSigninResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(context.IncludeAuthorizationCode); + } + } + + /// + /// Represents a filter that excludes the associated handlers when no client identifier is received. + /// + public class RequireClientIdParameter : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(!string.IsNullOrEmpty(context.Request.ClientId)); + } + } + + /// + /// Represents a filter that excludes the associated handlers if the degraded mode was not enabled. + /// + public class RequireDegradedModeDisabled : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(!context.Options.EnableDegradedMode); + } + } + + /// + /// Represents a filter that excludes the associated handlers if the degraded mode was enabled. + /// + public class RequireDegradedModeEnabled : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(context.Options.EnableDegradedMode); + } + } + + /// + /// Represents a filter that excludes the associated handlers if endpoint permissions were disabled. + /// + public class RequireEndpointPermissionsEnabled : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(!context.Options.IgnoreEndpointPermissions); + } + } + + /// + /// Represents a filter that excludes the associated handlers if grant type permissions were disabled. + /// + public class RequireGrantTypePermissionsEnabled : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(!context.Options.IgnoreGrantTypePermissions); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no identity token is returned. + /// + public class RequireIdentityTokenIncluded : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] ProcessSigninResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(context.IncludeIdentityToken); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no refresh token is returned. + /// + public class RequireRefreshTokenIncluded : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] ProcessSigninResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(context.IncludeRefreshToken); + } + } + + /// + /// Represents a filter that excludes the associated handlers if scope permissions were disabled. + /// + public class RequireScopePermissionsEnabled : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(!context.Options.IgnoreScopePermissions); + } + } + + /// + /// Represents a filter that excludes the associated handlers if scope validation was not enabled. + /// + public class RequireScopeValidationEnabled : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(!context.Options.DisableScopeValidation); + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs new file mode 100644 index 000000000..d0e15e433 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -0,0 +1,1613 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlerFilters; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerHandlers + { + public static class Authentication + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Authorization request top-level processing: + */ + ExtractAuthorizationRequest.Descriptor, + ValidateAuthorizationRequest.Descriptor, + HandleAuthorizationRequest.Descriptor, + ApplyAuthorizationResponse.Descriptor, + ApplyAuthorizationResponse.Descriptor, + ApplyAuthorizationResponse.Descriptor, + ApplyAuthorizationResponse.Descriptor, + + /* + * Authorization request validation: + */ + ValidateRequestParameter.Descriptor, + ValidateRequestUriParameter.Descriptor, + ValidateClientIdParameter.Descriptor, + ValidateRedirectUriParameter.Descriptor, + ValidateResponseTypeParameter.Descriptor, + ValidateResponseModeParameter.Descriptor, + ValidateNonceParameter.Descriptor, + ValidatePromptParameter.Descriptor, + ValidateCodeChallengeParameters.Descriptor, + ValidateClientId.Descriptor, + ValidateClientType.Descriptor, + ValidateClientRedirectUri.Descriptor, + ValidateScopes.Descriptor, + ValidateEndpointPermissions.Descriptor, + ValidateGrantTypePermissions.Descriptor, + ValidateScopePermissions.Descriptor, + + /* + * Authorization response processing: + */ + AttachRedirectUri.Descriptor, + InferResponseMode.Descriptor, + AttachResponseState.Descriptor); + + /// + /// Contains the logic responsible of extracting authorization requests and invoking the corresponding event handlers. + /// + public class ExtractAuthorizationRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ExtractAuthorizationRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Authorization) + { + return; + } + + var notification = new ExtractAuthorizationRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + if (notification.Request == null) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The authorization request was not correctly extracted. To extract authorization requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .ToString()); + } + + context.Logger.LogInformation("The authorization request was successfully extracted: {Request}.", notification.Request); + } + } + + /// + /// Contains the logic responsible of validating authorization requests and invoking the corresponding event handlers. + /// + public class ValidateAuthorizationRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateAuthorizationRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ExtractAuthorizationRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Authorization) + { + return; + } + + var notification = new ValidateAuthorizationRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + if (string.IsNullOrEmpty(notification.RedirectUri)) + { + throw new InvalidOperationException("The request cannot be validated because no client_id was specified."); + } + + // Store the validated redirect_uri as an environment property. + context.Transaction.Properties[Properties.ValidatedRedirectUri] = notification.RedirectUri; + + context.Logger.LogInformation("The authorization request was successfully validated."); + } + } + + /// + /// Contains the logic responsible of handling authorization requests and invoking the corresponding event handlers. + /// + public class HandleAuthorizationRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public HandleAuthorizationRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ValidateAuthorizationRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Authorization) + { + return; + } + + var notification = new HandleAuthorizationRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + if (notification.Principal != null) + { + var @event = new ProcessSigninResponseContext(context.Transaction) + { + Principal = notification.Principal, + Response = new OpenIddictResponse() + }; + + await _provider.DispatchAsync(@event); + + if (@event.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (@event.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The authorization request was not handled. To handle authorization requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .Append("Alternatively, enable the pass-through mode to handle them at a later stage.") + .ToString()); + } + } + + /// + /// Contains the logic responsible of processing sign-in responses and invoking the corresponding event handlers. + /// + public class ApplyAuthorizationResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + private readonly IOpenIddictServerProvider _provider; + + public ApplyAuthorizationResponse([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Authorization) + { + return; + } + + var notification = new ApplyAuthorizationResponseContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that specify the unsupported request parameter. + /// + public class ValidateRequestParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Reject requests using the unsupported request parameter. + if (!string.IsNullOrEmpty(context.Request.Request)) + { + context.Logger.LogError("The authorization request was rejected because it contained " + + "an unsupported parameter: {Parameter}.", "request"); + + context.Reject( + error: Errors.RequestNotSupported, + description: "The 'request' parameter is not supported."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that specify the unsupported request_uri parameter. + /// + public class ValidateRequestUriParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateRequestParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Reject requests using the unsupported request_uri parameter. + if (!string.IsNullOrEmpty(context.Request.RequestUri)) + { + context.Logger.LogError("The authorization request was rejected because it contained " + + "an unsupported parameter: {Parameter}.", "request_uri"); + + context.Reject( + error: Errors.RequestUriNotSupported, + description: "The 'request_uri' parameter is not supported."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that lack the mandatory client_id parameter. + /// + public class ValidateClientIdParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateRequestUriParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // client_id is a required parameter and MUST cause an error when missing. + // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest. + if (string.IsNullOrEmpty(context.ClientId)) + { + context.Logger.LogError("The authorization request was rejected because " + + "the mandatory 'client_id' parameter was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'client_id' parameter is missing."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that lack the mandatory redirect_uri parameter. + /// + public class ValidateRedirectUriParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateClientIdParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // While redirect_uri was not mandatory in OAuth 2.0, this parameter + // is now declared as REQUIRED and MUST cause an error when missing. + // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest. + // To keep OpenIddict compatible with pure OAuth 2.0 clients, an error + // is only returned if the request was made by an OpenID Connect client. + if (string.IsNullOrEmpty(context.RedirectUri)) + { + if (context.Request.HasScope(Scopes.OpenId)) + { + context.Logger.LogError("The authorization request was rejected because " + + "the mandatory 'redirect_uri' parameter was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'redirect_uri' parameter is missing."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + + // Note: when specified, redirect_uri MUST be an absolute URI. + // See http://tools.ietf.org/html/rfc6749#section-3.1.2 + // and http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest. + // + // Note: on Linux/macOS, "/path" URLs are treated as valid absolute file URLs. + // To ensure relative redirect_uris are correctly rejected on these platforms, + // an additional check using IsWellFormedOriginalString() is made here. + // See https://github.com/dotnet/corefx/issues/22098 for more information. + if (!Uri.TryCreate(context.RedirectUri, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString()) + { + context.Logger.LogError("The authorization request was rejected because the 'redirect_uri' parameter " + + "didn't correspond to a valid absolute URL: {RedirectUri}.", context.RedirectUri); + + context.Reject( + error: Errors.InvalidRequest, + description: "The 'redirect_uri' parameter must be a valid absolute URL."); + + return Task.CompletedTask; + } + + // Note: when specified, redirect_uri MUST NOT include a fragment component. + // See http://tools.ietf.org/html/rfc6749#section-3.1.2 + // and http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + if (!string.IsNullOrEmpty(uri.Fragment)) + { + context.Logger.LogError("The authorization request was rejected because the 'redirect_uri' " + + "contained a URL fragment: {RedirectUri}.", context.RedirectUri); + + context.Reject( + error: Errors.InvalidRequest, + description: "The 'redirect_uri' parameter must not include a fragment."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that specify an invalid response_type parameter. + /// + public class ValidateResponseTypeParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateRedirectUriParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Reject requests missing the mandatory response_type parameter. + if (string.IsNullOrEmpty(context.Request.ResponseType)) + { + context.Logger.LogError("The authorization request was rejected because " + + "the mandatory 'response_type' parameter was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'response_type' parameter is missing."); + + return Task.CompletedTask; + } + + // Reject requests containing the id_token response_type if no openid scope has been received. + if (context.Request.HasResponseType(ResponseTypes.IdToken) && !context.Request.HasScope(Scopes.OpenId)) + { + context.Logger.LogError("The authorization request was rejected because the 'openid' scope was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'openid' scope is missing."); + + return Task.CompletedTask; + } + + // Reject requests containing the code response_type if the token endpoint has been disabled. + if (context.Request.HasResponseType(ResponseTypes.Code) && context.Options.TokenEndpointUris.Count == 0) + { + context.Logger.LogError("The authorization request was rejected because the authorization code flow was disabled."); + + context.Reject( + error: Errors.UnsupportedResponseType, + description: "The specified 'response_type' is not supported by this server."); + + return Task.CompletedTask; + } + + // Reject requests that specify an unsupported response_type. + if (!context.Request.IsAuthorizationCodeFlow() && !context.Request.IsHybridFlow() && !context.Request.IsImplicitFlow()) + { + context.Logger.LogError("The authorization request was rejected because the '{ResponseType}' " + + "response type is not supported.", context.Request.ResponseType); + + context.Reject( + error: Errors.UnsupportedResponseType, + description: "The specified 'response_type' parameter is not supported."); + + return Task.CompletedTask; + } + + // Reject code flow authorization requests if the authorization code flow is not enabled. + if (context.Request.IsAuthorizationCodeFlow() && !context.Options.GrantTypes.Contains(GrantTypes.AuthorizationCode)) + { + context.Logger.LogError("The authorization request was rejected because " + + "the authorization code flow was not enabled."); + + context.Reject( + error: Errors.UnsupportedResponseType, + description: "The specified 'response_type' parameter is not allowed."); + + return Task.CompletedTask; + } + + // Reject implicit flow authorization requests if the implicit flow is not enabled. + if (context.Request.IsImplicitFlow() && !context.Options.GrantTypes.Contains(GrantTypes.Implicit)) + { + context.Logger.LogError("The authorization request was rejected because the implicit flow was not enabled."); + + context.Reject( + error: Errors.UnsupportedResponseType, + description: "The specified 'response_type' parameter is not allowed."); + + return Task.CompletedTask; + } + + // Reject hybrid flow authorization requests if the authorization code or the implicit flows are not enabled. + if (context.Request.IsHybridFlow() && (!context.Options.GrantTypes.Contains(GrantTypes.AuthorizationCode) || + !context.Options.GrantTypes.Contains(GrantTypes.Implicit))) + { + context.Logger.LogError("The authorization request was rejected because the " + + "authorization code flow or the implicit flow was not enabled."); + + context.Reject( + error: Errors.UnsupportedResponseType, + description: "The specified 'response_type' parameter is not allowed."); + + return Task.CompletedTask; + } + + // Reject authorization requests that specify scope=offline_access if the refresh token flow is not enabled. + if (context.Request.HasScope(Scopes.OfflineAccess) && !context.Options.GrantTypes.Contains(GrantTypes.RefreshToken)) + { + context.Reject( + error: Errors.InvalidRequest, + description: "The 'offline_access' scope is not allowed."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that specify an invalid response_mode parameter. + /// + public class ValidateResponseModeParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateResponseTypeParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // 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 (context.Request.IsQueryResponseMode() && (context.Request.HasResponseType(ResponseTypes.IdToken) || + context.Request.HasResponseType(ResponseTypes.Token))) + { + context.Logger.LogError("The authorization request was rejected because the 'response_type'/'response_mode' " + + "combination was invalid: {ResponseType} ; {ResponseMode}.", + context.Request.ResponseType, context.Request.ResponseMode); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'response_type'/'response_mode' combination is invalid."); + + return Task.CompletedTask; + } + + // Reject requests that specify an unsupported response_mode. + if (!string.IsNullOrEmpty(context.Request.ResponseMode) && !context.Request.IsFormPostResponseMode() && + !context.Request.IsFragmentResponseMode() && + !context.Request.IsQueryResponseMode()) + { + context.Logger.LogError("The authorization request was rejected because the '{ResponseMode}' " + + "response mode is not supported.", context.Request.ResponseMode); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'response_mode' parameter is not supported."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that don't specify a nonce. + /// + public class ValidateNonceParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateResponseModeParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Reject OpenID Connect implicit/hybrid requests missing the mandatory nonce parameter. + // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest, + // http://openid.net/specs/openid-connect-implicit-1_0.html#RequestParameters + // and http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken. + + if (!string.IsNullOrEmpty(context.Request.Nonce) || !context.Request.HasScope(Scopes.OpenId)) + { + return Task.CompletedTask; + } + + if (context.Request.IsImplicitFlow() || context.Request.IsHybridFlow()) + { + context.Logger.LogError("The authorization request was rejected because the mandatory 'nonce' parameter was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'nonce' parameter is missing."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that don't specify a valid prompt parameter. + /// + public class ValidatePromptParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateNonceParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Reject requests specifying prompt=none with consent/login or select_account. + if (context.Request.HasPrompt(Prompts.None) && (context.Request.HasPrompt(Prompts.Consent) || + context.Request.HasPrompt(Prompts.Login) || + context.Request.HasPrompt(Prompts.SelectAccount))) + { + context.Logger.LogError("The authorization request was rejected because an invalid prompt parameter was specified."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'prompt' parameter is invalid."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that don't specify valid code challenge parameters. + /// + public class ValidateCodeChallengeParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidatePromptParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.Request.CodeChallenge) && + string.IsNullOrEmpty(context.Request.CodeChallengeMethod)) + { + return Task.CompletedTask; + } + + // Ensure a code_challenge was specified if a code_challenge_method was used. + if (string.IsNullOrEmpty(context.Request.CodeChallenge)) + { + context.Logger.LogError("The authorization request was rejected because the code_challenge was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The 'code_challenge_method' parameter cannot be used without 'code_challenge'."); + + return Task.CompletedTask; + } + + // When code_challenge or code_challenge_method is specified, ensure the response_type includes "code". + if (!context.Request.HasResponseType(ResponseTypes.Code)) + { + context.Logger.LogError("The authorization request was rejected because the response type " + + "was not compatible with 'code_challenge'/'code_challenge_method'."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The 'code_challenge' and 'code_challenge_method' parameters " + + "can only be used with a response type containing 'code'."); + + return Task.CompletedTask; + } + + // Reject authorization requests that contain response_type=token when a code_challenge is specified. + if (context.Request.HasResponseType(ResponseTypes.Token)) + { + context.Logger.LogError("The authorization request was rejected because the " + + "specified response type was not compatible with PKCE."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'response_type' parameter is not allowed when using PKCE."); + + return Task.CompletedTask; + } + + // If a code_challenge_method was specified, ensure the algorithm is supported. + if (!string.IsNullOrEmpty(context.Request.CodeChallengeMethod) && + !string.Equals(context.Request.CodeChallengeMethod, CodeChallengeMethods.Plain, StringComparison.Ordinal) && + !string.Equals(context.Request.CodeChallengeMethod, CodeChallengeMethods.Sha256, StringComparison.Ordinal)) + { + context.Logger.LogError("The authorization request was rejected because " + + "the specified code challenge method was not supported."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified code_challenge_method is not supported'."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that use unregistered scopes. + /// Note: this handler is not used when the degraded mode is enabled or when scope validation is disabled. + /// + public class ValidateScopes : IOpenIddictServerHandler + { + private readonly IOpenIddictScopeManager _scopeManager; + + public ValidateScopes() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateScopes([NotNull] IOpenIddictScopeManager scopeManager) + => _scopeManager = scopeManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateCodeChallengeParameters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If all the specified scopes are registered in the options, avoid making a database lookup. + var scopes = context.Request.GetScopes().Except(context.Options.Scopes); + if (scopes.Count != 0) + { + foreach (var scope in await _scopeManager.FindByNamesAsync(scopes.ToImmutableArray())) + { + scopes = scopes.Remove(await _scopeManager.GetNameAsync(scope)); + } + } + + // If at least one scope was not recognized, return an error. + if (scopes.Count != 0) + { + context.Logger.LogError("The authentication request was rejected because " + + "invalid scopes were specified: {Scopes}.", scopes); + + context.Reject( + error: Errors.InvalidScope, + description: "The specified 'scope' parameter is not valid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that use an invalid client_id. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientId : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientId() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateClientId([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateScopes.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + context.Logger.LogError("The authorization request was rejected because the client " + + "application was not found: '{ClientId}'.", context.ClientId); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'client_id' parameter is invalid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests + /// that use a response_type incompatible with the client application. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientType : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientType() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateClientType([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientId.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // To prevent downgrade attacks, ensure that authorization requests returning an access token directly + // from the authorization endpoint are rejected if the client_id corresponds to a confidential application. + // Note: when using the authorization code grant, the ValidateClientSecret handler is responsible of rejecting + // the token request if the client_id corresponds to an unauthenticated confidential client. + if (context.Request.HasResponseType(ResponseTypes.Token) && await _applicationManager.IsConfidentialAsync(application)) + { + context.Logger.LogError("The authorization request was rejected because the confidential application '{ClientId}' " + + "was not allowed to retrieve an access token from the authorization endpoint.", context.ClientId); + + context.Reject( + error: Errors.UnauthorizedClient, + description: "The specified 'response_type' parameter is not valid for this client application."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that use an invalid redirect_uri. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientRedirectUri : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientRedirectUri() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateClientRedirectUri([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientType.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // Ensure that the specified redirect_uri is valid and is associated with the client application. + if (!await _applicationManager.ValidateRedirectUriAsync(application, context.RedirectUri)) + { + context.Logger.LogError("The authorization request was rejected because the redirect_uri " + + "was invalid: '{RedirectUri}'.", context.RedirectUri); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'redirect_uri' parameter is not valid for this client application."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests made by unauthorized applications. + /// Note: this handler is not used when the degraded mode is enabled or when endpoint permissions are disabled. + /// + public class ValidateEndpointPermissions : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateEndpointPermissions() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateEndpointPermissions([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientRedirectUri.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // Reject the request if the application is not allowed to use the authorization endpoint. + if (!await _applicationManager.HasPermissionAsync(application, Permissions.Endpoints.Authorization)) + { + context.Logger.LogError("The authorization request was rejected because the application '{ClientId}' " + + "was not allowed to use the authorization endpoint.", context.ClientId); + + context.Reject( + error: Errors.UnauthorizedClient, + description: "This client application is not allowed to use the authorization endpoint."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests made by unauthorized applications. + /// Note: this handler is not used when the degraded mode is enabled or when grant type permissions are disabled. + /// + public class ValidateGrantTypePermissions : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateGrantTypePermissions() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateGrantTypePermissions([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // Reject the request if the application is not allowed to use the authorization code flow. + if (context.Request.IsAuthorizationCodeFlow() && + !await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.AuthorizationCode)) + { + context.Logger.LogError("The authorization request was rejected because the application '{ClientId}' " + + "was not allowed to use the authorization code flow.", context.ClientId); + + context.Reject( + error: Errors.UnauthorizedClient, + description: "The client application is not allowed to use the authorization code flow."); + + return; + } + + // Reject the request if the application is not allowed to use the implicit flow. + if (context.Request.IsImplicitFlow() && + !await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.Implicit)) + { + context.Logger.LogError("The authorization request was rejected because the application '{ClientId}' " + + "was not allowed to use the implicit flow.", context.ClientId); + + context.Reject( + error: Errors.UnauthorizedClient, + description: "The client application is not allowed to use the implicit flow."); + + return; + } + + // Reject the request if the application is not allowed to use the authorization code/implicit flows. + if (context.Request.IsHybridFlow() && + (!await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.AuthorizationCode) || + !await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.Implicit))) + { + context.Logger.LogError("The authorization request was rejected because the application '{ClientId}' " + + "was not allowed to use the hybrid flow.", context.ClientId); + + context.Reject( + error: Errors.UnauthorizedClient, + description: "The client application is not allowed to use the hybrid flow."); + + return; + } + + // Reject the request if the offline_access scope was request and if + // the application is not allowed to use the refresh token grant type. + if (context.Request.HasScope(Scopes.OfflineAccess) && + !await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.RefreshToken)) + { + context.Logger.LogError("The authorization request was rejected because the application '{ClientId}' " + + "was not allowed to request the 'offline_access' scope.", context.ClientId); + + context.Reject( + error: Errors.InvalidRequest, + description: "The client application is not allowed to use the 'offline_access' scope."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests made by unauthorized applications. + /// Note: this handler is not used when the degraded mode is enabled or when scope permissions are disabled. + /// + public class ValidateScopePermissions : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateScopePermissions() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateScopePermissions([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateAuthorizationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + foreach (var scope in context.Request.GetScopes()) + { + // Avoid validating the "openid" and "offline_access" scopes as they represent protocol scopes. + if (string.Equals(scope, Scopes.OfflineAccess, StringComparison.Ordinal) || + string.Equals(scope, Scopes.OpenId, StringComparison.Ordinal)) + { + continue; + } + + // Reject the request if the application is not allowed to use the iterated scope. + if (!await _applicationManager.HasPermissionAsync(application, Permissions.Prefixes.Scope + scope)) + { + context.Logger.LogError("The authorization request was rejected because the application '{ClientId}' " + + "was not allowed to use the scope {Scope}.", context.ClientId, scope); + + context.Reject( + error: Errors.InvalidRequest, + description: "This client application is not allowed to use the specified scope."); + + return; + } + } + } + } + + /// + /// Contains the logic responsible of inferring the redirect URL + /// used to send the response back to the client application. + /// + public class AttachRedirectUri : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Request == null) + { + return Task.CompletedTask; + } + + // Note: at this stage, the validated redirect URI property may be null (e.g if an error + // is returned from the ExtractAuthorizationRequest/ValidateAuthorizationRequest events). + if (context.Transaction.Properties.TryGetValue(Properties.ValidatedRedirectUri, out var property)) + { + context.RedirectUri = (string) property; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of inferring the response mode + /// used to send the response back to the client application. + /// + public class InferResponseMode : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachRedirectUri.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Request == null) + { + return Task.CompletedTask; + } + + context.ResponseMode = context.Request.ResponseMode; + + // If the response_mode parameter was not specified, try to infer it. + if (string.IsNullOrEmpty(context.ResponseMode) && !string.IsNullOrEmpty(context.RedirectUri)) + { + context.ResponseMode = context.Request.IsFormPostResponseMode() ? ResponseModes.FormPost : + context.Request.IsFragmentResponseMode() ? ResponseModes.Fragment : + context.Request.IsQueryResponseMode() ? ResponseModes.Query : null; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching the state to the response. + /// + public class AttachResponseState : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(InferResponseMode.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyAuthorizationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Attach the request state to the authorization response. + if (string.IsNullOrEmpty(context.Response.State)) + { + context.Response.State = context.Request?.State; + } + + return Task.CompletedTask; + } + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs new file mode 100644 index 000000000..5589bf081 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs @@ -0,0 +1,1496 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerHandlers + { + public static class Discovery + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Configuration request top-level processing: + */ + ExtractConfigurationRequest.Descriptor, + ValidateConfigurationRequest.Descriptor, + HandleConfigurationRequest.Descriptor, + ApplyConfigurationResponse.Descriptor, + ApplyConfigurationResponse.Descriptor, + + /* + * Configuration request handling: + */ + AttachIssuer.Descriptor, + AttachEndpoints.Descriptor, + AttachGrantTypes.Descriptor, + AttachResponseModes.Descriptor, + AttachResponseTypes.Descriptor, + AttachClientAuthenticationMethods.Descriptor, + AttachCodeChallengeMethods.Descriptor, + AttachScopes.Descriptor, + AttachClaims.Descriptor, + AttachSubjectTypes.Descriptor, + AttachSigningAlgorithms.Descriptor, + AttachAdditionalMetadata.Descriptor, + + /* + * Cryptography request top-level processing: + */ + ExtractCryptographyRequest.Descriptor, + ValidateCryptographyRequest.Descriptor, + HandleCryptographyRequest.Descriptor, + ApplyCryptographyResponse.Descriptor, + ApplyCryptographyResponse.Descriptor, + + /* + * Cryptography request handling: + */ + AttachSigningKeys.Descriptor); + + /// + /// Contains the logic responsible of extracting configuration requests and invoking the corresponding event handlers. + /// + public class ExtractConfigurationRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ExtractConfigurationRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Configuration) + { + return; + } + + var notification = new ExtractConfigurationRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + if (notification.Request == null) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The configuration request was not correctly extracted. To extract configuration requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .ToString()); + } + + context.Logger.LogInformation("The configuration request was successfully extracted: {Request}.", notification.Request); + } + } + + /// + /// Contains the logic responsible of validating configuration requests and invoking the corresponding event handlers. + /// + public class ValidateConfigurationRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateConfigurationRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ExtractConfigurationRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Configuration) + { + return; + } + + var notification = new ValidateConfigurationRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + context.Logger.LogInformation("The configuration request was successfully validated."); + } + } + + /// + /// Contains the logic responsible of handling configuration requests and invoking the corresponding event handlers. + /// + public class HandleConfigurationRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public HandleConfigurationRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ValidateConfigurationRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Configuration) + { + return; + } + + var notification = new HandleConfigurationRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + var response = new OpenIddictResponse + { + [Metadata.Issuer] = notification.Issuer?.AbsoluteUri, + [Metadata.AuthorizationEndpoint] = notification.AuthorizationEndpoint?.AbsoluteUri, + [Metadata.TokenEndpoint] = notification.TokenEndpoint?.AbsoluteUri, + [Metadata.IntrospectionEndpoint] = notification.IntrospectionEndpoint?.AbsoluteUri, + [Metadata.EndSessionEndpoint] = notification.LogoutEndpoint?.AbsoluteUri, + [Metadata.RevocationEndpoint] = notification.RevocationEndpoint?.AbsoluteUri, + [Metadata.UserinfoEndpoint] = notification.UserinfoEndpoint?.AbsoluteUri, + [Metadata.JwksUri] = notification.CryptographyEndpoint?.AbsoluteUri, + [Metadata.GrantTypesSupported] = notification.GrantTypes.ToArray(), + [Metadata.ResponseTypesSupported] = notification.ResponseTypes.ToArray(), + [Metadata.ResponseModesSupported] = notification.ResponseModes.ToArray(), + [Metadata.ScopesSupported] = notification.Scopes.ToArray(), + [Metadata.ClaimsSupported] = notification.Claims.ToArray(), + [Metadata.IdTokenSigningAlgValuesSupported] = notification.IdTokenSigningAlgorithms.ToArray(), + [Metadata.CodeChallengeMethodsSupported] = notification.CodeChallengeMethods.ToArray(), + [Metadata.SubjectTypesSupported] = notification.SubjectTypes.ToArray(), + [Metadata.TokenEndpointAuthMethodsSupported] = notification.TokenEndpointAuthenticationMethods.ToArray(), + [Metadata.IntrospectionEndpointAuthMethodsSupported] = notification.IntrospectionEndpointAuthenticationMethods.ToArray(), + [Metadata.RevocationEndpointAuthMethodsSupported] = notification.RevocationEndpointAuthenticationMethods.ToArray() + }; + + foreach (var metadata in notification.Metadata) + { + response.SetParameter(metadata.Key, metadata.Value); + } + + context.Response = response; + } + } + + /// + /// Contains the logic responsible of processing configuration responses and invoking the corresponding event handlers. + /// + public class ApplyConfigurationResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + private readonly IOpenIddictServerProvider _provider; + + public ApplyConfigurationResponse([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Configuration) + { + return; + } + + var notification = new ApplyConfigurationResponseContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + } + + /// + /// Contains the logic responsible of attaching the issuer URL to the provider discovery document. + /// + public class AttachIssuer : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Options.Issuer != null) + { + context.Issuer = context.Options.Issuer; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching the endpoint URLs to the provider discovery document. + /// + public class AttachEndpoints : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachIssuer.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: while OpenIddict allows specifying multiple endpoint addresses, the OAuth 2.0 + // and OpenID Connect discovery specifications only allow a single address per endpoint. + context.AuthorizationEndpoint ??= context.Options.AuthorizationEndpointUris.FirstOrDefault(); + context.CryptographyEndpoint ??= context.Options.CryptographyEndpointUris.FirstOrDefault(); + context.IntrospectionEndpoint ??= context.Options.IntrospectionEndpointUris.FirstOrDefault(); + context.LogoutEndpoint ??= context.Options.LogoutEndpointUris.FirstOrDefault(); + context.RevocationEndpoint ??= context.Options.RevocationEndpointUris.FirstOrDefault(); + context.TokenEndpoint ??= context.Options.TokenEndpointUris.FirstOrDefault(); + context.UserinfoEndpoint ??= context.Options.UserinfoEndpointUris.FirstOrDefault(); + + // Note: this handler doesn't have any access to the request context. As such, it depends + // on another handler to determine the issuer location from the ambient request if it was not + // explicitly set in the server options. If the issuer is not set, an exception is thrown. + if (context.AuthorizationEndpoint != null && !context.AuthorizationEndpoint.IsAbsoluteUri) + { + if (context.Issuer == null || !context.Issuer.IsAbsoluteUri) + { + throw new InvalidOperationException("An absolute URL cannot be built for the authorization endpoint path."); + } + + context.AuthorizationEndpoint = new Uri(context.Issuer, context.AuthorizationEndpoint); + } + + if (context.CryptographyEndpoint != null && !context.CryptographyEndpoint.IsAbsoluteUri) + { + if (context.Issuer == null || !context.Issuer.IsAbsoluteUri) + { + throw new InvalidOperationException("An absolute URL cannot be built for the cryptography endpoint path."); + } + + context.CryptographyEndpoint = new Uri(context.Issuer, context.CryptographyEndpoint); + } + + if (context.IntrospectionEndpoint != null && !context.IntrospectionEndpoint.IsAbsoluteUri) + { + if (context.Issuer == null || !context.Issuer.IsAbsoluteUri) + { + throw new InvalidOperationException("An absolute URL cannot be built for the introspection endpoint path."); + } + + context.IntrospectionEndpoint = new Uri(context.Issuer, context.IntrospectionEndpoint); + } + + if (context.LogoutEndpoint != null && !context.LogoutEndpoint.IsAbsoluteUri) + { + if (context.Issuer == null || !context.Issuer.IsAbsoluteUri) + { + throw new InvalidOperationException("An absolute URL cannot be built for the logout endpoint path."); + } + + context.LogoutEndpoint = new Uri(context.Issuer, context.LogoutEndpoint); + } + + if (context.RevocationEndpoint != null && !context.RevocationEndpoint.IsAbsoluteUri) + { + if (context.Issuer == null || !context.Issuer.IsAbsoluteUri) + { + throw new InvalidOperationException("An absolute URL cannot be built for the revocation endpoint path."); + } + + context.RevocationEndpoint = new Uri(context.Issuer, context.RevocationEndpoint); + } + + if (context.TokenEndpoint != null && !context.TokenEndpoint.IsAbsoluteUri) + { + if (context.Issuer == null || !context.Issuer.IsAbsoluteUri) + { + throw new InvalidOperationException("An absolute URL cannot be built for the token endpoint path."); + } + + context.TokenEndpoint = new Uri(context.Issuer, context.TokenEndpoint); + } + + if (context.UserinfoEndpoint != null && !context.UserinfoEndpoint.IsAbsoluteUri) + { + if (context.Issuer == null || !context.Issuer.IsAbsoluteUri) + { + throw new InvalidOperationException("An absolute URL cannot be built for the userinfo endpoint path."); + } + + context.UserinfoEndpoint = new Uri(context.Issuer, context.UserinfoEndpoint); + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching the supported grant types to the provider discovery document. + /// + public class AttachGrantTypes : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachEndpoints.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Only populate grant_type_supported if the authorization or token endpoints are enabled. + if (context.AuthorizationEndpoint != null || context.TokenEndpoint != null) + { + context.GrantTypes.UnionWith(context.Options.GrantTypes); + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching the supported response modes to the provider discovery document. + /// + public class AttachResponseModes : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachGrantTypes.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Only populate response_modes_supported if the authorization endpoint is enabled. + if (context.AuthorizationEndpoint != null) + { + context.ResponseModes.Add(ResponseModes.FormPost); + context.ResponseModes.Add(ResponseModes.Fragment); + context.ResponseModes.Add(ResponseModes.Query); + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching the supported response types to the provider discovery document. + /// + public class AttachResponseTypes : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachResponseModes.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.GrantTypes.Contains(GrantTypes.AuthorizationCode)) + { + context.ResponseTypes.Add(ResponseTypes.Code); + } + + if (context.GrantTypes.Contains(GrantTypes.AuthorizationCode) && + context.GrantTypes.Contains(GrantTypes.Implicit)) + { + context.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken); + context.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token); + context.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.Token); + } + + if (context.GrantTypes.Contains(GrantTypes.Implicit)) + { + context.ResponseTypes.Add(ResponseTypes.IdToken); + context.ResponseTypes.Add(ResponseTypes.IdToken + ' ' + ResponseTypes.Token); + context.ResponseTypes.Add(ResponseTypes.Token); + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching the supported client + /// authentication methods to the provider discovery document. + /// + public class AttachClientAuthenticationMethods : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachResponseTypes.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.IntrospectionEndpoint != null) + { + context.IntrospectionEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic); + context.IntrospectionEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost); + } + + if (context.RevocationEndpoint != null) + { + context.RevocationEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic); + context.RevocationEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost); + } + + if (context.TokenEndpoint != null) + { + context.TokenEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic); + context.TokenEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost); + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching the supported + /// code challenge methods to the provider discovery document. + /// + public class AttachCodeChallengeMethods : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachClientAuthenticationMethods.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Only populate code_challenge_methods_supported if both the authorization and token endpoints are enabled. + if (context.AuthorizationEndpoint != null && context.TokenEndpoint != null) + { + // Note: supporting S256 is mandatory for authorization servers that implement PKCE. + // See https://tools.ietf.org/html/rfc7636#section-4.2 for more information. + context.CodeChallengeMethods.Add(CodeChallengeMethods.Plain); + context.CodeChallengeMethods.Add(CodeChallengeMethods.Sha256); + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching the supported response types to the provider discovery document. + /// + public class AttachScopes : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachCodeChallengeMethods.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.Scopes.UnionWith(context.Options.Scopes); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching the supported claims to the provider discovery document. + /// + public class AttachClaims : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachScopes.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.Claims.UnionWith(context.Options.Claims); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching the supported subject types to the provider discovery document. + /// + public class AttachSubjectTypes : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachClaims.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.SubjectTypes.Add(SubjectTypes.Public); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching the supported signing algorithms to the provider discovery document. + /// + public class AttachSigningAlgorithms : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachSubjectTypes.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + foreach (var credentials in context.Options.SigningCredentials) + { + // Try to resolve the JWA algorithm short name. + var algorithm = credentials.Digest switch + { + SecurityAlgorithms.Sha256 => SecurityAlgorithms.Sha256, + SecurityAlgorithms.Sha384 => SecurityAlgorithms.Sha384, + SecurityAlgorithms.Sha512 => SecurityAlgorithms.Sha512, + SecurityAlgorithms.Sha256Digest => SecurityAlgorithms.Sha256, + SecurityAlgorithms.Sha384Digest => SecurityAlgorithms.Sha384, + SecurityAlgorithms.Sha512Digest => SecurityAlgorithms.Sha512, + + // If the digest algorithm was not explicitly set or was not recognized, + // try to infer the digest algorithm from the specified signature algorithm. + _ => credentials.Algorithm switch + { + +#if SUPPORTS_ECDSA + SecurityAlgorithms.EcdsaSha256 => SecurityAlgorithms.Sha256, + SecurityAlgorithms.EcdsaSha384 => SecurityAlgorithms.Sha384, + SecurityAlgorithms.EcdsaSha512 => SecurityAlgorithms.Sha512, + SecurityAlgorithms.EcdsaSha256Signature => SecurityAlgorithms.Sha256, + SecurityAlgorithms.EcdsaSha384Signature => SecurityAlgorithms.Sha384, + SecurityAlgorithms.EcdsaSha512Signature => SecurityAlgorithms.Sha512, +#endif + SecurityAlgorithms.HmacSha256 => SecurityAlgorithms.Sha256, + SecurityAlgorithms.HmacSha384 => SecurityAlgorithms.Sha384, + SecurityAlgorithms.HmacSha512 => SecurityAlgorithms.Sha512, + SecurityAlgorithms.HmacSha256Signature => SecurityAlgorithms.Sha256, + SecurityAlgorithms.HmacSha384Signature => SecurityAlgorithms.Sha384, + SecurityAlgorithms.HmacSha512Signature => SecurityAlgorithms.Sha512, + + SecurityAlgorithms.RsaSha256 => SecurityAlgorithms.Sha256, + SecurityAlgorithms.RsaSha384 => SecurityAlgorithms.Sha384, + SecurityAlgorithms.RsaSha512 => SecurityAlgorithms.Sha512, + SecurityAlgorithms.RsaSha256Signature => SecurityAlgorithms.Sha256, + SecurityAlgorithms.RsaSha384Signature => SecurityAlgorithms.Sha384, + SecurityAlgorithms.RsaSha512Signature => SecurityAlgorithms.Sha512, + + _ => null + } + }; + + // If the algorithm cannot be resolved, ignore it. + if (string.IsNullOrEmpty(algorithm)) + { + continue; + } + + context.IdTokenSigningAlgorithms.Add(algorithm); + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching additional metadata to the provider discovery document. + /// + public class AttachAdditionalMetadata : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachSigningAlgorithms.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleConfigurationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the optional claims/request/request_uri parameters are not yet supported + // by OpenIddict, so "false" is returned to encourage clients not to use them. + context.Metadata[Metadata.ClaimsParameterSupported] = false; + context.Metadata[Metadata.RequestParameterSupported] = false; + context.Metadata[Metadata.RequestUriParameterSupported] = false; + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of extracting cryptography requests and invoking the corresponding event handlers. + /// + public class ExtractCryptographyRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ExtractCryptographyRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Cryptography) + { + return; + } + + var notification = new ExtractCryptographyRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + if (notification.Request == null) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The cryptography request was not correctly extracted. To extract configuration requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .ToString()); + } + + context.Logger.LogInformation("The cryptography request was successfully extracted: {Request}.", notification.Request); + } + } + + /// + /// Contains the logic responsible of validating cryptography requests and invoking the corresponding event handlers. + /// + public class ValidateCryptographyRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateCryptographyRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ExtractCryptographyRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Cryptography) + { + return; + } + + var notification = new ValidateCryptographyRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + context.Logger.LogInformation("The cryptography request was successfully validated."); + } + } + + /// + /// Contains the logic responsible of handling cryptography requests and invoking the corresponding event handlers. + /// + public class HandleCryptographyRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public HandleCryptographyRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ValidateCryptographyRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Cryptography) + { + return; + } + + var notification = new HandleCryptographyRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + var keys = new JArray(); + + foreach (var key in notification.Keys) + { + var item = new JObject(); + + // Ensure a key type has been provided. + // See https://tools.ietf.org/html/rfc7517#section-4.1 + if (string.IsNullOrEmpty(key.Kty)) + { + context.Logger.LogError("A JSON Web Key was excluded from the key set because " + + "it didn't contain the mandatory 'kid' parameter."); + + continue; + } + + // Create a dictionary associating the + // JsonWebKey components with their values. + var parameters = new Dictionary + { + [JsonWebKeyParameterNames.Kid] = key.Kid, + [JsonWebKeyParameterNames.Use] = key.Use, + [JsonWebKeyParameterNames.Kty] = key.Kty, + [JsonWebKeyParameterNames.Alg] = key.Alg, + [JsonWebKeyParameterNames.Crv] = key.Crv, + [JsonWebKeyParameterNames.E] = key.E, + [JsonWebKeyParameterNames.N] = key.N, + [JsonWebKeyParameterNames.X] = key.X, + [JsonWebKeyParameterNames.Y] = key.Y, + [JsonWebKeyParameterNames.X5t] = key.X5t, + [JsonWebKeyParameterNames.X5u] = key.X5u + }; + + foreach (var parameter in parameters) + { + if (!string.IsNullOrEmpty(parameter.Value)) + { + item.Add(parameter.Key, parameter.Value); + } + } + + if (key.KeyOps.Count != 0) + { + item.Add(JsonWebKeyParameterNames.KeyOps, new JArray(key.KeyOps)); + } + + if (key.X5c.Count != 0) + { + item.Add(JsonWebKeyParameterNames.X5c, new JArray(key.X5c)); + } + + keys.Add(item); + } + + // Note: AddParameter() is used here to ensure the mandatory "keys" node + // is returned to the caller, even if the key set doesn't expose any key. + // See https://tools.ietf.org/html/rfc7517#section-5 for more information. + var response = new OpenIddictResponse(); + response.AddParameter(Parameters.Keys, keys); + + context.Response = response; + } + } + + /// + /// Contains the logic responsible of processing cryptography responses and invoking the corresponding event handlers. + /// + public class ApplyCryptographyResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + private readonly IOpenIddictServerProvider _provider; + + public ApplyCryptographyResponse([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Cryptography) + { + return; + } + + var notification = new ApplyCryptographyResponseContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + } + + /// + /// Contains the logic responsible of attaching the signing keys to the JWKS document. + /// + public class AttachSigningKeys : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleCryptographyRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + foreach (var credentials in context.Options.SigningCredentials) + { +#if SUPPORTS_ECDSA + if (!IsAlgorithmSupported(credentials.Key, SecurityAlgorithms.RsaSha256) && + !IsAlgorithmSupported(credentials.Key, SecurityAlgorithms.EcdsaSha256) && + !IsAlgorithmSupported(credentials.Key, SecurityAlgorithms.EcdsaSha384) && + !IsAlgorithmSupported(credentials.Key, SecurityAlgorithms.EcdsaSha512)) + { + context.Logger.LogInformation("An unsupported signing key of type '{Type}' was ignored and excluded " + + "from the key set. Only RSA and ECDSA asymmetric security keys can be " + + "exposed via the JWKS endpoint.", credentials.Key.GetType().Name); + + continue; + } +#else + if (!IsAlgorithmSupported(credentials.Key, SecurityAlgorithms.RsaSha256)) + { + context.Logger.LogInformation("An unsupported signing key of type '{Type}' was ignored and excluded " + + "from the key set. Only RSA asymmetric security keys can be exposed " + + "via the JWKS endpoint.", credentials.Key.GetType().Name); + + continue; + } +#endif + + var key = new JsonWebKey + { + Use = JsonWebKeyUseNames.Sig, + + // Resolve the JWA identifier from the algorithm specified in the credentials. + Alg = credentials.Algorithm switch + { +#if SUPPORTS_ECDSA + SecurityAlgorithms.EcdsaSha256 => SecurityAlgorithms.EcdsaSha256, + SecurityAlgorithms.EcdsaSha384 => SecurityAlgorithms.EcdsaSha384, + SecurityAlgorithms.EcdsaSha512 => SecurityAlgorithms.EcdsaSha512, + SecurityAlgorithms.EcdsaSha256Signature => SecurityAlgorithms.EcdsaSha256, + SecurityAlgorithms.EcdsaSha384Signature => SecurityAlgorithms.EcdsaSha384, + SecurityAlgorithms.EcdsaSha512Signature => SecurityAlgorithms.EcdsaSha512, +#endif + SecurityAlgorithms.RsaSha256 => SecurityAlgorithms.RsaSha256, + SecurityAlgorithms.RsaSha384 => SecurityAlgorithms.RsaSha384, + SecurityAlgorithms.RsaSha512 => SecurityAlgorithms.RsaSha512, + SecurityAlgorithms.RsaSha256Signature => SecurityAlgorithms.RsaSha256, + SecurityAlgorithms.RsaSha384Signature => SecurityAlgorithms.RsaSha384, + SecurityAlgorithms.RsaSha512Signature => SecurityAlgorithms.RsaSha512, + + _ => null + }, + + // Use the key identifier specified in the signing credentials. + Kid = credentials.Kid + }; + + if (IsAlgorithmSupported(credentials.Key, SecurityAlgorithms.RsaSha256)) + { + // Note: IdentityModel 5 doesn't expose a method allowing to retrieve the underlying algorithm + // from a generic asymmetric security key. To work around this limitation, try to cast + // the security key to the built-in IdentityModel types to extract the required RSA instance. + // See https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/395. + + var parameters = credentials.Key switch + { + X509SecurityKey x509SecurityKey when x509SecurityKey.PublicKey is RSA algorithm => + algorithm.ExportParameters(includePrivateParameters: false), + + RsaSecurityKey rsaSecurityKey when rsaSecurityKey.Rsa != null => + rsaSecurityKey.Rsa.ExportParameters(includePrivateParameters: false), + + RsaSecurityKey rsaSecurityKey => rsaSecurityKey.Parameters, + + _ => (RSAParameters?) null + }; + + if (parameters == null) + { + context.Logger.LogWarning("A signing key of type '{Type}' was ignored because its RSA public " + + "parameters couldn't be extracted.", credentials.Key.GetType().Name); + + continue; + } + + Debug.Assert(parameters.Value.Exponent != null && + parameters.Value.Modulus != null, + "RSA.ExportParameters() shouldn't return a null exponent/modulus."); + + key.Kty = JsonWebAlgorithmsKeyTypes.RSA; + + // Note: both E and N must be base64url-encoded. + // See https://tools.ietf.org/html/rfc7518#section-6.3.1.1 + key.E = Base64UrlEncoder.Encode(parameters.Value.Exponent); + key.N = Base64UrlEncoder.Encode(parameters.Value.Modulus); + } + +#if SUPPORTS_ECDSA + else if (IsAlgorithmSupported(credentials.Key, SecurityAlgorithms.EcdsaSha256) || + IsAlgorithmSupported(credentials.Key, SecurityAlgorithms.EcdsaSha384) || + IsAlgorithmSupported(credentials.Key, SecurityAlgorithms.EcdsaSha512)) + { + var parameters = credentials.Key switch + { + X509SecurityKey x509SecurityKey when x509SecurityKey.PublicKey is ECDsa algorithm => + algorithm.ExportParameters(includePrivateParameters: false), + + ECDsaSecurityKey ecdsaSecurityKey when ecdsaSecurityKey.ECDsa != null => + ecdsaSecurityKey.ECDsa.ExportParameters(includePrivateParameters: false), + + _ => (ECParameters?) null + }; + + if (parameters == null) + { + context.Logger.LogWarning("A signing key of type '{Type}' was ignored because its EC public " + + "parameters couldn't be extracted.", credentials.Key.GetType().Name); + + continue; + } + + Debug.Assert(parameters.Value.Q.X != null && + parameters.Value.Q.Y != null, + "ECDsa.ExportParameters() shouldn't return null coordinates."); + + Debug.Assert(parameters.Value.Curve.IsNamed, + "ECDsa.ExportParameters() shouldn't return an unnamed curve."); + + key.Kty = JsonWebAlgorithmsKeyTypes.EllipticCurve; + key.Crv = IsCurve(parameters.Value, ECCurve.NamedCurves.nistP256) ? JsonWebKeyECTypes.P256 : + IsCurve(parameters.Value, ECCurve.NamedCurves.nistP384) ? JsonWebKeyECTypes.P384 : + IsCurve(parameters.Value, ECCurve.NamedCurves.nistP521) ? JsonWebKeyECTypes.P521 : null; + + // Note: both X and Y must be base64url-encoded. + // See https://tools.ietf.org/html/rfc7518#section-6.2.1.2 + key.X = Base64UrlEncoder.Encode(parameters.Value.Q.X); + key.Y = Base64UrlEncoder.Encode(parameters.Value.Q.Y); + } +#endif + + // If the signing key is embedded in a X.509 certificate, set + // the x5t and x5c parameters using the certificate details. + var certificate = (credentials.Key as X509SecurityKey)?.Certificate; + if (certificate != null) + { + // x5t must be base64url-encoded. + // See https://tools.ietf.org/html/rfc7517#section-4.8 + key.X5t = Base64UrlEncoder.Encode(certificate.GetCertHash()); + + // x5t#S256 must be base64url-encoded. + // See https://tools.ietf.org/html/rfc7517#section-4.9 + key.X5tS256 = Base64UrlEncoder.Encode(GetCertificateHash(certificate, HashAlgorithmName.SHA256)); + + // Unlike E or N, the certificates contained in x5c + // must be base64-encoded and not base64url-encoded. + // See https://tools.ietf.org/html/rfc7517#section-4.7 + key.X5c.Add(Convert.ToBase64String(certificate.RawData)); + } + + context.Keys.Add(key); + } + + return Task.CompletedTask; + + static bool IsAlgorithmSupported(SecurityKey key, string algorithm) => + key.CryptoProviderFactory.IsSupportedAlgorithm(algorithm, key); + +#if SUPPORTS_ECDSA + static bool IsCurve(ECParameters parameters, ECCurve curve) => + string.Equals(parameters.Curve.Oid.FriendlyName, curve.Oid.FriendlyName, StringComparison.Ordinal); +#endif + + static byte[] GetCertificateHash(X509Certificate2 certificate, HashAlgorithmName algorithm) + { +#if SUPPORTS_CERTIFICATE_HASHING_WITH_SPECIFIED_ALGORITHM + return certificate.GetCertHash(algorithm); +#else + using var hash = CryptoConfig.CreateFromName(algorithm.Name) as HashAlgorithm; + if (hash == null || hash is KeyedHashAlgorithm) + { + throw new InvalidOperationException("The specified hash algorithm is not valid."); + } + + return hash.ComputeHash(certificate.RawData); +#endif + } + } + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs new file mode 100644 index 000000000..100dfca4a --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -0,0 +1,1752 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlerFilters; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerHandlers + { + public static class Exchange + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Token request top-level processing: + */ + ExtractTokenRequest.Descriptor, + ValidateTokenRequest.Descriptor, + HandleTokenRequest.Descriptor, + ApplyTokenResponse.Descriptor, + ApplyTokenResponse.Descriptor, + ApplyTokenResponse.Descriptor, + ApplyTokenResponse.Descriptor, + + /* + * Token request validation: + */ + ValidateGrantType.Descriptor, + ValidateClientIdParameter.Descriptor, + ValidateAuthorizationCodeParameter.Descriptor, + ValidateClientCredentialsParameters.Descriptor, + ValidateRefreshTokenParameter.Descriptor, + ValidatePasswordParameters.Descriptor, + ValidateScopes.Descriptor, + ValidateClientId.Descriptor, + ValidateClientType.Descriptor, + ValidateClientSecret.Descriptor, + ValidateEndpointPermissions.Descriptor, + ValidateGrantTypePermissions.Descriptor, + ValidateScopePermissions.Descriptor, + ValidateAuthorizationCode.Descriptor, + ValidateRefreshToken.Descriptor, + ValidatePresenters.Descriptor, + ValidateRedirectUri.Descriptor, + ValidateCodeVerifier.Descriptor, + ValidateGrantedScopes.Descriptor, + + /* + * Token request handling: + */ + AttachPrincipal.Descriptor); + + /// + /// Contains the logic responsible of extracting token requests and invoking the corresponding event handlers. + /// + public class ExtractTokenRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ExtractTokenRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Token) + { + return; + } + + var notification = new ExtractTokenRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + if (notification.Request == null) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The token request was not correctly extracted. To extract token requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .ToString()); + } + + context.Logger.LogInformation("The token request was successfully extracted: {Request}.", notification.Request); + } + } + + /// + /// Contains the logic responsible of validating token requests and invoking the corresponding event handlers. + /// + public class ValidateTokenRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateTokenRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ExtractTokenRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Token) + { + return; + } + + var notification = new ValidateTokenRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + // Store the security principal extracted from the authorization code/refresh token as an environment property. + context.Transaction.Properties[Properties.OriginalPrincipal] = notification.Principal; + + context.Logger.LogInformation("The token request was successfully validated."); + } + } + + /// + /// Contains the logic responsible of handling token requests and invoking the corresponding event handlers. + /// + public class HandleTokenRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public HandleTokenRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ValidateTokenRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Token) + { + return; + } + + var notification = new HandleTokenRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + if (notification.Principal != null) + { + var @event = new ProcessSigninResponseContext(context.Transaction) + { + Principal = notification.Principal, + Response = new OpenIddictResponse() + }; + + await _provider.DispatchAsync(@event); + + if (@event.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (@event.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The token request was not handled. To handle token requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .Append("Alternatively, enable the pass-through mode to handle them at a later stage.") + .ToString()); + } + } + + /// + /// Contains the logic responsible of processing sign-in responses and invoking the corresponding event handlers. + /// + public class ApplyTokenResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + private readonly IOpenIddictServerProvider _provider; + + public ApplyTokenResponse([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Token) + { + return; + } + + var notification = new ApplyTokenResponseContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting token requests that specify an invalid grant type. + /// + public class ValidateGrantType : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Reject token requests missing the mandatory grant_type parameter. + if (string.IsNullOrEmpty(context.Request.GrantType)) + { + context.Logger.LogError("The token request was rejected because the grant type was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'grant_type' parameter is missing."); + + return Task.CompletedTask; + } + + // Reject token requests that don't specify a supported grant type. + if (!context.Options.GrantTypes.Contains(context.Request.GrantType)) + { + context.Logger.LogError("The token request was rejected because the '{GrantType}' " + + "grant type is not supported.", context.Request.GrantType); + + context.Reject( + error: Errors.UnsupportedGrantType, + description: "The specified 'grant_type' parameter is not supported."); + + return Task.CompletedTask; + } + + // Reject token requests that specify scope=offline_access if the refresh token flow is not enabled. + if (context.Request.HasScope(Scopes.OfflineAccess) && + !context.Options.GrantTypes.Contains(GrantTypes.RefreshToken)) + { + context.Reject( + error: Errors.InvalidRequest, + description: "The 'offline_access' scope is not allowed."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting token requests that don't + /// specify a client identifier for the authorization code grant type. + /// + public class ValidateClientIdParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateGrantType.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!string.IsNullOrEmpty(context.ClientId)) + { + return Task.CompletedTask; + } + + // At this stage, reject the token request unless the client identification requirement was disabled. + // Independently of this setting, also reject grant_type=authorization_code requests that don't specify + // a client_id, as the client identifier MUST be sent by the client application in the request body + // if it cannot be inferred from the client authentication method (e.g the username when using basic). + // See https://tools.ietf.org/html/rfc6749#section-4.1.3 for more information. + if (!context.Options.AcceptAnonymousClients || context.Request.IsAuthorizationCodeGrantType()) + { + context.Logger.LogError("The token request was rejected because the mandatory 'client_id' was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'client_id' parameter is missing."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting token requests that don't + /// specify an authorization code for the authorization code grant type. + /// + public class ValidateAuthorizationCodeParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateClientIdParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Reject grant_type=authorization_code requests missing the authorization code. + // See https://tools.ietf.org/html/rfc6749#section-4.1.3 for more information. + if (context.Request.IsAuthorizationCodeGrantType() && string.IsNullOrEmpty(context.Request.Code)) + { + context.Logger.LogError("The token request was rejected because the authorization code was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'code' parameter is missing."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting token requests that don't + /// specify client credentials for the client credentials grant type. + /// + public class ValidateClientCredentialsParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateAuthorizationCodeParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Reject grant_type=client_credentials requests missing the client credentials. + // See https://tools.ietf.org/html/rfc6749#section-4.4.1 for more information. + if (context.Request.IsClientCredentialsGrantType() && (string.IsNullOrEmpty(context.Request.ClientId) || + string.IsNullOrEmpty(context.Request.ClientSecret))) + { + context.Reject( + error: Errors.InvalidRequest, + description: "The 'client_id' and 'client_secret' parameters are " + + "required when using the client credentials grant."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting token requests that + /// specify invalid parameters for the refresh token grant type. + /// + public class ValidateRefreshTokenParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateClientCredentialsParameters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Reject grant_type=refresh_token requests missing the refresh token. + // See https://tools.ietf.org/html/rfc6749#section-6 for more information. + if (context.Request.IsRefreshTokenGrantType() && string.IsNullOrEmpty(context.Request.RefreshToken)) + { + context.Logger.LogError("The token request was rejected because the refresh token was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'refresh_token' parameter is missing."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting token requests + /// that specify invalid parameters for the password grant type. + /// + public class ValidatePasswordParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateRefreshTokenParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Reject grant_type=password requests missing username or password. + // See https://tools.ietf.org/html/rfc6749#section-4.3.2 for more information. + if (context.Request.IsPasswordGrantType() && (string.IsNullOrEmpty(context.Request.Username) || + string.IsNullOrEmpty(context.Request.Password))) + { + context.Logger.LogError("The token request was rejected because the resource owner credentials were missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'username' and/or 'password' parameters are missing."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that use unregistered scopes. + /// Note: this handler is not used when the degraded mode is enabled or when scope validation is disabled. + /// + public class ValidateScopes : IOpenIddictServerHandler + { + private readonly IOpenIddictScopeManager _scopeManager; + + public ValidateScopes() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateScopes([NotNull] IOpenIddictScopeManager scopeManager) + => _scopeManager = scopeManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidatePasswordParameters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If all the specified scopes are registered in the options, avoid making a database lookup. + var scopes = context.Request.GetScopes().Except(context.Options.Scopes); + if (scopes.Count != 0) + { + foreach (var scope in await _scopeManager.FindByNamesAsync(scopes.ToImmutableArray())) + { + scopes = scopes.Remove(await _scopeManager.GetNameAsync(scope)); + } + } + + // If at least one scope was not recognized, return an error. + if (scopes.Count != 0) + { + context.Logger.LogError("The token request was rejected because " + + "invalid scopes were specified: {Scopes}.", scopes); + + context.Reject( + error: Errors.InvalidScope, + description: "The specified 'scope' parameter is not valid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting token requests that use an invalid client_id. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientId : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientId() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateClientId([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateScopes.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Retrieve the application details corresponding to the requested client_id. + // If no entity can be found, this likely indicates that the client_id is invalid. + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + context.Logger.LogError("The token request was rejected because the client " + + "application was not found: '{ClientId}'.", context.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: "The specified 'client_id' parameter is invalid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting token requests made by applications + /// whose client type is not compatible with the requested grant type. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientType : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientType() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateClientType([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientId.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + if (await _applicationManager.IsPublicAsync(application)) + { + // Public applications are not allowed to use the client credentials grant. + if (context.Request.IsClientCredentialsGrantType()) + { + context.Logger.LogError("The token request was rejected because the public client application '{ClientId}' " + + "was not allowed to use the client credentials grant.", context.Request.ClientId); + + context.Reject( + error: Errors.UnauthorizedClient, + description: "The specified 'grant_type' parameter is not valid for this client application."); + + return; + } + + // Reject token requests containing a client_secret when the client is a public application. + if (!string.IsNullOrEmpty(context.ClientSecret)) + { + context.Logger.LogError("The token request was rejected because the public application '{ClientId}' " + + "was not allowed to send a client secret.", context.ClientId); + + context.Reject( + error: Errors.InvalidRequest, + description: "The 'client_secret' parameter is not valid for this client application."); + + return; + } + + return; + } + + // Confidential and hybrid applications MUST authenticate to protect them from impersonation attacks. + if (string.IsNullOrEmpty(context.ClientSecret)) + { + context.Logger.LogError("The token request was rejected because the confidential or hybrid application " + + "'{ClientId}' didn't specify a client secret.", context.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: "The 'client_secret' parameter required for this client application is missing."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting token requests specifying an invalid client secret. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientSecret : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientSecret() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateClientSecret([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientType.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // If the application is not a public client, validate the client secret. + if (!await _applicationManager.IsPublicAsync(application) && + !await _applicationManager.ValidateClientSecretAsync(application, context.ClientSecret)) + { + context.Logger.LogError("The token request was rejected because the confidential or hybrid application " + + "'{ClientId}' didn't specify valid client credentials.", context.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: "The specified client credentials are invalid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting token requests made by + /// applications that haven't been granted the token endpoint permission. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateEndpointPermissions : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateEndpointPermissions() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateEndpointPermissions([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientSecret.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // Reject the request if the application is not allowed to use the token endpoint. + if (!await _applicationManager.HasPermissionAsync(application, Permissions.Endpoints.Token)) + { + context.Logger.LogError("The token request was rejected because the application '{ClientId}' " + + "was not allowed to use the token endpoint.", context.ClientId); + + context.Reject( + error: Errors.UnauthorizedClient, + description: "This client application is not allowed to use the token endpoint."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting token requests made by applications + /// that haven't been granted the appropriate grant type permissions. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateGrantTypePermissions : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateGrantTypePermissions() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateGrantTypePermissions([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // Reject the request if the application is not allowed to use the specified grant type. + if (!await _applicationManager.HasPermissionAsync(application, Permissions.Prefixes.GrantType + context.Request.GrantType)) + { + context.Logger.LogError("The token request was rejected because the application '{ClientId}' was not allowed to " + + "use the specified grant type: {GrantType}.", context.ClientId, context.Request.GrantType); + + context.Reject( + error: Errors.UnauthorizedClient, + description: "This client application is not allowed to use the specified grant type."); + + return; + } + + // Reject the request if the offline_access scope was request and if + // the application is not allowed to use the refresh token grant type. + if (context.Request.HasScope(Scopes.OfflineAccess) && + !await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.RefreshToken)) + { + context.Logger.LogError("The token request was rejected because the application '{ClientId}' " + + "was not allowed to request the 'offline_access' scope.", context.ClientId); + + context.Reject( + error: Errors.InvalidRequest, + description: "The client application is not allowed to use the 'offline_access' scope."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting token requests made by applications + /// that haven't been granted the appropriate grant type permission. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateScopePermissions : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateScopePermissions() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateScopePermissions([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + foreach (var scope in context.Request.GetScopes()) + { + // Avoid validating the "openid" and "offline_access" scopes as they represent protocol scopes. + if (string.Equals(scope, Scopes.OfflineAccess, StringComparison.Ordinal) || + string.Equals(scope, Scopes.OpenId, StringComparison.Ordinal)) + { + continue; + } + + // Reject the request if the application is not allowed to use the iterated scope. + if (!await _applicationManager.HasPermissionAsync(application, Permissions.Prefixes.Scope + scope)) + { + context.Logger.LogError("The token request was rejected because the application '{ClientId}' " + + "was not allowed to use the scope {Scope}.", context.ClientId, scope); + + context.Reject( + error: Errors.InvalidRequest, + description: "This client application is not allowed to use the specified scope."); + + return; + } + } + } + } + + /// + /// Contains the logic responsible of rejecting token requests that specify an invalid authorization code. + /// + public class ValidateAuthorizationCode : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateAuthorizationCode([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + // This handler is deliberately registered with a high order to ensure it runs + // after custom handlers registered with the default order and prevent the token + // endpoint from disclosing whether an authorization code or refresh token is + // valid before the caller's identity can first be fully verified. + .SetOrder(100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.Request.IsAuthorizationCodeGrantType()) + { + return; + } + + var notification = new DeserializeAuthorizationCodeContext(context.Transaction) + { + Token = context.Request.Code + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The authorization code was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating authorization codes ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + if (notification.Principal == null) + { + context.Logger.LogError("The token request was rejected because the authorization code was invalid."); + + context.Reject( + error: Errors.InvalidGrant, + description: "The specified authorization code is invalid."); + + return; + } + + var date = notification.Principal.GetExpirationDate(); + if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + { + context.Logger.LogError("The token request was rejected because the authorization code was expired."); + + context.Reject( + error: Errors.InvalidGrant, + description: "The specified authorization code is no longer valid."); + + return; + } + + // Attach the principal extracted from the authorization code to the parent event context. + context.Principal = notification.Principal; + } + } + + /// + /// Contains the logic responsible of rejecting token requests that specify an invalid refresh token. + /// + public class ValidateRefreshToken : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateRefreshToken([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ValidateAuthorizationCode.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.Request.IsRefreshTokenGrantType()) + { + return; + } + + var notification = new DeserializeRefreshTokenContext(context.Transaction) + { + Token = context.Request.RefreshToken + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The refresh token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating refresh tokens ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + if (notification.Principal == null) + { + context.Logger.LogError("The token request was rejected because the refresh token was invalid."); + + context.Reject( + error: Errors.InvalidGrant, + description: "The specified refresh token is invalid."); + + return; + } + + var date = notification.Principal.GetExpirationDate(); + if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + { + context.Logger.LogError("The token request was rejected because the refresh token was expired."); + + context.Reject( + error: Errors.InvalidGrant, + description: "The specified refresh token is no longer valid."); + + return; + } + + // Attach the principal extracted from the refresh token to the parent event context. + context.Principal = notification.Principal; + } + } + + /// + /// Contains the logic responsible of rejecting token requests that use an authorization code + /// or a refresh token that was issued for a different client application. + /// + public class ValidatePresenters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateRefreshToken.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType()) + { + return Task.CompletedTask; + } + + var presenters = context.Principal.GetPresenters(); + if (presenters.Count == 0) + { + // Note: presenters may be empty during a grant_type=refresh_token request if the refresh token + // was issued to a public client but cannot be null for an authorization code grant request. + if (context.Request.IsAuthorizationCodeGrantType()) + { + throw new InvalidOperationException("The presenters list cannot be extracted from the authorization code."); + } + + return Task.CompletedTask; + } + + // If at least one presenter was associated to the authorization code/refresh token, + // reject the request if the client_id of the caller cannot be retrieved or inferred. + if (string.IsNullOrEmpty(context.ClientId)) + { + context.Logger.LogError("The token request was rejected because the client identifier of the application " + + "was not available and could not be compared to the presenters list stored " + + "in the authorization code or the refresh token."); + + context.Reject( + error: Errors.InvalidGrant, + description: context.Request.IsAuthorizationCodeGrantType() ? + "The specified authorization code cannot be used without specifying a client identifier." : + "The specified refresh token cannot be used without specifying a client identifier."); + + return Task.CompletedTask; + } + + // Ensure the authorization code/refresh token was issued to the client application making the token request. + // Note: when using the refresh token grant, client_id is optional but MUST be validated if present. + // See https://tools.ietf.org/html/rfc6749#section-6 + // and http://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken. + if (!presenters.Contains(context.ClientId)) + { + context.Logger.LogError("The token request was rejected because the authorization code " + + "or the refresh token was issued to a different client application."); + + context.Reject( + error: Errors.InvalidGrant, + description: context.Request.IsAuthorizationCodeGrantType() ? + "The specified authorization code cannot be used by this client application." : + "The specified refresh token cannot be used by this client application."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting token requests that specify an invalid redirect_uri. + /// + public class ValidateRedirectUri : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidatePresenters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.Request.IsAuthorizationCodeGrantType()) + { + return Task.CompletedTask; + } + + // Validate the redirect_uri sent by the client application as part of this token request. + // Note: for pure OAuth 2.0 requests, redirect_uri is only mandatory if the authorization request + // contained an explicit redirect_uri. OpenID Connect requests MUST include a redirect_uri + // but the specifications allow proceeding the token request without returning an error + // if the authorization request didn't contain an explicit redirect_uri. + // See https://tools.ietf.org/html/rfc6749#section-4.1.3 + // and http://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation. + var address = context.Principal.GetClaim(Claims.Private.OriginalRedirectUri); + if (string.IsNullOrEmpty(address)) + { + return Task.CompletedTask; + } + + if (string.IsNullOrEmpty(context.Request.RedirectUri)) + { + context.Logger.LogError("The token request was rejected because the mandatory 'redirect_uri' " + + "parameter was missing from the grant_type=authorization_code request."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'redirect_uri' parameter is missing."); + + return Task.CompletedTask; + } + + if (!string.Equals(address, context.Request.RedirectUri, StringComparison.Ordinal)) + { + context.Logger.LogError("The token request was rejected because the 'redirect_uri' " + + "parameter didn't correspond to the expected value."); + + context.Reject( + error: Errors.InvalidGrant, + description: "The specified 'redirect_uri' parameter doesn't match the client " + + "redirection endpoint the authorization code was initially sent to."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting token requests that specify an invalid code verifier. + /// + public class ValidateCodeVerifier : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateRedirectUri.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.Request.IsAuthorizationCodeGrantType()) + { + return Task.CompletedTask; + } + + // If a code challenge was initially sent in the authorization request and associated with the + // code, validate the code verifier to ensure the token request is sent by a legit caller. + var challenge = context.Principal.GetClaim(Claims.Private.CodeChallenge); + if (string.IsNullOrEmpty(challenge)) + { + return Task.CompletedTask; + } + + // Get the code verifier from the token request. + // If it cannot be found, return an invalid_grant error. + if (string.IsNullOrEmpty(context.Request.CodeVerifier)) + { + context.Logger.LogError("The token request was rejected because the required 'code_verifier' " + + "parameter was missing from the grant_type=authorization_code request."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'code_verifier' parameter is missing."); + + return Task.CompletedTask; + } + + // If no code challenge method was specified, default to S256. + var method = context.Principal.GetClaim(Claims.Private.CodeChallengeMethod); + if (string.IsNullOrEmpty(method)) + { + method = CodeChallengeMethods.Sha256; + } + + // Note: when using the "plain" code challenge method, no hashing is actually performed. + // In this case, the raw ASCII bytes of the verifier are directly compared to the challenge. + ReadOnlySpan data; + if (string.Equals(method, CodeChallengeMethods.Plain, StringComparison.Ordinal)) + { + data = Encoding.ASCII.GetBytes(context.Request.CodeVerifier); + } + + else if (string.Equals(method, CodeChallengeMethods.Sha256, StringComparison.Ordinal)) + { + using var algorithm = SHA256.Create(); + data = algorithm.ComputeHash(Encoding.ASCII.GetBytes(context.Request.CodeVerifier)); + } + + else + { + context.Logger.LogError("The token request was rejected because the 'code_challenge_method' was invalid."); + + context.Reject( + error: Errors.InvalidGrant, + description: "The specified 'code_challenge_method' is invalid."); + + return Task.CompletedTask; + } + + // Compare the verifier and the code challenge: if the two don't match, return an error. + // Note: to prevent timing attacks, a time-constant comparer is always used. + if (!FixedTimeEquals(data, Base64UrlEncoder.DecodeBytes(challenge))) + { + context.Logger.LogError("The token request was rejected because the 'code_verifier' was invalid."); + + context.Reject( + error: Errors.InvalidGrant, + description: "The specified 'code_verifier' parameter is invalid."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static bool FixedTimeEquals(ReadOnlySpan left, ReadOnlySpan right) + { +#if SUPPORTS_TIME_CONSTANT_COMPARISONS + return CryptographicOperations.FixedTimeEquals(left, right); +#else + // Note: these null checks can be theoretically considered as early checks + // (which would defeat the purpose of a time-constant comparison method), + // but the expected string length is the only information an attacker + // could get at this stage, which is not critical where this method is used. + + if (left.Length != right.Length) + { + return false; + } + + var result = true; + + for (var index = 0; index < left.Length; index++) + { + result &= left[index] == right[index]; + } + + return result; +#endif + } + } + + /// + /// Contains the logic responsible of rejecting token requests that specify scopes that + /// were not initially granted by the resource owner during the authorization request. + /// + public class ValidateGrantedScopes : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateCodeVerifier.Descriptor.Order + 1_000) + .Build(); + + public Task HandleAsync([NotNull] ValidateTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.Request.IsAuthorizationCodeGrantType() || string.IsNullOrEmpty(context.Request.Scope)) + { + return Task.CompletedTask; + } + + // When an explicit scope parameter has been included in the token request + // but was missing from the initial request, the request MUST be rejected. + // See http://tools.ietf.org/html/rfc6749#section-6 for more information. + var scopes = context.Principal.GetScopes(); + if (scopes.Count == 0) + { + context.Logger.LogError("The token request was rejected because the 'scope' parameter was not allowed."); + + context.Reject( + error: Errors.InvalidGrant, + description: "The 'scope' parameter is not valid in this context."); + + return Task.CompletedTask; + } + + // When an explicit scope parameter has been included in the token request, + // the authorization server MUST ensure that it doesn't contain scopes + // that were not allowed during the initial authorization/token request. + // See https://tools.ietf.org/html/rfc6749#section-6 for more information. + else if (!scopes.IsSupersetOf(context.Request.GetScopes())) + { + context.Logger.LogError("The token request was rejected because the 'scope' parameter was not valid."); + + context.Reject( + error: Errors.InvalidGrant, + description: "The specified 'scope' parameter is invalid."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching the principal extracted + /// from the authorization code/refresh token to the event context. + /// + public class AttachPrincipal : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleTokenRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType()) + { + return Task.CompletedTask; + } + + if (context.Transaction.Properties.TryGetValue(Properties.OriginalPrincipal, out var principal)) + { + context.Principal ??= (ClaimsPrincipal) principal; + } + + return Task.CompletedTask; + } + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Serialization.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Serialization.cs new file mode 100644 index 000000000..9e263a275 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Serialization.cs @@ -0,0 +1,635 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerHandlers + { + public static class Serialization + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Access token serialization: + */ + AttachAccessTokenSerializationParameters.Descriptor, + SerializeJwtBearerToken.Descriptor, + + /* + * Authorization code serialization: + */ + AttachAuthorizationCodeSerializationParameters.Descriptor, + SerializeJwtBearerToken.Descriptor, + + /* + * Identity token serialization: + */ + AttachIdentityTokenSerializationParameters.Descriptor, + SerializeJwtBearerToken.Descriptor, + + /* + * Refresh token serialization: + */ + AttachRefreshTokenSerializationParameters.Descriptor, + SerializeJwtBearerToken.Descriptor, + + /* + * Access token deserialization: + */ + AttachAccessTokenDeserializationParameters.Descriptor, + DeserializeJwtBearerToken.Descriptor, + + /* + * Authorization code deserialization: + */ + AttachAuthorizationCodeDeserializationParameters.Descriptor, + DeserializeJwtBearerToken.Descriptor, + + /* + * Identity token deserialization: + */ + AttachIdentityTokenDeserializationParameters.Descriptor, + DeserializeJwtBearerToken.Descriptor, + + /* + * Authorization code deserialization: + */ + AttachRefreshTokenDeserializationParameters.Descriptor, + DeserializeJwtBearerToken.Descriptor); + + /// + /// Contains the logic responsible of generating a JWT bearer token using IdentityModel. + /// + public class SerializeJwtBearerToken : IOpenIddictServerHandler where TContext : BaseSerializingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.TokenUsage)) + { + throw new InvalidOperationException("The token usage cannot be null or empty."); + } + + var destinations = new Dictionary(StringComparer.Ordinal); + var claims = new Dictionary(StringComparer.Ordinal) + { + [Claims.Private.TokenUsage] = context.TokenUsage + }; + + foreach (var group in context.Principal.Claims.GroupBy(claim => claim.Type)) + { + var collection = group.ToList(); + switch (collection.Count) + { + case 1: + claims[group.Key] = collection[0].ValueType switch + { + ClaimValueTypes.Boolean => bool.Parse(collection[0].Value), + ClaimValueTypes.Double => double.Parse(collection[0].Value, NumberStyles.Number, CultureInfo.InvariantCulture), + ClaimValueTypes.Integer => int.Parse(collection[0].Value, NumberStyles.Integer, CultureInfo.InvariantCulture), + ClaimValueTypes.Integer32 => int.Parse(collection[0].Value, NumberStyles.Integer, CultureInfo.InvariantCulture), + ClaimValueTypes.Integer64 => long.Parse(collection[0].Value, NumberStyles.Integer, CultureInfo.InvariantCulture), + + "JSON" => JObject.Parse(collection[0].Value), + "JSON_ARRAY" => JArray.Parse(collection[0].Value), + + _ => (object) collection[0].Value + }; + break; + + default: + claims[group.Key] = collection.Select(claim => claim.Value).ToArray(); + break; + } + + // Note: destinations are attached to claims as special CLR properties. Such properties can't be serialized + // as part of classic JWT tokens. To work around this limitation, claim destinations are added to a special + // claim named oi_cl_dstn that contains a map of all the claims and their attached destinations, if any. + + var set = new HashSet(collection[0].GetDestinations(), StringComparer.OrdinalIgnoreCase); + if (set.Count != 0) + { + // Ensure the other claims of the same type use the same exact destinations. + for (var index = 0; index < collection.Count; index++) + { + if (!set.SetEquals(collection[index].GetDestinations())) + { + throw new InvalidOperationException($"Conflicting destinations for the claim '{group.Key}' were specified."); + } + } + + destinations[group.Key] = set.ToArray(); + } + } + + // Unless at least one claim was added to the claim destinations map, + // don't add the special claim to avoid adding a useless empty claim. + if (destinations.Count != 0) + { + claims[Claims.Private.ClaimDestinations] = destinations; + } + + context.Token = context.SecurityTokenHandler.CreateToken(new SecurityTokenDescriptor + { + Claims = new ReadOnlyDictionary(claims), + EncryptingCredentials = context.EncryptingCredentials, + Issuer = context.Issuer?.AbsoluteUri, + SigningCredentials = context.SigningCredentials + }); + + context.HandleSerialization(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of unprotecting a JWT bearer token using IdentityModel. + /// + public class DeserializeJwtBearerToken : IOpenIddictServerHandler where TContext : BaseDeserializingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.SecurityTokenHandler.CanReadToken(context.Token)) + { + context.Logger.LogTrace("The token '{Token}' was not compatible with the JWT format.", context.Token); + + return Task.CompletedTask; + } + + try + { + var result = context.SecurityTokenHandler.ValidateToken(context.Token, context.TokenValidationParameters); + if (result == null || !result.IsValid) + { + if (result?.Exception != null) + { + context.Logger.LogTrace(result.Exception, "The JWT token '{Token}' could not be validated.", context.Token); + } + + else + { + context.Logger.LogTrace("The token '{Token}' could not be validated.", context.Token); + } + } + + var assertion = ((JsonWebToken) result.SecurityToken)?.InnerToken ?? (JsonWebToken) result.SecurityToken; + + if (!assertion.TryGetPayloadValue(Claims.Private.TokenUsage, out string usage) || + !string.Equals(usage, context.TokenUsage, StringComparison.OrdinalIgnoreCase)) + { + context.Logger.LogDebug("The token usage associated to the token {Token} does not match the expected type."); + context.HandleDeserialization(); + + return Task.CompletedTask; + } + + context.Principal = new ClaimsPrincipal(result.ClaimsIdentity); + + // Restore the claim destinations from the special oi_cl_dstn claim (represented as a dictionary/JSON object). + if (assertion.TryGetPayloadValue(Claims.Private.ClaimDestinations, out IDictionary definitions)) + { + foreach (var definition in definitions) + { + foreach (var claim in context.Principal.Claims.Where(claim => claim.Type == definition.Key)) + { + claim.SetDestinations(definition.Value); + } + } + } + + context.HandleDeserialization(); + + return Task.CompletedTask; + } + + catch (Exception exception) + { + context.Logger.LogDebug(exception, "An exception occured while deserializing a token."); + context.HandleDeserialization(); + + return Task.CompletedTask; + } + } + } + + /// + /// Contains the logic responsible of populating the serialization parameters needed to generate an access token. + /// + public class AttachAccessTokenSerializationParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] SerializeAccessTokenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Options.SigningCredentials.Count == 0) + { + throw new InvalidOperationException("No suitable signing credentials could be found."); + } + + context.EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault( + credentials => credentials.Key is SymmetricSecurityKey); + context.Issuer = context.Options.Issuer; + context.SecurityTokenHandler = context.Options.AccessTokenHandler; + context.SigningCredentials = context.Options.SigningCredentials.FirstOrDefault( + credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of populating the serialization parameters needed to generate an authorization code. + /// + public class AttachAuthorizationCodeSerializationParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] SerializeAuthorizationCodeContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Options.EncryptionCredentials.Count == 0) + { + throw new InvalidOperationException("No suitable encryption credentials could be found."); + } + + if (context.Options.SigningCredentials.Count == 0) + { + throw new InvalidOperationException("No suitable signing credentials could be found."); + } + + context.EncryptingCredentials = context.Options.EncryptionCredentials[0]; + context.Issuer = context.Options.Issuer; + context.SecurityTokenHandler = context.Options.AuthorizationCodeHandler; + context.SigningCredentials = context.Options.SigningCredentials.FirstOrDefault( + credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of populating the serialization parameters needed to generate an identity token. + /// + public class AttachIdentityTokenSerializationParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] SerializeIdentityTokenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.Options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey)) + { + throw new InvalidOperationException("No suitable signing credentials could be found."); + } + + context.Issuer = context.Options.Issuer; + context.SecurityTokenHandler = context.Options.IdentityTokenHandler; + context.SigningCredentials = context.Options.SigningCredentials.First( + credentials => credentials.Key is AsymmetricSecurityKey); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of populating the serialization parameters needed to generate a refresh token. + /// + public class AttachRefreshTokenSerializationParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] SerializeRefreshTokenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Options.EncryptionCredentials.Count == 0) + { + throw new InvalidOperationException("No suitable encryption credentials could be found."); + } + + if (context.Options.SigningCredentials.Count == 0) + { + throw new InvalidOperationException("No suitable signing credentials could be found."); + } + + context.EncryptingCredentials = context.Options.EncryptionCredentials[0]; + context.Issuer = context.Options.Issuer; + context.SecurityTokenHandler = context.Options.AuthorizationCodeHandler; + context.SigningCredentials = context.Options.SigningCredentials.FirstOrDefault( + credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of populating the deserialization parameters needed to unprotect an access token. + /// + public class AttachAccessTokenDeserializationParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] DeserializeAccessTokenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.SecurityTokenHandler = context.Options.AccessTokenHandler; + + context.TokenValidationParameters.IssuerSigningKeys = context.Options.SigningCredentials + .Select(credentials => credentials.Key); + context.TokenValidationParameters.NameClaimType = Claims.Name; + context.TokenValidationParameters.RoleClaimType = Claims.Role; + context.TokenValidationParameters.TokenDecryptionKeys = context.Options.EncryptionCredentials + .Select(credentials => credentials.Key) + .Where(key => key is SymmetricSecurityKey); + context.TokenValidationParameters.ValidIssuer = context.Options.Issuer?.AbsoluteUri; + context.TokenValidationParameters.ValidateAudience = false; + context.TokenValidationParameters.ValidateLifetime = false; + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of populating the deserialization parameters needed to unprotect an authorization code. + /// + public class AttachAuthorizationCodeDeserializationParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] DeserializeAuthorizationCodeContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.SecurityTokenHandler = context.Options.AuthorizationCodeHandler; + + context.TokenValidationParameters.IssuerSigningKeys = context.Options.SigningCredentials + .Select(credentials => credentials.Key); + context.TokenValidationParameters.NameClaimType = Claims.Name; + context.TokenValidationParameters.RoleClaimType = Claims.Role; + context.TokenValidationParameters.TokenDecryptionKeys = context.Options.EncryptionCredentials + .Select(credentials => credentials.Key); + context.TokenValidationParameters.ValidIssuer = context.Options.Issuer?.AbsoluteUri; + context.TokenValidationParameters.ValidateAudience = false; + context.TokenValidationParameters.ValidateLifetime = false; + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of populating the deserialization parameters needed to unprotect an identity token. + /// + public class AttachIdentityTokenDeserializationParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] DeserializeIdentityTokenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.SecurityTokenHandler = context.Options.IdentityTokenHandler; + + context.TokenValidationParameters.IssuerSigningKeys = context.Options.SigningCredentials + .Select(credentials => credentials.Key) + .OfType(); + context.TokenValidationParameters.NameClaimType = Claims.Name; + context.TokenValidationParameters.RoleClaimType = Claims.Role; + context.TokenValidationParameters.ValidIssuer = context.Options.Issuer?.AbsoluteUri; + context.TokenValidationParameters.ValidateAudience = false; + context.TokenValidationParameters.ValidateLifetime = false; + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of populating the deserialization parameters needed to unprotect a refresh token. + /// + public class AttachRefreshTokenDeserializationParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] DeserializeRefreshTokenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.SecurityTokenHandler = context.Options.AuthorizationCodeHandler; + + context.TokenValidationParameters.IssuerSigningKeys = context.Options.SigningCredentials + .Select(credentials => credentials.Key); + context.TokenValidationParameters.NameClaimType = Claims.Name; + context.TokenValidationParameters.RoleClaimType = Claims.Role; + context.TokenValidationParameters.TokenDecryptionKeys = context.Options.EncryptionCredentials + .Select(credentials => credentials.Key); + context.TokenValidationParameters.ValidIssuer = context.Options.Issuer?.AbsoluteUri; + context.TokenValidationParameters.ValidateAudience = false; + context.TokenValidationParameters.ValidateLifetime = false; + + return Task.CompletedTask; + } + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs new file mode 100644 index 000000000..77d826276 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -0,0 +1,815 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.ComponentModel; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlerFilters; + +namespace OpenIddict.Server +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public static partial class OpenIddictServerHandlers + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Challenge response processing: + */ + AttachDefaultChallengeError.Descriptor, + + /* + * Sign-in response processing: + */ + ValidateSigninResponse.Descriptor, + AttachDefaultScopes.Descriptor, + AttachDefaultPresenters.Descriptor, + EvaluateReturnedTokens.Descriptor, + AttachAccessToken.Descriptor, + AttachAuthorizationCode.Descriptor, + AttachRefreshToken.Descriptor, + AttachIdentityToken.Descriptor) + + .AddRange(Authentication.DefaultHandlers) + .AddRange(Discovery.DefaultHandlers) + .AddRange(Exchange.DefaultHandlers) + .AddRange(Serialization.DefaultHandlers); + + /// + /// Contains the logic responsible of ensuring that the challenge response contains an appropriate error. + /// + public class AttachDefaultChallengeError : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ProcessChallengeResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.Response.Error)) + { + context.Response.Error = context.EndpointType switch + { + OpenIddictServerEndpointType.Authorization => Errors.AccessDenied, + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + + _ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.") + }; + } + + if (string.IsNullOrEmpty(context.Response.ErrorDescription)) + { + context.Response.ErrorDescription = context.EndpointType switch + { + OpenIddictServerEndpointType.Authorization => "The authorization was denied by the resource owner.", + OpenIddictServerEndpointType.Token => "The token request was rejected by the authorization server.", + + _ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.") + }; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of ensuring that the sign-in response + /// is compatible with the type of the endpoint that handled the request. + /// + public class ValidateSigninResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ProcessSigninResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + switch (context.EndpointType) + { + case OpenIddictServerEndpointType.Authorization: + case OpenIddictServerEndpointType.Token: + break; + + default: throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); + } + + if (context.Principal.Identity == null || !context.Principal.Identity.IsAuthenticated) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The specified principal doesn't contain a valid or authenticated identity.") + .Append("Make sure that both 'ClaimsPrincipal.Identity' and 'ClaimsPrincipal.Identity.AuthenticationType' ") + .Append("are not null and that 'ClaimsPrincipal.Identity.IsAuthenticated' returns 'true'.") + .ToString()); + } + + if (string.IsNullOrEmpty(context.Principal.GetClaim(Claims.Subject))) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The security principal was rejected because the mandatory subject claim was missing.") + .ToString()); + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching default scopes to the authentication principal. + /// + public class AttachDefaultScopes : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateSigninResponse.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ProcessSigninResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Always include the "openid" scope when the developer doesn't explicitly call SetScopes. + // Note: the application is allowed to specify a different "scopes": in this case, + // don't replace the "scopes" property stored in the authentication ticket. + if (!context.Principal.HasScope() && context.Request.HasScope(Scopes.OpenId)) + { + context.Principal.SetScopes(Scopes.OpenId); + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching default presenters to the authentication principal. + /// + public class AttachDefaultPresenters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachDefaultScopes.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ProcessSigninResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Add the validated client_id to the list of authorized presenters, + // unless the presenters were explicitly set by the developer. + if (!context.Principal.HasPresenter() && !string.IsNullOrEmpty(context.ClientId)) + { + context.Principal.SetPresenters(context.ClientId); + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of selecting the token types returned to the client application. + /// + public class EvaluateReturnedTokens : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachDefaultPresenters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ProcessSigninResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.IncludeAccessToken = context.EndpointType switch + { + // For authorization requests, return an access token if a response type containing token was specified. + OpenIddictServerEndpointType.Authorization => context.Request.HasResponseType(ResponseTypes.Token), + + // For token requests, always return an access token. + OpenIddictServerEndpointType.Token => true, + + _ => false + }; + + context.IncludeAuthorizationCode = context.EndpointType switch + { + // For authorization requests, return an authorization code if a response type containing code was specified. + OpenIddictServerEndpointType.Authorization => context.Request.HasResponseType(ResponseTypes.Code), + + // For token requests, prevent an authorization code from being returned as this type of token + // cannot be issued from the token endpoint in the standard OAuth 2.0/OpenID Connect flows. + OpenIddictServerEndpointType.Token => false, + + _ => false + }; + + context.IncludeRefreshToken = context.EndpointType switch + { + // For authorization requests, prevent a refresh token from being returned as OAuth 2.0 + // explicitly disallows returning a refresh token from the authorization endpoint. + // See https://tools.ietf.org/html/rfc6749#section-4.2.2 for more information. + OpenIddictServerEndpointType.Authorization => false, + + // For token requests, don't return a refresh token is the offline_access scope was not granted. + OpenIddictServerEndpointType.Token when !context.Principal.HasScope(Scopes.OfflineAccess) => false, + + // For token requests, only return a refresh token is the offline_access scope was granted and + // if sliding expiration is disabled or if the request is not a grant_type=refresh_token request. + OpenIddictServerEndpointType.Token => context.Options.UseSlidingExpiration || + !context.Request.IsRefreshTokenGrantType(), + + _ => false + }; + + context.IncludeIdentityToken = context.EndpointType switch + { + // For authorization requests, return an identity token if a response type containing code + // was specified and if the openid scope was explicitly or implicitly granted. + OpenIddictServerEndpointType.Authorization => context.Principal.HasScope(Scopes.OpenId) && + context.Request.HasResponseType(ResponseTypes.IdToken), + + // For token requests, only return an identity token if the openid scope was granted. + OpenIddictServerEndpointType.Token => context.Principal.HasScope(Scopes.OpenId), + + _ => false + }; + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of generating and attaching an access token. + /// + public class AttachAccessToken : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public AttachAccessToken([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(EvaluateReturnedTokens.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessSigninResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Create a new principal containing only the filtered claims. + // Actors identities are also filtered (delegation scenarios). + var principal = context.Principal.Clone(claim => + { + // Never exclude the subject claim. + if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Always exclude private claims, whose values must generally be kept secret. + if (claim.Type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Claims whose destination is not explicitly referenced or doesn't + // contain "access_token" are not included in the access token. + if (!claim.HasDestination(Destinations.AccessToken)) + { + context.Logger.LogDebug("'{Claim}' was excluded from the access token claims.", claim.Type); + + return false; + } + + return true; + }); + + principal.SetTokenId(Guid.NewGuid().ToString()).SetCreationDate(DateTimeOffset.UtcNow); + + var lifetime = context.Principal.GetAccessTokenLifetime() ?? context.Options.AccessTokenLifetime; + if (lifetime.HasValue) + { + principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); + } + + // Remove the destinations from the claim properties. + foreach (var claim in principal.Claims) + { + claim.Properties.Remove(Properties.Destinations); + } + + // When receiving a grant_type=refresh_token request, determine whether the client application + // requests a limited set of scopes and immediately replace the scopes collection if necessary. + if (context.EndpointType == OpenIddictServerEndpointType.Token && + context.Request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(context.Request.Scope)) + { + var scopes = context.Request.GetScopes(); + if (scopes.Count != 0) + { + context.Logger.LogDebug("The access token scopes will be limited to the scopes " + + "requested by the client application: {Scopes}.", scopes); + + principal.SetScopes(scopes.Intersect(context.Principal.GetScopes())); + } + } + + var notification = new SerializeAccessTokenContext(context.Transaction) + { + Principal = principal + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The access token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of generating access tokens ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + context.Response.TokenType = TokenTypes.Bearer; + context.Response.AccessToken = notification.Token; + + // If an expiration date was set, return it to the client application. + var date = notification.Principal.GetExpirationDate(); + if (date.HasValue && date.Value > DateTimeOffset.UtcNow) + { + context.Response.ExpiresIn = (long) ((date.Value - DateTimeOffset.UtcNow).TotalSeconds + .5); + } + + // If the granted scopes differ from the request scopes, return the granted scopes list as a parameter. + if (context.Request.IsAuthorizationCodeGrantType() || + !context.Principal.GetScopes().SetEquals(context.Request.GetScopes())) + { + context.Response.Scope = string.Join(" ", context.Principal.GetScopes()); + } + } + } + + /// + /// Contains the logic responsible of generating and attaching an authorization code. + /// + public class AttachAuthorizationCode : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public AttachAuthorizationCode([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(AttachAccessToken.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessSigninResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var principal = context.Principal.Clone(_ => true) + .SetTokenId(Guid.NewGuid().ToString()) + .SetCreationDate(DateTimeOffset.UtcNow); + + var lifetime = context.Principal.GetAuthorizationCodeLifetime() ?? context.Options.AuthorizationCodeLifetime; + if (lifetime.HasValue) + { + principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); + } + + // Attach the redirect_uri to allow for later comparison when + // receiving a grant_type=authorization_code token request. + if (!string.IsNullOrEmpty(context.Request.RedirectUri)) + { + principal.SetClaim(Claims.Private.OriginalRedirectUri, context.Request.RedirectUri); + } + + // Attach the code challenge and the code challenge methods to allow the ValidateCodeVerifier + // handler to validate the code verifier sent by the client as part of the token request. + if (!string.IsNullOrEmpty(context.Request.CodeChallenge)) + { + principal.SetClaim(Claims.Private.CodeChallenge, context.Request.CodeChallenge); + + // Default to S256 if no explicit code challenge method was specified. + principal.SetClaim(Claims.Private.CodeChallengeMethod, + !string.IsNullOrEmpty(context.Request.CodeChallengeMethod) ? + context.Request.CodeChallengeMethod : CodeChallengeMethods.Sha256); + } + + // Attach the nonce so that it can be later returned by + // the token endpoint as part of the JWT identity token. + if (!string.IsNullOrEmpty(context.Request.Nonce)) + { + principal.SetClaim(Claims.Nonce, context.Request.Nonce); + } + + var notification = new SerializeAuthorizationCodeContext(context.Transaction) + { + Principal = principal + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The authorization code was not correctly processed. This may indicate ") + .Append("that the event handler responsible of generating authorization codes ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + context.Response.Code = notification.Token; + } + } + + /// + /// Contains the logic responsible of generating and attaching a refresh token. + /// + public class AttachRefreshToken : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public AttachRefreshToken([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(AttachAuthorizationCode.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessSigninResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var principal = context.Principal.Clone(_ => true) + .SetTokenId(Guid.NewGuid().ToString()) + .SetCreationDate(DateTimeOffset.UtcNow); + + var lifetime = context.Principal.GetRefreshTokenLifetime() ?? context.Options.RefreshTokenLifetime; + if (lifetime.HasValue) + { + principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); + } + + var notification = new SerializeRefreshTokenContext(context.Transaction) + { + Principal = principal + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The refresh token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of generating refresh tokens ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + context.Response.RefreshToken = notification.Token; + } + } + + /// + /// Contains the logic responsible of generating and attaching an identity token. + /// + public class AttachIdentityToken : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public AttachIdentityToken([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(AttachRefreshToken.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessSigninResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + + // Replace the principal by a new one containing only the filtered claims. + // Actors identities are also filtered (delegation scenarios). + var principal = context.Principal.Clone(claim => + { + // Never exclude the subject claim. + if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Always exclude private claims, whose values must generally be kept secret. + if (claim.Type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Claims whose destination is not explicitly referenced or doesn't + // contain "id_token" are not included in the identity token. + if (!claim.HasDestination(Destinations.IdentityToken)) + { + context.Logger.LogDebug("'{Claim}' was excluded from the identity token claims.", claim.Type); + + return false; + } + + return true; + }); + + principal.SetTokenId(Guid.NewGuid().ToString()).SetCreationDate(DateTimeOffset.UtcNow); + + // Remove the destinations from the claim properties. + foreach (var claim in principal.Claims) + { + claim.Properties.Remove(Properties.Destinations); + } + + var lifetime = context.Principal.GetIdentityTokenLifetime() ?? context.Options.IdentityTokenLifetime; + if (lifetime.HasValue) + { + principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); + } + + if (!string.IsNullOrEmpty(context.ClientId)) + { + principal.SetAudiences(context.ClientId); + } + + // If a nonce was present in the authorization request, it MUST be included in the id_token generated + // by the token endpoint. For that, OpenIddict simply flows the nonce as an authorization code claim. + // See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information. + + if (context.EndpointType == OpenIddictServerEndpointType.Authorization && !string.IsNullOrEmpty(context.Request.Nonce)) + { + principal.SetClaim(Claims.Nonce, context.Request.Nonce); + } + + else if (context.EndpointType == OpenIddictServerEndpointType.Token) + { + var nonce = context.Principal.GetClaim(Claims.Nonce); + if (!string.IsNullOrEmpty(nonce)) + { + principal.SetClaim(Claims.Nonce, nonce); + } + } + + if (!string.IsNullOrEmpty(context.Response.AccessToken) || !string.IsNullOrEmpty(context.Response.Code)) + { + var credentials = context.Options.SigningCredentials.FirstOrDefault( + credentials => credentials.Key is AsymmetricSecurityKey); + if (credentials == null) + { + throw new InvalidOperationException("No suitable signing credentials could be found."); + } + + using var hash = GetHashAlgorithm(credentials); + if (hash == null || hash is KeyedHashAlgorithm) + { + throw new InvalidOperationException("The signing credentials algorithm is not valid."); + } + + if (!string.IsNullOrEmpty(context.Response.Code)) + { + var digest = hash.ComputeHash(Encoding.ASCII.GetBytes(context.Response.Code)); + + // Note: only the left-most half of the hash is used. + // See http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken + principal.SetClaim(Claims.CodeHash, Base64UrlEncoder.Encode(digest, 0, digest.Length / 2)); + } + + if (!string.IsNullOrEmpty(context.Response.AccessToken)) + { + var digest = hash.ComputeHash(Encoding.ASCII.GetBytes(context.Response.AccessToken)); + + // Note: only the left-most half of the hash is used. + // See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken + principal.SetClaim(Claims.AccessTokenHash, Base64UrlEncoder.Encode(digest, 0, digest.Length / 2)); + } + } + + var notification = new SerializeIdentityTokenContext(context.Transaction) + { + Principal = principal + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The identity token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of generating identity tokens ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + context.Response.IdToken = notification.Token; + + static HashAlgorithm GetHashAlgorithm(SigningCredentials credentials) + { + HashAlgorithm hash = null; + + if (!string.IsNullOrEmpty(credentials.Digest)) + { + hash = CryptoConfig.CreateFromName(credentials.Digest) as HashAlgorithm; + } + + if (hash == null) + { + var algorithm = credentials.Digest switch + { + SecurityAlgorithms.Sha256 => HashAlgorithmName.SHA256, + SecurityAlgorithms.Sha384 => HashAlgorithmName.SHA384, + SecurityAlgorithms.Sha512 => HashAlgorithmName.SHA512, + SecurityAlgorithms.Sha256Digest => HashAlgorithmName.SHA256, + SecurityAlgorithms.Sha384Digest => HashAlgorithmName.SHA384, + SecurityAlgorithms.Sha512Digest => HashAlgorithmName.SHA512, + + _ => credentials.Algorithm switch + { +#if SUPPORTS_ECDSA + SecurityAlgorithms.EcdsaSha256 => HashAlgorithmName.SHA256, + SecurityAlgorithms.EcdsaSha384 => HashAlgorithmName.SHA384, + SecurityAlgorithms.EcdsaSha512 => HashAlgorithmName.SHA512, + SecurityAlgorithms.EcdsaSha256Signature => HashAlgorithmName.SHA256, + SecurityAlgorithms.EcdsaSha384Signature => HashAlgorithmName.SHA384, + SecurityAlgorithms.EcdsaSha512Signature => HashAlgorithmName.SHA512, +#endif + SecurityAlgorithms.HmacSha256 => HashAlgorithmName.SHA256, + SecurityAlgorithms.HmacSha384 => HashAlgorithmName.SHA384, + SecurityAlgorithms.HmacSha512 => HashAlgorithmName.SHA512, + SecurityAlgorithms.HmacSha256Signature => HashAlgorithmName.SHA256, + SecurityAlgorithms.HmacSha384Signature => HashAlgorithmName.SHA384, + SecurityAlgorithms.HmacSha512Signature => HashAlgorithmName.SHA512, + + SecurityAlgorithms.RsaSha256 => HashAlgorithmName.SHA256, + SecurityAlgorithms.RsaSha384 => HashAlgorithmName.SHA384, + SecurityAlgorithms.RsaSha512 => HashAlgorithmName.SHA512, + SecurityAlgorithms.RsaSha256Signature => HashAlgorithmName.SHA256, + SecurityAlgorithms.RsaSha384Signature => HashAlgorithmName.SHA384, + SecurityAlgorithms.RsaSha512Signature => HashAlgorithmName.SHA512, + + _ => throw new InvalidOperationException("The signing credentials algorithm is not supported.") + } + }; + + hash = CryptoConfig.CreateFromName(algorithm.Name) as HashAlgorithm; + } + + return hash; + } + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerHelpers.cs b/src/OpenIddict.Server/OpenIddictServerHelpers.cs deleted file mode 100644 index 8f7fb7edf..000000000 --- a/src/OpenIddict.Server/OpenIddictServerHelpers.cs +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using AspNet.Security.OpenIdConnect.Extensions; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Authentication; -using OpenIddict.Abstractions; - -namespace OpenIddict.Server -{ - /// - /// Exposes extensions allowing to store and retrieve - /// OpenIddict-specific properties in authentication tickets. - /// - public static class OpenIddictServerHelpers - { - /// - /// Gets the internal authorization identifier associated with the authentication ticket. - /// Note: this identifier can be used to retrieve the authorization from the database. - /// - /// The authentication ticket. - /// The authorization identifier or null if it cannot be found. - public static string GetInternalAuthorizationId([NotNull] this AuthenticationTicket ticket) - { - if (ticket == null) - { - throw new ArgumentNullException(nameof(ticket)); - } - - return ticket.GetProperty(OpenIddictConstants.Properties.InternalAuthorizationId); - } - - /// - /// Gets the internal token identifier associated with the authentication ticket. - /// Note: this identifier can be used to retrieve the token from the database. - /// - /// The authentication ticket. - /// The token identifier or null if it cannot be found. - public static string GetInternalTokenId([NotNull] this AuthenticationTicket ticket) - { - if (ticket == null) - { - throw new ArgumentNullException(nameof(ticket)); - } - - return ticket.GetProperty(OpenIddictConstants.Properties.InternalTokenId); - } - - /// - /// Sets the internal authorization identifier associated with the authentication ticket. - /// Note: the identifier MUST correspond to a valid authorization entry in the database. - /// - /// The authentication ticket. - /// The internal authorization identifier. - /// The authentication ticket. - public static AuthenticationTicket SetInternalAuthorizationId( - [NotNull] this AuthenticationTicket ticket, [CanBeNull] string identifier) - { - if (ticket == null) - { - throw new ArgumentNullException(nameof(ticket)); - } - - return ticket.SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, identifier); - } - - /// - /// Sets the internal token identifier associated with the authentication ticket. - /// Note: the identifier MUST correspond to a valid token entry in the database. - /// - /// The authentication ticket. - /// The internal token identifier. - /// The authentication ticket. - public static AuthenticationTicket SetInternalTokenId( - [NotNull] this AuthenticationTicket ticket, [CanBeNull] string identifier) - { - if (ticket == null) - { - throw new ArgumentNullException(nameof(ticket)); - } - - return ticket.SetProperty(OpenIddictConstants.Properties.InternalTokenId, identifier); - } - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 277fb3e6d..777ffeb6d 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -6,9 +6,8 @@ using System; using System.Collections.Generic; -using System.Security.Cryptography; -using AspNet.Security.OpenIdConnect.Server; -using Microsoft.Extensions.Caching.Distributed; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; namespace OpenIddict.Server @@ -16,16 +15,148 @@ namespace OpenIddict.Server /// /// Provides various settings needed to configure the OpenIddict server handler. /// - public class OpenIddictServerOptions : OpenIdConnectServerOptions + public class OpenIddictServerOptions { /// - /// Creates a new instance of the class. + /// Gets or sets the optional base address used to uniquely identify the authorization server. + /// The URI must be absolute and may contain a path, but no query string or fragment part. /// - public OpenIddictServerOptions() + public Uri Issuer { get; set; } + + /// + /// Gets the list of credentials used to encrypt the tokens issued by the + /// OpenIddict server services. Note: only symmetric credentials are supported. + /// + public IList EncryptionCredentials { get; } = new List(); + + /// + /// Gets the list of credentials used to sign the tokens issued by the OpenIddict server services. + /// Both asymmetric and symmetric keys are supported, but only asymmetric keys can be used to sign identity tokens. + /// Note that only asymmetric RSA and ECDSA keys can be exposed by the JWKS metadata endpoint. + /// + public IList SigningCredentials { get; } = new List(); + + /// + /// Gets the absolute and relative URIs associated to the authorization endpoint. + /// + public IList AuthorizationEndpointUris { get; } = new List(); + + /// + /// Gets the absolute and relative URIs associated to the configuration endpoint. + /// + public IList ConfigurationEndpointUris { get; } = new List + { + new Uri("/.well-known/openid-configuration", UriKind.Relative), + new Uri("/.well-known/oauth-authorization-server", UriKind.Relative) + }; + + /// + /// Gets the absolute and relative URIs associated to the cryptography endpoint. + /// + public IList CryptographyEndpointUris { get; } = new List { - Provider = null; - ProviderType = typeof(OpenIddictServerProvider); - } + new Uri("/.well-known/jwks", UriKind.Relative) + }; + + /// + /// Gets the absolute and relative URIs associated to the introspection endpoint. + /// + public IList IntrospectionEndpointUris { get; } = new List(); + + /// + /// Gets the absolute and relative URIs associated to the logout endpoint. + /// + public IList LogoutEndpointUris { get; } = new List(); + + /// + /// Gets the absolute and relative URIs associated to the revocation endpoint. + /// + public IList RevocationEndpointUris { get; } = new List(); + + /// + /// Gets the absolute and relative URIs associated to the token endpoint. + /// + public IList TokenEndpointUris { get; } = new List(); + + /// + /// Gets the absolute and relative URIs associated to the userinfo endpoint. + /// + public IList UserinfoEndpointUris { get; } = new List(); + + /// + /// Gets or sets the security token handler used to protect and unprotect authorization codes. + /// + public JsonWebTokenHandler AuthorizationCodeHandler { get; set; } = new JsonWebTokenHandler(); + + /// + /// Gets or sets the security token handler used to protect and unprotect access tokens. + /// + public JsonWebTokenHandler AccessTokenHandler { get; set; } = new JsonWebTokenHandler(); + + /// + /// Gets or sets the security token handler used to protect and unprotect identity tokens. + /// + public JsonWebTokenHandler IdentityTokenHandler { get; set; } = new JsonWebTokenHandler(); + + /// + /// Gets or sets the security token handler used to protect and unprotect refresh tokens. + /// + public JsonWebTokenHandler RefreshTokenHandler { get; set; } = new JsonWebTokenHandler(); + + /// + /// Gets or sets the period of time the authorization codes remain valid after being issued. + /// While not recommended, this property can be set to null to issue codes that never expire. + /// + public TimeSpan? AuthorizationCodeLifetime { get; set; } = TimeSpan.FromMinutes(5); + + /// + /// Gets or sets the period of time access tokens remain valid after being issued. The default value is 1 hour. + /// The client application is expected to refresh or acquire a new access token after the token has expired. + /// While not recommended, this property can be set to null to issue access tokens that never expire. + /// + public TimeSpan? AccessTokenLifetime { get; set; } = TimeSpan.FromHours(1); + + /// + /// Gets or sets the period of time identity tokens remain valid after being issued. The default value is 20 minutes. + /// The client application is expected to refresh or acquire a new identity token after the token has expired. + /// While not recommended, this property can be set to null to issue identity tokens that never expire. + /// + public TimeSpan? IdentityTokenLifetime { get; set; } = TimeSpan.FromMinutes(20); + + /// + /// Gets or sets the period of time refresh tokens remain valid after being issued. The default value is 14 days. + /// The client application is expected to start a whole new authentication flow after the refresh token has expired. + /// While not recommended, this property can be set to null to issue refresh tokens that never expire. + /// + public TimeSpan? RefreshTokenLifetime { get; set; } = TimeSpan.FromDays(14); + + /// + /// Gets or sets a boolean indicating whether the degraded mode is enabled. When this degraded mode + /// is enabled, all the security checks that depend on the OpenIddict core managers are disabled. + /// This option MUST be enabled with extreme caution and custom handlers MUST be registered to + /// properly validate OpenID Connect requests. + /// + public bool EnableDegradedMode { get; set; } + + /// + /// Gets the list of the user-defined/custom handlers responsible of processing the OpenIddict server requests. + /// Note: the handlers added to this list must be also registered in the DI container using an appropriate lifetime. + /// + public IList CustomHandlers { get; } = + new List(); + + /// + /// Gets the list of the built-in handlers responsible of processing the OpenIddict server requests + /// + public IList DefaultHandlers { get; } = + new List(OpenIddictServerHandlers.DefaultHandlers); + + /// + /// Gets or sets a boolean indicating whether new refresh tokens should be issued during a refresh token request. + /// Set this property to true to issue a new refresh token, false to prevent the OpenID Connect + /// server middleware from issuing new refresh tokens when receiving a grant_type=refresh_token request. + /// + public bool UseSlidingExpiration { get; set; } = true; /// /// Gets or sets a boolean determining whether client identification is optional. @@ -35,13 +166,7 @@ public OpenIddictServerOptions() public bool AcceptAnonymousClients { get; set; } /// - /// Gets or sets the distributed cache used by OpenIddict. If no cache is explicitly - /// provided, the cache registered in the dependency injection container is used. - /// - public IDistributedCache Cache { get; set; } - - /// - /// Gets the OAuth2/OpenID Connect claims supported by this application. + /// Gets the OAuth 2.0/OpenID Connect claims supported by this application. /// public ISet Claims { get; } = new HashSet(StringComparer.Ordinal) { @@ -67,22 +192,13 @@ public OpenIddictServerOptions() /// public bool DisableTokenStorage { get; set; } - /// - /// Gets or sets a boolean indicating whether request caching should be enabled. - /// When enabled, both authorization and logout requests are automatically stored - /// in the distributed cache, which allows flowing large payloads across requests. - /// Enabling this option is recommended when using external authentication providers - /// or when large GET or POST OpenID Connect authorization requests support is required. - /// - public bool EnableRequestCaching { get; set; } - /// /// Gets or sets a boolean indicating whether scope validation is disabled. /// public bool DisableScopeValidation { get; set; } /// - /// Gets the OAuth2/OpenID Connect flows enabled for this application. + /// Gets the OAuth 2.0/OpenID Connect flows enabled for this application. /// public ISet GrantTypes { get; } = new HashSet(StringComparer.Ordinal); @@ -108,30 +224,7 @@ public OpenIddictServerOptions() public bool IgnoreScopePermissions { get; set; } /// - /// Gets or sets the random number generator used to generate crypto-secure identifiers. - /// - public RandomNumberGenerator RandomNumberGenerator { get; set; } = RandomNumberGenerator.Create(); - - /// - /// Gets or sets the caching policy used to determine how long the authorization - /// and end session requests should be cached by the distributed cache implementation. - /// - public DistributedCacheEntryOptions RequestCachingPolicy { get; set; } = new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1), - SlidingExpiration = TimeSpan.FromMinutes(30) - }; - - /// - /// Gets or sets a boolean indicating whether PKCE must be used by client applications - /// when requesting an authorization code (e.g when using the code or hybrid flows). - /// If this property is set to true, authorization requests that lack the - /// code_challenge/code_challenge_method parameters will be automatically rejected. - /// - public bool RequireProofKeyForCodeExchange { get; set; } - - /// - /// Gets the OAuth2/OpenID Connect scopes enabled for this application. + /// Gets the OAuth 2.0/OpenID Connect scopes enabled for this application. /// public ISet Scopes { get; } = new HashSet(StringComparer.Ordinal) { diff --git a/src/OpenIddict.Server/OpenIddictServerProvider.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerProvider.Authentication.cs deleted file mode 100644 index af9f2dc8c..000000000 --- a/src/OpenIddict.Server/OpenIddictServerProvider.Authentication.cs +++ /dev/null @@ -1,547 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.IO; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Primitives; -using AspNet.Security.OpenIdConnect.Server; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json; -using Newtonsoft.Json.Bson; -using Newtonsoft.Json.Linq; -using OpenIddict.Abstractions; - -namespace OpenIddict.Server -{ - /// - /// Provides the logic necessary to extract, validate and handle OpenID Connect requests. - /// - internal sealed partial class OpenIddictServerProvider : OpenIdConnectServerProvider - { - public override async Task ExtractAuthorizationRequest([NotNull] ExtractAuthorizationRequestContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - // Reject requests using the unsupported request parameter. - if (!string.IsNullOrEmpty(context.Request.Request)) - { - _logger.LogError("The authorization request was rejected because it contained " + - "an unsupported parameter: {Parameter}.", "request"); - - context.Reject( - error: OpenIddictConstants.Errors.RequestNotSupported, - description: "The 'request' parameter is not supported."); - - return; - } - - // Reject requests using the unsupported request_uri parameter. - if (!string.IsNullOrEmpty(context.Request.RequestUri)) - { - _logger.LogError("The authorization request was rejected because it contained " + - "an unsupported parameter: {Parameter}.", "request_uri"); - - context.Reject( - error: OpenIddictConstants.Errors.RequestUriNotSupported, - description: "The 'request_uri' parameter is not supported."); - - return; - } - - // If a request_id parameter can be found in the authorization request, - // restore the complete authorization request from the distributed cache. - if (!string.IsNullOrEmpty(context.Request.RequestId)) - { - // Return an error if request caching support was not enabled. - if (!options.EnableRequestCaching) - { - _logger.LogError("The authorization request was rejected because " + - "request caching support was not enabled."); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The 'request_id' parameter is not supported."); - - return; - } - - // Note: the cache key is always prefixed with a specific marker - // to avoid collisions with the other types of cached requests. - var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId; - - var payload = await options.Cache.GetAsync(key); - if (payload == null) - { - _logger.LogError("The authorization request was rejected because an unknown " + - "or invalid request_id parameter was specified."); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The specified 'request_id' parameter is invalid."); - - return; - } - - // Restore the authorization request parameters from the serialized payload. - using (var reader = new BsonDataReader(new MemoryStream(payload))) - { - foreach (var parameter in JObject.Load(reader)) - { - // Avoid overriding the current request parameters. - if (context.Request.HasParameter(parameter.Key)) - { - continue; - } - - context.Request.SetParameter(parameter.Key, parameter.Value); - } - } - } - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ExtractAuthorizationRequest(context)); - } - - public override async Task ValidateAuthorizationRequest([NotNull] ValidateAuthorizationRequestContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - // Note: the OpenID Connect server middleware supports authorization code, implicit, hybrid, - // none and custom flows but OpenIddict uses a stricter policy rejecting none and custum flows. - if (!context.Request.IsAuthorizationCodeFlow() && !context.Request.IsHybridFlow() && !context.Request.IsImplicitFlow()) - { - _logger.LogError("The authorization request was rejected because the '{ResponseType}' " + - "response type is not supported.", context.Request.ResponseType); - - context.Reject( - error: OpenIddictConstants.Errors.UnsupportedResponseType, - description: "The specified 'response_type' parameter is not supported."); - - return; - } - - // Reject code flow authorization requests if the authorization code flow is not enabled. - if (context.Request.IsAuthorizationCodeFlow() && - !options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.AuthorizationCode)) - { - _logger.LogError("The authorization request was rejected because " + - "the authorization code flow was not enabled."); - - context.Reject( - error: OpenIddictConstants.Errors.UnsupportedResponseType, - description: "The specified 'response_type' parameter is not allowed."); - - return; - } - - // Reject implicit flow authorization requests if the implicit flow is not enabled. - if (context.Request.IsImplicitFlow() && !options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.Implicit)) - { - _logger.LogError("The authorization request was rejected because the implicit flow was not enabled."); - - context.Reject( - error: OpenIddictConstants.Errors.UnsupportedResponseType, - description: "The specified 'response_type' parameter is not allowed."); - - return; - } - - // Reject hybrid flow authorization requests if the authorization code or the implicit flows are not enabled. - if (context.Request.IsHybridFlow() && (!options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.AuthorizationCode) || - !options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.Implicit))) - { - _logger.LogError("The authorization request was rejected because the " + - "authorization code flow or the implicit flow was not enabled."); - - context.Reject( - error: OpenIddictConstants.Errors.UnsupportedResponseType, - description: "The specified 'response_type' parameter is not allowed."); - - return; - } - - // Reject authorization requests that specify scope=offline_access if the refresh token flow is not enabled. - if (context.Request.HasScope(OpenIddictConstants.Scopes.OfflineAccess) && - !options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.RefreshToken)) - { - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The 'offline_access' scope is not allowed."); - - return; - } - - // Validates scopes, unless scope validation was explicitly disabled. - if (!options.DisableScopeValidation) - { - var scopes = new HashSet(context.Request.GetScopes(), StringComparer.Ordinal); - scopes.ExceptWith(options.Scopes); - - // If all the specified scopes are registered in the options, avoid making a database lookup. - if (scopes.Count != 0) - { - foreach (var scope in await _scopeManager.FindByNamesAsync(scopes.ToImmutableArray())) - { - scopes.Remove(await _scopeManager.GetNameAsync(scope)); - } - } - - // If at least one scope was not recognized, return an error. - if (scopes.Count != 0) - { - _logger.LogError("The authentication request was rejected because invalid scopes were specified: {Scopes}.", scopes); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidScope, - description: "The specified 'scope' parameter is not valid."); - - return; - } - } - - // Note: the OpenID Connect server middleware supports the query, form_post and fragment response modes - // and doesn't reject unknown/custom modes until the ApplyAuthorizationResponse event is invoked. - // To ensure authorization requests are rejected early enough, an additional check is made by OpenIddict. - if (!string.IsNullOrEmpty(context.Request.ResponseMode) && !context.Request.IsFormPostResponseMode() && - !context.Request.IsFragmentResponseMode() && - !context.Request.IsQueryResponseMode()) - { - _logger.LogError("The authorization request was rejected because the '{ResponseMode}' " + - "response mode is not supported.", context.Request.ResponseMode); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The specified 'response_mode' parameter is not supported."); - - return; - } - - // Note: redirect_uri is not required for pure OAuth2 requests - // but this provider uses a stricter policy making it mandatory, - // as required by the OpenID Connect core specification. - // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest. - if (string.IsNullOrEmpty(context.RedirectUri)) - { - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The mandatory 'redirect_uri' parameter is missing."); - - return; - } - - // If OpenIddict was configured to require PKCE, reject the request if the code challenge - // is missing and if an authorization code was requested by the client application. - if (options.RequireProofKeyForCodeExchange && string.IsNullOrEmpty(context.Request.CodeChallenge) && - context.Request.HasResponseType(OpenIddictConstants.ResponseTypes.Code)) - { - _logger.LogError("The authorization request was rejected because the " + - "required 'code_challenge' parameter was missing."); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The mandatory 'code_challenge' parameter is missing."); - - return; - } - - // Note: the OpenID Connect server middleware always ensures a - // code_challenge_method can't be specified without code_challenge. - if (!string.IsNullOrEmpty(context.Request.CodeChallenge)) - { - // Since the default challenge method (plain) is explicitly disallowed, - // reject the authorization request if the code_challenge_method is missing. - if (string.IsNullOrEmpty(context.Request.CodeChallengeMethod)) - { - _logger.LogError("The authorization request was rejected because the " + - "required 'code_challenge_method' parameter was missing."); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The 'code_challenge_method' parameter must be specified."); - - return; - } - - // Disallow the use of the unsecure code_challenge_method=plain method. - // See https://tools.ietf.org/html/rfc7636#section-7.2 for more information. - if (string.Equals(context.Request.CodeChallengeMethod, OpenIdConnectConstants.CodeChallengeMethods.Plain)) - { - _logger.LogError("The authorization request was rejected because the " + - "'code_challenge_method' parameter was set to 'plain'."); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The specified 'code_challenge_method' parameter is not allowed."); - - return; - } - - // Reject authorization requests that contain response_type=token when a code_challenge is specified. - if (context.Request.HasResponseType(OpenIddictConstants.ResponseTypes.Token)) - { - _logger.LogError("The authorization request was rejected because the " + - "specified response type was not compatible with PKCE."); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The specified 'response_type' parameter is not allowed when using PKCE."); - - return; - } - } - - // Retrieve the application details corresponding to the requested client_id. - var application = await _applicationManager.FindByClientIdAsync(context.ClientId); - if (application == null) - { - _logger.LogError("The authorization request was rejected because the client " + - "application was not found: '{ClientId}'.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The specified 'client_id' parameter is invalid."); - - return; - } - - // To prevent downgrade attacks, ensure that authorization requests returning an access token directly - // from the authorization endpoint are rejected if the client_id corresponds to a confidential application. - // Note: when using the authorization code grant, ValidateTokenRequest is responsible of rejecting - // the token request if the client_id corresponds to an unauthenticated confidential client. - if (await _applicationManager.IsConfidentialAsync(application) && - context.Request.HasResponseType(OpenIddictConstants.ResponseTypes.Token)) - { - _logger.LogError("The authorization request was rejected because the confidential application '{ClientId}' " + - "was not allowed to retrieve an access token from the authorization endpoint.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.UnauthorizedClient, - description: "The specified 'response_type' parameter is not valid for this client application."); - - return; - } - - // Reject the request if the application is not allowed to use the authorization endpoint. - if (!options.IgnoreEndpointPermissions && - !await _applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Authorization)) - { - _logger.LogError("The authorization request was rejected because the application '{ClientId}' " + - "was not allowed to use the authorization endpoint.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.UnauthorizedClient, - description: "This client application is not allowed to use the authorization endpoint."); - - return; - } - - if (!options.IgnoreGrantTypePermissions) - { - // Reject the request if the application is not allowed to use the authorization code flow. - if (context.Request.IsAuthorizationCodeFlow() && !await _applicationManager.HasPermissionAsync( - application, OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode)) - { - _logger.LogError("The authorization request was rejected because the application '{ClientId}' " + - "was not allowed to use the authorization code flow.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.UnauthorizedClient, - description: "The client application is not allowed to use the authorization code flow."); - - return; - } - - // Reject the request if the application is not allowed to use the implicit flow. - if (context.Request.IsImplicitFlow() && !await _applicationManager.HasPermissionAsync( - application, OpenIddictConstants.Permissions.GrantTypes.Implicit)) - { - _logger.LogError("The authorization request was rejected because the application '{ClientId}' " + - "was not allowed to use the implicit flow.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.UnauthorizedClient, - description: "The client application is not allowed to use the implicit flow."); - - return; - } - - // Reject the request if the application is not allowed to use the authorization code/implicit flows. - if (context.Request.IsHybridFlow() && - (!await _applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode) || - !await _applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.GrantTypes.Implicit))) - { - _logger.LogError("The authorization request was rejected because the application '{ClientId}' " + - "was not allowed to use the hybrid flow.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.UnauthorizedClient, - description: "The client application is not allowed to use the hybrid flow."); - - return; - } - - // Reject the request if the offline_access scope was request and if - // the application is not allowed to use the refresh token grant type. - if (context.Request.HasScope(OpenIddictConstants.Scopes.OfflineAccess) && - !await _applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.GrantTypes.RefreshToken)) - { - _logger.LogError("The authorization request was rejected because the application '{ClientId}' " + - "was not allowed to request the 'offline_access' scope.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The client application is not allowed to use the 'offline_access' scope."); - - return; - } - } - - // Ensure that the specified redirect_uri is valid and is associated with the client application. - if (!await _applicationManager.ValidateRedirectUriAsync(application, context.RedirectUri)) - { - _logger.LogError("The authorization request was rejected because the redirect_uri " + - "was invalid: '{RedirectUri}'.", context.RedirectUri); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The specified 'redirect_uri' parameter is not valid for this client application."); - - return; - } - - // Unless permission enforcement was explicitly disabled, ensure - // the client application is allowed to use the specified scopes. - if (!options.IgnoreScopePermissions) - { - foreach (var scope in context.Request.GetScopes()) - { - // Avoid validating the "openid" and "offline_access" scopes as they represent protocol scopes. - if (string.Equals(scope, OpenIddictConstants.Scopes.OfflineAccess, StringComparison.Ordinal) || - string.Equals(scope, OpenIddictConstants.Scopes.OpenId, StringComparison.Ordinal)) - { - continue; - } - - // Reject the request if the application is not allowed to use the iterated scope. - if (!await _applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.Prefixes.Scope + scope)) - { - _logger.LogError("The authorization request was rejected because the application '{ClientId}' " + - "was not allowed to use the scope {Scope}.", context.ClientId, scope); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "This client application is not allowed to use the specified scope."); - - return; - } - } - } - - context.Validate(); - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ValidateAuthorizationRequest(context)); - } - - public override async Task HandleAuthorizationRequest([NotNull] HandleAuthorizationRequestContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - // If no request_id parameter can be found in the current request, assume the OpenID Connect request - // was not serialized yet and store the entire payload in the distributed cache to make it easier - // to flow across requests and internal/external authentication/registration workflows. - if (options.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId)) - { - // Generate a 256-bit request identifier using a crypto-secure random number generator. - var bytes = new byte[256 / 8]; - options.RandomNumberGenerator.GetBytes(bytes); - context.Request.RequestId = Base64UrlEncoder.Encode(bytes); - - // Store the serialized authorization request parameters in the distributed cache. - var stream = new MemoryStream(); - using (var writer = new BsonDataWriter(stream)) - { - writer.CloseOutput = false; - - var serializer = JsonSerializer.CreateDefault(); - serializer.Serialize(writer, context.Request); - } - - // Note: the cache key is always prefixed with a specific marker - // to avoid collisions with the other types of cached requests. - var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId; - - await options.Cache.SetAsync(key, stream.ToArray(), options.RequestCachingPolicy); - - // Create a new authorization request containing only the request_id parameter. - var address = QueryHelpers.AddQueryString( - uri: context.HttpContext.Request.Scheme + "://" + context.HttpContext.Request.Host + - context.HttpContext.Request.PathBase + context.HttpContext.Request.Path, - name: OpenIddictConstants.Parameters.RequestId, value: context.Request.RequestId); - - context.HttpContext.Response.Redirect(address); - - // Mark the response as handled - // to skip the rest of the pipeline. - context.HandleResponse(); - - return; - } - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.HandleAuthorizationRequest(context)); - } - - public override async Task ApplyAuthorizationResponse([NotNull] ApplyAuthorizationResponseContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - // Note: as this stage, the request associated with the context may be null if an error - // occurred very early in the pipeline (e.g an invalid HTTP verb was used by the caller). - - // Remove the authorization request from the distributed cache. - if (options.EnableRequestCaching && !string.IsNullOrEmpty(context.Request?.RequestId)) - { - // Note: the cache key is always prefixed with a specific marker - // to avoid collisions with the other types of cached requests. - var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId; - - // Note: the ApplyAuthorizationResponse event is called for both successful - // and errored authorization responses but discrimination is not necessary here, - // as the authorization request must be removed from the distributed cache in both cases. - await options.Cache.RemoveAsync(key); - } - - if (!options.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) && - string.IsNullOrEmpty(context.RedirectUri)) - { - // Determine if the status code pages middleware has been enabled for this request. - // If it was not registered or enabled, let the OpenID Connect server middleware render - // a default error page instead of delegating the rendering to the status code middleware. - var feature = context.HttpContext.Features.Get(); - if (feature != null && feature.Enabled) - { - // Replace the default status code to return a 400 response. - context.HttpContext.Response.StatusCode = 400; - - // Mark the request as fully handled to prevent the OpenID Connect server middleware - // from displaying the default error page and to allow the status code pages middleware - // to rewrite the response using the logic defined by the developer when registering it. - context.HandleResponse(); - - return; - } - } - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ApplyAuthorizationResponse(context)); - } - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerProvider.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerProvider.Discovery.cs deleted file mode 100644 index 6ac051672..000000000 --- a/src/OpenIddict.Server/OpenIddictServerProvider.Discovery.cs +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Primitives; -using AspNet.Security.OpenIdConnect.Server; -using JetBrains.Annotations; - -namespace OpenIddict.Server -{ - /// - /// Provides the logic necessary to extract, validate and handle OpenID Connect requests. - /// - internal sealed partial class OpenIddictServerProvider : OpenIdConnectServerProvider - { - public override Task ExtractConfigurationRequest([NotNull] ExtractConfigurationRequestContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ExtractConfigurationRequest(context)); - - public override Task ValidateConfigurationRequest([NotNull] ValidateConfigurationRequestContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ValidateConfigurationRequest(context)); - - public override Task HandleConfigurationRequest([NotNull] HandleConfigurationRequestContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - // Note: though it's natively supported by the OpenID Connect server middleware, - // OpenIddict disallows the use of the unsecure code_challenge_method=plain method, - // which is manually removed from the code_challenge_methods_supported property. - // See https://tools.ietf.org/html/rfc7636#section-7.2 for more information. - context.CodeChallengeMethods.Remove(OpenIdConnectConstants.CodeChallengeMethods.Plain); - - // Note: the OpenID Connect server middleware automatically populates grant_types_supported - // by determining whether the authorization and token endpoints are enabled or not but - // OpenIddict uses a different approach and relies on a configurable "grants list". - context.GrantTypes.Clear(); - context.GrantTypes.UnionWith(options.GrantTypes); - - // Only return the scopes and the claims configured by the developer. - context.Scopes.Clear(); - context.Scopes.UnionWith(options.Scopes); - context.Claims.Clear(); - context.Claims.UnionWith(options.Claims); - - // Note: the optional claims/request/request_uri parameters are not supported - // by OpenIddict, so "false" is returned to encourage clients not to use them. - context.Metadata[OpenIdConnectConstants.Metadata.ClaimsParameterSupported] = false; - context.Metadata[OpenIdConnectConstants.Metadata.RequestParameterSupported] = false; - context.Metadata[OpenIdConnectConstants.Metadata.RequestUriParameterSupported] = false; - - return _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.HandleConfigurationRequest(context)); - } - - public override Task ApplyConfigurationResponse([NotNull] ApplyConfigurationResponseContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ApplyConfigurationResponse(context)); - - public override Task ExtractCryptographyRequest([NotNull] ExtractCryptographyRequestContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ExtractCryptographyRequest(context)); - - public override Task ValidateCryptographyRequest([NotNull] ValidateCryptographyRequestContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ValidateCryptographyRequest(context)); - - public override Task HandleCryptographyRequest([NotNull] HandleCryptographyRequestContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.HandleCryptographyRequest(context)); - - public override Task ApplyCryptographyResponse([NotNull] ApplyCryptographyResponseContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ApplyCryptographyResponse(context)); - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerProvider.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerProvider.Exchange.cs deleted file mode 100644 index e67fa85b5..000000000 --- a/src/OpenIddict.Server/OpenIddictServerProvider.Exchange.cs +++ /dev/null @@ -1,446 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Primitives; -using AspNet.Security.OpenIdConnect.Server; -using JetBrains.Annotations; -using Microsoft.Extensions.Logging; -using OpenIddict.Abstractions; - -namespace OpenIddict.Server -{ - /// - /// Provides the logic necessary to extract, validate and handle OpenID Connect requests. - /// - internal sealed partial class OpenIddictServerProvider : OpenIdConnectServerProvider - { - public override Task ExtractTokenRequest([NotNull] ExtractTokenRequestContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ExtractTokenRequest(context)); - - public override async Task ValidateTokenRequest([NotNull] ValidateTokenRequestContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - // Reject token requests that don't specify a supported grant type. - if (!options.GrantTypes.Contains(context.Request.GrantType)) - { - _logger.LogError("The token request was rejected because the '{GrantType}' " + - "grant type is not supported.", context.Request.GrantType); - - context.Reject( - error: OpenIddictConstants.Errors.UnsupportedGrantType, - description: "The specified 'grant_type' parameter is not supported."); - - return; - } - - // Reject token requests that specify scope=offline_access if the refresh token flow is not enabled. - if (context.Request.HasScope(OpenIddictConstants.Scopes.OfflineAccess) && - !options.GrantTypes.Contains(OpenIddictConstants.GrantTypes.RefreshToken)) - { - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The 'offline_access' scope is not allowed."); - - return; - } - - if (context.Request.IsAuthorizationCodeGrantType()) - { - // Optimization: the OpenID Connect server middleware automatically rejects grant_type=authorization_code - // requests missing the redirect_uri parameter if one was specified in the initial authorization request. - // Since OpenIddict doesn't allow redirect_uri-less authorization requests, an earlier check can be made here, - // which saves the OpenID Connect server middleware from having to deserialize the authorization code ticket. - // See http://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation for more information. - if (string.IsNullOrEmpty(context.Request.RedirectUri)) - { - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The mandatory 'redirect_uri' parameter is missing."); - - return; - } - - // Optimization: the OpenID Connect server middleware automatically rejects grant_type=authorization_code - // requests missing the code_verifier parameter when a challenge was specified in the authorization request. - // That check requires decrypting the authorization code and determining whether a code challenge was set. - // If OpenIddict was configured to require PKCE, this can be potentially avoided by making an early check here. - if (options.RequireProofKeyForCodeExchange && string.IsNullOrEmpty(context.Request.CodeVerifier)) - { - _logger.LogError("The token request was rejected because the required 'code_verifier' parameter was missing."); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The mandatory 'code_verifier' parameter is missing."); - - return; - } - } - - // Note: the OpenID Connect server middleware allows returning a refresh token with grant_type=client_credentials, - // though it's usually not recommended by the OAuth2 specification. To encourage developers to make a new - // grant_type=client_credentials request instead of using refresh tokens, OpenIddict uses a stricter policy - // that rejects grant_type=client_credentials requests containing the 'offline_access' scope. - // See https://tools.ietf.org/html/rfc6749#section-4.4.3 for more information. - if (context.Request.IsClientCredentialsGrantType() && - context.Request.HasScope(OpenIddictConstants.Scopes.OfflineAccess)) - { - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The 'offline_access' scope is not valid for the specified 'grant_type' parameter."); - - return; - } - - // Validates scopes, unless scope validation was explicitly disabled. - if (!options.DisableScopeValidation) - { - var scopes = new HashSet(context.Request.GetScopes(), StringComparer.Ordinal); - scopes.ExceptWith(options.Scopes); - - // If all the specified scopes are registered in the options, avoid making a database lookup. - if (scopes.Count != 0) - { - foreach (var scope in await _scopeManager.FindByNamesAsync(scopes.ToImmutableArray())) - { - scopes.Remove(await _scopeManager.GetNameAsync(scope)); - } - } - - // If at least one scope was not recognized, return an error. - if (scopes.Count != 0) - { - _logger.LogError("The token request was rejected because invalid scopes were specified: {Scopes}.", scopes); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidScope, - description: "The specified 'scope' parameter is not valid."); - - return; - } - } - - // Optimization: the OpenID Connect server middleware automatically rejects grant_type=client_credentials - // requests when validation is skipped but an earlier check is made here to avoid making unnecessary - // database roundtrips to retrieve the client application corresponding to the client_id. - if (context.Request.IsClientCredentialsGrantType() && (string.IsNullOrEmpty(context.Request.ClientId) || - string.IsNullOrEmpty(context.Request.ClientSecret))) - { - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The 'client_id' and 'client_secret' parameters are " + - "required when using the client credentials grant."); - - return; - } - - // At this stage, skip client authentication if the client identifier is missing - // or reject the token request if client identification is set as required. - // Note: the OpenID Connect server middleware will automatically ensure that - // the calling application cannot use an authorization code or a refresh token - // if it's not the intended audience, even if client authentication was skipped. - if (string.IsNullOrEmpty(context.ClientId)) - { - // Reject the request if client identification is mandatory. - if (!options.AcceptAnonymousClients) - { - _logger.LogError("The token request was rejected becaused the " + - "mandatory client_id parameter was missing or empty."); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The mandatory 'client_id' parameter is missing."); - - return; - } - - _logger.LogDebug("The token request validation process was partially skipped " + - "because the 'client_id' parameter was missing or empty."); - - context.Skip(); - - return; - } - - // Retrieve the application details corresponding to the requested client_id. - var application = await _applicationManager.FindByClientIdAsync(context.ClientId); - if (application == null) - { - _logger.LogError("The token request was rejected because the client " + - "application was not found: '{ClientId}'.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidClient, - description: "The specified 'client_id' parameter is invalid."); - - return; - } - - // Reject the request if the application is not allowed to use the token endpoint. - if (!options.IgnoreEndpointPermissions && - !await _applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Token)) - { - _logger.LogError("The token request was rejected because the application '{ClientId}' " + - "was not allowed to use the token endpoint.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.UnauthorizedClient, - description: "This client application is not allowed to use the token endpoint."); - - return; - } - - if (!options.IgnoreGrantTypePermissions) - { - // Reject the request if the application is not allowed to use the specified grant type. - if (!await _applicationManager.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.GrantType + context.Request.GrantType)) - { - _logger.LogError("The token request was rejected because the application '{ClientId}' was not allowed to " + - "use the specified grant type: {GrantType}.", context.ClientId, context.Request.GrantType); - - context.Reject( - error: OpenIddictConstants.Errors.UnauthorizedClient, - description: "This client application is not allowed to use the specified grant type."); - - return; - } - - // Reject the request if the offline_access scope was request and if - // the application is not allowed to use the refresh token grant type. - if (context.Request.HasScope(OpenIddictConstants.Scopes.OfflineAccess) && - !await _applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.GrantTypes.RefreshToken)) - { - _logger.LogError("The token request was rejected because the application '{ClientId}' " + - "was not allowed to request the 'offline_access' scope.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The client application is not allowed to use the 'offline_access' scope."); - - return; - } - } - - // Unless permission enforcement was explicitly disabled, ensure - // the client application is allowed to use the specified scopes. - if (!options.IgnoreScopePermissions) - { - foreach (var scope in context.Request.GetScopes()) - { - // Avoid validating the "openid" and "offline_access" scopes as they represent protocol scopes. - if (string.Equals(scope, OpenIddictConstants.Scopes.OfflineAccess, StringComparison.Ordinal) || - string.Equals(scope, OpenIddictConstants.Scopes.OpenId, StringComparison.Ordinal)) - { - continue; - } - - // Reject the request if the application is not allowed to use the iterated scope. - if (!await _applicationManager.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + scope)) - { - _logger.LogError("The token request was rejected because the application '{ClientId}' " + - "was not allowed to use the scope {Scope}.", context.ClientId, scope); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "This client application is not allowed to use the specified scope."); - - return; - } - } - } - - if (await _applicationManager.IsPublicAsync(application)) - { - // Note: public applications are not allowed to use the client credentials grant. - if (context.Request.IsClientCredentialsGrantType()) - { - _logger.LogError("The token request was rejected because the public client application '{ClientId}' " + - "was not allowed to use the client credentials grant.", context.Request.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.UnauthorizedClient, - description: "The specified 'grant_type' parameter is not valid for this client application."); - - return; - } - - // Reject token requests containing a client_secret when the client is a public application. - if (!string.IsNullOrEmpty(context.ClientSecret)) - { - _logger.LogError("The token request was rejected because the public application '{ClientId}' " + - "was not allowed to send a client secret.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The 'client_secret' parameter is not valid for this client application."); - - return; - } - - _logger.LogDebug("The token request validation process was not fully validated because " + - "the client '{ClientId}' was a public application.", context.ClientId); - - // If client authentication cannot be enforced, call context.Skip() to inform - // the OpenID Connect server middleware that the caller cannot be fully trusted. - context.Skip(); - - return; - } - - // Confidential and hybrid applications MUST authenticate - // to protect them from impersonation attacks. - if (string.IsNullOrEmpty(context.ClientSecret)) - { - _logger.LogError("The token request was rejected because the confidential or hybrid application " + - "'{ClientId}' didn't specify a client secret.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidClient, - description: "The 'client_secret' parameter required for this client application is missing."); - - return; - } - - if (!await _applicationManager.ValidateClientSecretAsync(application, context.ClientSecret)) - { - _logger.LogError("The token request was rejected because the confidential or hybrid application " + - "'{ClientId}' didn't specify valid client credentials.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidClient, - description: "The specified client credentials are invalid."); - - return; - } - - context.Validate(); - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ValidateTokenRequest(context)); - } - - public override async Task HandleTokenRequest([NotNull] HandleTokenRequestContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - if (context.Ticket != null) - { - // Store the authentication ticket as a request property so it can be later retrieved, if necessary. - context.Request.SetProperty(OpenIddictConstants.Properties.AuthenticationTicket, context.Ticket); - } - - if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType()) - { - // Invoke the rest of the pipeline to allow - // the user code to handle the token request. - context.SkipHandler(); - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.HandleTokenRequest(context)); - - return; - } - - Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null."); - - // Unless token revocation was explicitly disabled, ensure - // the authorization code/refresh token is still valid. - if (!options.DisableTokenStorage) - { - // Extract the token identifier from the authentication ticket. - var identifier = context.Ticket.GetInternalTokenId(); - Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a token identifier."); - - // If the authorization code/refresh token is already marked as redeemed, this may indicate that - // it was compromised. In this case, revoke the authorization and all the associated tokens. - // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information. - var token = await _tokenManager.FindByIdAsync(identifier); - if (token == null || await _tokenManager.IsRedeemedAsync(token)) - { - if (token != null) - { - await TryRevokeTokenAsync(token); - } - - // Try to revoke the authorization and the associated tokens. - // If the operation fails, the helpers will automatically log - // and swallow the exception to ensure that a valid error - // response will be returned to the client application. - if (!options.DisableAuthorizationStorage) - { - await TryRevokeAuthorizationAsync(context.Ticket); - await TryRevokeTokensAsync(context.Ticket); - } - - _logger.LogError("The token request was rejected because the authorization code " + - "or refresh token '{Identifier}' has already been redeemed.", identifier); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidGrant, - description: context.Request.IsAuthorizationCodeGrantType() ? - "The specified authorization code has already been redeemed." : - "The specified refresh token has already been redeemed."); - - return; - } - - else if (!await _tokenManager.IsValidAsync(token)) - { - _logger.LogError("The token request was rejected because the authorization code " + - "or refresh token '{Identifier}' was no longer valid.", identifier); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidGrant, - description: context.Request.IsAuthorizationCodeGrantType() ? - "The specified authorization code is no longer valid." : - "The specified refresh token is no longer valid."); - - return; - } - } - - // Unless authorization revocation was explicitly disabled, ensure the - // authorization associated with the code/refresh token is still valid. - if (!options.DisableAuthorizationStorage) - { - // Extract the authorization identifier from the authentication ticket. - var identifier = context.Ticket.GetInternalAuthorizationId(); - if (!string.IsNullOrEmpty(identifier)) - { - var authorization = await _authorizationManager.FindByIdAsync(identifier); - if (authorization == null || !await _authorizationManager.IsValidAsync(authorization)) - { - _logger.LogError("The token '{Identifier}' was rejected because " + - "the associated authorization was no longer valid."); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidGrant, - description: context.Request.IsAuthorizationCodeGrantType() ? - "The authorization associated with the authorization code is no longer valid." : - "The authorization associated with the refresh token is no longer valid."); - - return; - } - } - } - - // Invoke the rest of the pipeline to allow - // the user code to handle the token request. - context.SkipHandler(); - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.HandleTokenRequest(context)); - } - - public override Task ApplyTokenResponse([NotNull] ApplyTokenResponseContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ApplyTokenResponse(context)); - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerProvider.Helpers.cs b/src/OpenIddict.Server/OpenIddictServerProvider.Helpers.cs deleted file mode 100644 index 5117ef84e..000000000 --- a/src/OpenIddict.Server/OpenIddictServerProvider.Helpers.cs +++ /dev/null @@ -1,645 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Primitives; -using AspNet.Security.OpenIdConnect.Server; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json.Linq; -using OpenIddict.Abstractions; - -namespace OpenIddict.Server -{ - /// - /// Provides the logic necessary to extract, validate and handle OpenID Connect requests. - /// - internal sealed partial class OpenIddictServerProvider : OpenIdConnectServerProvider - { - private async Task CreateAuthorizationAsync([NotNull] AuthenticationTicket ticket, - [NotNull] OpenIddictServerOptions options, [NotNull] OpenIdConnectRequest request) - { - var descriptor = new OpenIddictAuthorizationDescriptor - { - Principal = ticket.Principal, - Status = OpenIddictConstants.Statuses.Valid, - Subject = ticket.Principal.GetClaim(OpenIddictConstants.Claims.Subject), - Type = OpenIddictConstants.AuthorizationTypes.AdHoc - }; - - foreach (var property in ticket.Properties.Items) - { - descriptor.Properties.Add(property); - } - - foreach (var scope in ticket.GetScopes()) - { - descriptor.Scopes.Add(scope); - } - - // If the client application is known, bind it to the authorization. - if (!string.IsNullOrEmpty(request.ClientId)) - { - var application = await _applicationManager.FindByClientIdAsync(request.ClientId); - if (application == null) - { - throw new InvalidOperationException("The application entry cannot be found in the database."); - } - - descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); - } - - var authorization = await _authorizationManager.CreateAsync(descriptor); - if (authorization != null) - { - var identifier = await _authorizationManager.GetIdAsync(authorization); - - if (string.IsNullOrEmpty(request.ClientId)) - { - _logger.LogInformation("An ad hoc authorization was automatically created and " + - "associated with an unknown application: {Identifier}.", identifier); - } - - else - { - _logger.LogInformation("An ad hoc authorization was automatically created and " + - "associated with the '{ClientId}' application: {Identifier}.", - request.ClientId, identifier); - } - - // Attach the unique identifier of the ad hoc authorization to the authentication ticket - // so that it is attached to all the derived tokens, allowing batched revocations support. - ticket.SetInternalAuthorizationId(identifier); - } - } - - private async Task CreateTokenAsync( - [NotNull] string type, [NotNull] AuthenticationTicket ticket, - [NotNull] OpenIddictServerOptions options, - [NotNull] OpenIdConnectRequest request, - [NotNull] ISecureDataFormat format) - { - Debug.Assert(!(options.DisableTokenStorage && options.UseReferenceTokens), - "Token storage cannot be disabled when using reference tokens."); - - Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken || - type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || - type == OpenIdConnectConstants.TokenUsages.RefreshToken, - "Only authorization codes, access and refresh tokens should be created using this method."); - - // When sliding expiration is disabled, the expiration date of generated refresh tokens is fixed - // and must exactly match the expiration date of the refresh token used in the token request. - if (request.IsTokenRequest() && request.IsRefreshTokenGrantType() && - !options.UseSlidingExpiration && type == OpenIdConnectConstants.TokenUsages.RefreshToken) - { - var properties = request.GetProperty( - OpenIddictConstants.Properties.AuthenticationTicket)?.Properties; - Debug.Assert(properties != null, "The authentication properties shouldn't be null."); - - ticket.Properties.ExpiresUtc = properties.ExpiresUtc; - } - - if (options.DisableTokenStorage) - { - return null; - } - - var descriptor = new OpenIddictTokenDescriptor - { - AuthorizationId = ticket.GetInternalAuthorizationId(), - CreationDate = ticket.Properties.IssuedUtc, - ExpirationDate = ticket.Properties.ExpiresUtc, - Principal = ticket.Principal, - Status = OpenIddictConstants.Statuses.Valid, - Subject = ticket.Principal.GetClaim(OpenIddictConstants.Claims.Subject), - Type = type - }; - - foreach (var property in ticket.Properties.Items) - { - descriptor.Properties.Add(property); - } - - // When reference tokens are enabled or when the token is an authorization code or a - // refresh token, remove the unnecessary properties from the authentication ticket. - if (options.UseReferenceTokens || - (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || - type == OpenIdConnectConstants.TokenUsages.RefreshToken)) - { - ticket.Properties.IssuedUtc = ticket.Properties.ExpiresUtc = null; - ticket.RemoveProperty(OpenIddictConstants.Properties.InternalAuthorizationId) - .RemoveProperty(OpenIddictConstants.Properties.InternalTokenId); - } - - // If reference tokens are enabled, create a new entry for - // authorization codes, refresh tokens and access tokens. - if (options.UseReferenceTokens) - { - // Note: the data format is automatically replaced at startup time to ensure - // that encrypted tokens stored in the database cannot be considered as - // valid tokens if the developer decides to disable reference tokens support. - descriptor.Payload = format.Protect(ticket); - - // Generate a new crypto-secure random identifier that will be - // substituted to the ciphertext returned by the data format. - var bytes = new byte[256 / 8]; - options.RandomNumberGenerator.GetBytes(bytes); - - // Note: the default token manager automatically obfuscates the - // reference identifier so it can be safely stored in the databse. - descriptor.ReferenceId = Base64UrlEncoder.Encode(bytes); - } - - // Otherwise, only create a token metadata entry for authorization codes and refresh tokens. - else if (type != OpenIdConnectConstants.TokenUsages.AuthorizationCode && - type != OpenIdConnectConstants.TokenUsages.RefreshToken) - { - return null; - } - - // If the client application is known, associate it with the token. - if (!string.IsNullOrEmpty(request.ClientId)) - { - var application = await _applicationManager.FindByClientIdAsync(request.ClientId); - if (application == null) - { - throw new InvalidOperationException("The application entry cannot be found in the database."); - } - - descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); - } - - // If a null value was returned by CreateAsync(), return immediately. - - // Note: the request cancellation token is deliberately not used here to ensure the caller - // cannot prevent this operation from being executed by resetting the TCP connection. - var token = await _tokenManager.CreateAsync(descriptor); - if (token == null) - { - return null; - } - - // Throw an exception if the token identifier can't be resolved. - var identifier = await _tokenManager.GetIdAsync(token); - if (string.IsNullOrEmpty(identifier)) - { - throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty."); - } - - // Dynamically set the creation and expiration dates. - ticket.Properties.IssuedUtc = descriptor.CreationDate; - ticket.Properties.ExpiresUtc = descriptor.ExpirationDate; - - // Restore the token/authorization identifiers using the identifiers attached with the database entry. - ticket.SetInternalAuthorizationId(descriptor.AuthorizationId) - .SetInternalTokenId(identifier); - - if (options.UseReferenceTokens) - { - _logger.LogTrace("A new reference token was successfully generated and persisted " + - "in the database: {Token} ; {Claims} ; {Properties}.", - descriptor.ReferenceId, ticket.Principal.Claims, ticket.Properties.Items); - - return descriptor.ReferenceId; - } - - return null; - } - - private async Task ReceiveTokenAsync( - [NotNull] string type, [NotNull] string value, - [NotNull] OpenIddictServerOptions options, - [NotNull] OpenIdConnectRequest request, - [NotNull] ISecureDataFormat format) - { - Debug.Assert(!(options.DisableTokenStorage && options.UseReferenceTokens), - "Token revocation cannot be disabled when using reference tokens."); - - Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken || - type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || - type == OpenIdConnectConstants.TokenUsages.RefreshToken, - "Only authorization codes, access and refresh tokens should be validated using this method."); - - string identifier; - AuthenticationTicket ticket; - object token; - - if (options.UseReferenceTokens) - { - token = await _tokenManager.FindByReferenceIdAsync(value); - if (token == null) - { - _logger.LogInformation("The reference token corresponding to the '{Identifier}' " + - "reference identifier cannot be found in the database.", value); - - return null; - } - - // Optimization: avoid extracting/decrypting the token payload - // (that relies on a format specific to the token type requested) - // if the token type associated with the token entry isn't valid. - var usage = await _tokenManager.GetTypeAsync(token); - if (string.IsNullOrEmpty(usage)) - { - _logger.LogWarning("The token type associated with the received token cannot be retrieved. " + - "This may indicate that the token entry is corrupted."); - - return null; - } - - if (!string.Equals(usage, type, StringComparison.OrdinalIgnoreCase)) - { - _logger.LogWarning("The token type '{ActualType}' associated with the database entry doesn't match " + - "the expected type: {ExpectedType}.", await _tokenManager.GetTypeAsync(token), type); - - return null; - } - - identifier = await _tokenManager.GetIdAsync(token); - if (string.IsNullOrEmpty(identifier)) - { - _logger.LogWarning("The identifier associated with the received token cannot be retrieved. " + - "This may indicate that the token entry is corrupted."); - - return null; - } - - // Extract the encrypted payload from the token. If it's null or empty, - // assume the token is not a reference token and consider it as invalid. - var payload = await _tokenManager.GetPayloadAsync(token); - if (string.IsNullOrEmpty(payload)) - { - _logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be retrieved. " + - "This may indicate that the token is not a reference token.", identifier); - - return null; - } - - ticket = format.Unprotect(payload); - if (ticket == null) - { - _logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be decrypted. " + - "This may indicate that the token entry is corrupted or tampered.", - await _tokenManager.GetIdAsync(token)); - - return null; - } - } - - else if (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || - type == OpenIdConnectConstants.TokenUsages.RefreshToken) - { - ticket = format.Unprotect(value); - if (ticket == null) - { - _logger.LogTrace("The received token was invalid or malformed: {Token}.", value); - - return null; - } - - identifier = ticket.GetInternalTokenId(); - if (string.IsNullOrEmpty(identifier)) - { - _logger.LogWarning("The identifier associated with the received token cannot be retrieved. " + - "This may indicate that the token entry is corrupted."); - - return null; - } - - token = await _tokenManager.FindByIdAsync(identifier); - if (token == null) - { - _logger.LogInformation("The token '{Identifier}' cannot be found in the database.", identifier); - - return null; - } - } - - else - { - return null; - } - - // Dynamically set the creation and expiration dates. - ticket.Properties.IssuedUtc = await _tokenManager.GetCreationDateAsync(token); - ticket.Properties.ExpiresUtc = await _tokenManager.GetExpirationDateAsync(token); - - // Restore the token/authorization identifiers using the identifiers attached with the database entry. - ticket.SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) - .SetInternalTokenId(identifier); - - _logger.LogTrace("The token '{Identifier}' was successfully decrypted and " + - "retrieved from the database: {Claims} ; {Properties}.", - identifier, ticket.Principal.Claims, ticket.Properties.Items); - - return ticket; - } - - private async Task TryRevokeAuthorizationAsync([NotNull] AuthenticationTicket ticket) - { - // Note: if the authorization identifier or the authorization itself - // cannot be found, return true as the authorization doesn't need - // to be revoked if it doesn't exist or is already invalid. - var identifier = ticket.GetInternalAuthorizationId(); - if (string.IsNullOrEmpty(identifier)) - { - return true; - } - - var authorization = await _authorizationManager.FindByIdAsync(identifier); - if (authorization == null) - { - return true; - } - - try - { - // Note: the request cancellation token is deliberately not used here to ensure the caller - // cannot prevent this operation from being executed by resetting the TCP connection. - await _authorizationManager.RevokeAsync(authorization); - - _logger.LogInformation("The authorization '{Identifier}' was automatically revoked.", identifier); - - return true; - } - - catch (OpenIddictExceptions.ConcurrencyException exception) - { - _logger.LogDebug(exception, "A concurrency exception occurred while trying to revoke the authorization " + - "associated with the token '{Identifier}'.", identifier); - - return false; - } - - catch (Exception exception) - { - _logger.LogWarning(exception, "An exception occurred while trying to revoke the authorization " + - "associated with the token '{Identifier}'.", identifier); - - return false; - } - } - - private async Task TryRevokeTokenAsync([NotNull] object token) - { - var identifier = await _tokenManager.GetIdAsync(token); - Debug.Assert(!string.IsNullOrEmpty(identifier), "The token identifier shouldn't be null or empty."); - - try - { - // Note: the request cancellation token is deliberately not used here to ensure the caller - // cannot prevent this operation from being executed by resetting the TCP connection. - await _tokenManager.RevokeAsync(token); - - _logger.LogInformation("The token '{Identifier}' was automatically revoked.", identifier); - - return true; - } - - catch (OpenIddictExceptions.ConcurrencyException exception) - { - _logger.LogDebug(exception, "A concurrency exception occurred while trying to revoke the token '{Identifier}'.", identifier); - - return false; - } - - catch (Exception exception) - { - _logger.LogWarning(exception, "An exception occurred while trying to revoke the token '{Identifier}'.", identifier); - - return false; - } - } - - private async Task TryRevokeTokensAsync([NotNull] AuthenticationTicket ticket) - { - // Note: if the authorization identifier is null, return true as no tokens need to be revoked. - var identifier = ticket.GetInternalAuthorizationId(); - if (string.IsNullOrEmpty(identifier)) - { - return true; - } - - var result = true; - - foreach (var token in await _tokenManager.FindByAuthorizationIdAsync(identifier)) - { - // Don't change the status of the token used in the token request. - if (string.Equals(ticket.GetInternalTokenId(), - await _tokenManager.GetIdAsync(token), StringComparison.Ordinal)) - { - continue; - } - - result &= await TryRevokeTokenAsync(token); - } - - return result; - } - - private async Task TryRedeemTokenAsync([NotNull] object token) - { - var identifier = await _tokenManager.GetIdAsync(token); - Debug.Assert(!string.IsNullOrEmpty(identifier), "The token identifier shouldn't be null or empty."); - - try - { - // Note: the request cancellation token is deliberately not used here to ensure the caller - // cannot prevent this operation from being executed by resetting the TCP connection. - await _tokenManager.RedeemAsync(token); - - _logger.LogInformation("The token '{Identifier}' was automatically marked as redeemed.", identifier); - - return true; - } - - catch (OpenIddictExceptions.ConcurrencyException exception) - { - _logger.LogDebug(exception, "A concurrency exception occurred while trying to redeem with the token '{Identifier}'.", identifier); - - return false; - } - - catch (Exception exception) - { - _logger.LogWarning(exception, "An exception occurred while trying to redeem the token '{Identifier}'.", identifier); - - return false; - } - } - - private async Task TryExtendRefreshTokenAsync( - [NotNull] object token, [NotNull] AuthenticationTicket ticket, [NotNull] OpenIddictServerOptions options) - { - var identifier = ticket.GetInternalTokenId(); - Debug.Assert(!string.IsNullOrEmpty(identifier), "The token identifier shouldn't be null or empty."); - - try - { - // Compute the new expiration date of the refresh token. - var lifetime = ticket.GetRefreshTokenLifetime() ?? options.RefreshTokenLifetime; - if (lifetime != null) - { - // Note: the request cancellation token is deliberately not used here to ensure the caller - // cannot prevent this operation from being executed by resetting the TCP connection. - var date = options.SystemClock.UtcNow + lifetime.Value; - await _tokenManager.ExtendAsync(token, date); - - _logger.LogInformation("The expiration date of the refresh token '{Identifier}' " + - "was automatically updated: {Date}.", identifier, date); - } - - else if (await _tokenManager.GetExpirationDateAsync(token) != null) - { - // Note: the request cancellation token is deliberately not used here to ensure the caller - // cannot prevent this operation from being executed by resetting the TCP connection. - await _tokenManager.ExtendAsync(token, date: null); - - _logger.LogInformation("The expiration date of the refresh token '{Identifier}' was removed.", identifier); - } - - return true; - } - - catch (OpenIddictExceptions.ConcurrencyException exception) - { - _logger.LogDebug(exception, "A concurrency exception occurred while trying to update the " + - "expiration date of the token '{Identifier}'.", identifier); - - return false; - } - - catch (Exception exception) - { - _logger.LogWarning(exception, "An exception occurred while trying to update the " + - "expiration date of the token '{Identifier}'.", identifier); - - return false; - } - } - - private IEnumerable<(string property, string parameter, OpenIdConnectParameter value)> GetParameters( - OpenIdConnectRequest request, AuthenticationProperties properties) - { - Debug.Assert(properties != null, "The authentication properties shouldn't be null."); - - Debug.Assert(request != null, "The request shouldn't be null."); - Debug.Assert(request.IsAuthorizationRequest() || request.IsLogoutRequest() || request.IsTokenRequest(), - "The request should be an authorization, logout or token request."); - - foreach (var property in properties.Items) - { - if (string.IsNullOrEmpty(property.Key)) - { - continue; - } - - if (string.IsNullOrEmpty(property.Value)) - { - continue; - } - - if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Boolean)) - { - var name = property.Key.Substring( - startIndex: 0, - length: property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.Boolean)); - - bool value; - - try - { - value = bool.Parse(property.Value); - } - - catch (Exception exception) - { - _logger.LogWarning(exception, "An error occurred while parsing the public property " + - "'{Name}' from the authentication ticket.", name); - - continue; - } - - yield return (property.Key, name, value); - } - - else if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Integer)) - { - var name = property.Key.Substring( - startIndex: 0, - length: property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.Integer)); - - long value; - - try - { - value = long.Parse(property.Value, CultureInfo.InvariantCulture); - } - - catch (Exception exception) - { - _logger.LogWarning(exception, "An error occurred while parsing the public property " + - "'{Name}' from the authentication ticket.", name); - - continue; - } - - yield return (property.Key, name, value); - } - - else if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Json)) - { - var name = property.Key.Substring( - startIndex: 0, - length: property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.Json)); - - if (request.IsAuthorizationRequest() || request.IsLogoutRequest()) - { - _logger.LogWarning("The JSON property '{Name}' was excluded as it was not " + - "compatible with the OpenID Connect response type.", name); - - continue; - } - - JToken value; - - try - { - value = JToken.Parse(property.Value); - } - - catch (Exception exception) - { - _logger.LogWarning(exception, "An error occurred while deserializing the public JSON " + - "property '{Name}' from the authentication ticket.", name); - - continue; - } - - yield return (property.Key, name, value); - } - - else if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.String)) - { - var name = property.Key.Substring( - startIndex: 0, - length: property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.String)); - - yield return (property.Key, name, property.Value); - } - - continue; - } - } - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerProvider.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerProvider.Introspection.cs deleted file mode 100644 index faec8357b..000000000 --- a/src/OpenIddict.Server/OpenIddictServerProvider.Introspection.cs +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Diagnostics; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Server; -using JetBrains.Annotations; -using Microsoft.Extensions.Logging; -using OpenIddict.Abstractions; - -namespace OpenIddict.Server -{ - /// - /// Provides the logic necessary to extract, validate and handle OpenID Connect requests. - /// - internal sealed partial class OpenIddictServerProvider : OpenIdConnectServerProvider - { - public override Task ExtractIntrospectionRequest([NotNull] ExtractIntrospectionRequestContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ExtractIntrospectionRequest(context)); - - public override async Task ValidateIntrospectionRequest([NotNull] ValidateIntrospectionRequestContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - // Note: the OpenID Connect server middleware supports unauthenticated introspection requests - // but OpenIddict uses a stricter policy preventing unauthenticated/public applications - // from using the introspection endpoint, as required by the specifications. - // See https://tools.ietf.org/html/rfc7662#section-2.1 for more information. - if (string.IsNullOrEmpty(context.ClientId) || string.IsNullOrEmpty(context.ClientSecret)) - { - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The mandatory 'client_id' and/or 'client_secret' parameters are missing."); - - return; - } - - // Retrieve the application details corresponding to the requested client_id. - var application = await _applicationManager.FindByClientIdAsync(context.ClientId); - if (application == null) - { - _logger.LogError("The introspection request was rejected because the client " + - "application was not found: '{ClientId}'.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidClient, - description: "The specified 'client_id' parameter is invalid."); - - return; - } - - // Reject the request if the application is not allowed to use the introspection endpoint. - if (!options.IgnoreEndpointPermissions && - !await _applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Introspection)) - { - _logger.LogError("The introspection request was rejected because the application '{ClientId}' " + - "was not allowed to use the introspection endpoint.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.UnauthorizedClient, - description: "This client application is not allowed to use the introspection endpoint."); - - return; - } - - // Reject introspection requests sent by public applications. - if (await _applicationManager.IsPublicAsync(application)) - { - _logger.LogError("The introspection request was rejected because the public application " + - "'{ClientId}' was not allowed to use this endpoint.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidClient, - description: "This client application is not allowed to use the introspection endpoint."); - - return; - } - - // Validate the client credentials. - if (!await _applicationManager.ValidateClientSecretAsync(application, context.ClientSecret)) - { - _logger.LogError("The introspection request was rejected because the confidential or hybrid application " + - "'{ClientId}' didn't specify valid client credentials.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidClient, - description: "The specified client credentials are invalid."); - - return; - } - - context.Validate(); - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ValidateIntrospectionRequest(context)); - } - - public override async Task HandleIntrospectionRequest([NotNull] HandleIntrospectionRequestContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null."); - Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId), "The client_id parameter shouldn't be null."); - - var identifier = context.Ticket.GetInternalTokenId(); - Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a token identifier."); - - if (!context.Ticket.IsAccessToken()) - { - _logger.LogError("The token '{Identifier}' is not an access token and thus cannot be introspected.", identifier); - - context.Active = false; - - return; - } - - // Note: the OpenID Connect server middleware allows authorized presenters (e.g relying parties) to introspect - // tokens but OpenIddict uses a stricter policy that only allows resource servers to use the introspection endpoint. - // For that, an error is automatically returned if no explicit audience is attached to the authentication ticket. - if (!context.Ticket.HasAudience()) - { - _logger.LogError("The token '{Identifier}' doesn't have any audience attached " + - "and cannot be introspected. To add an audience, use the " + - "'ticket.SetResources(...)' extension when creating the ticket.", identifier); - - context.Active = false; - - return; - } - - if (!context.Ticket.HasAudience(context.Request.ClientId)) - { - _logger.LogError("The client application '{ClientId}' is not allowed to introspect the access " + - "token '{Identifier}' because it's not listed as a valid audience.", - context.Request.ClientId, identifier); - - context.Active = false; - - return; - } - - // If an authorization was attached to the access token, ensure it is still valid. - if (!options.DisableAuthorizationStorage && !string.IsNullOrEmpty(context.Ticket.GetInternalAuthorizationId())) - { - var authorization = await _authorizationManager.FindByIdAsync(context.Ticket.GetInternalAuthorizationId()); - if (authorization == null || !await _authorizationManager.IsValidAsync(authorization)) - { - _logger.LogError("The token '{Identifier}' was declared as inactive because " + - "the associated authorization was no longer valid.", identifier); - - context.Active = false; - - return; - } - } - - // If the received token is a reference access token - i.e a token for - // which an entry exists in the database - ensure it is still valid. - if (options.UseReferenceTokens) - { - var token = await _tokenManager.FindByIdAsync(identifier); - if (token == null || !await _tokenManager.IsValidAsync(token)) - { - _logger.LogInformation("The token '{Identifier}' was declared as inactive because it was " + - "not found in the database or was no longer valid.", identifier); - - context.Active = false; - - return; - } - } - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.HandleIntrospectionRequest(context)); - } - - public override Task ApplyIntrospectionResponse([NotNull] ApplyIntrospectionResponseContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ApplyIntrospectionResponse(context)); - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerProvider.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerProvider.Revocation.cs deleted file mode 100644 index 4aa33697d..000000000 --- a/src/OpenIddict.Server/OpenIddictServerProvider.Revocation.cs +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Primitives; -using AspNet.Security.OpenIdConnect.Server; -using JetBrains.Annotations; -using Microsoft.Extensions.Logging; -using OpenIddict.Abstractions; - -namespace OpenIddict.Server -{ - /// - /// Provides the logic necessary to extract, validate and handle OpenID Connect requests. - /// - internal sealed partial class OpenIddictServerProvider : OpenIdConnectServerProvider - { - public override Task ExtractRevocationRequest([NotNull] ExtractRevocationRequestContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ExtractRevocationRequest(context)); - - public override async Task ValidateRevocationRequest([NotNull] ValidateRevocationRequestContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - Debug.Assert(!options.DisableTokenStorage, "Token storage support shouldn't be disabled at this stage."); - - // When token_type_hint is specified, reject the request if it doesn't correspond to a revocable token. - if (!string.IsNullOrEmpty(context.Request.TokenTypeHint)) - { - if (string.Equals(context.Request.TokenTypeHint, OpenIdConnectConstants.TokenTypeHints.IdToken)) - { - context.Reject( - error: OpenIddictConstants.Errors.UnsupportedTokenType, - description: "The specified 'token_type_hint' parameter is not supported."); - - return; - } - - if (!options.UseReferenceTokens && - string.Equals(context.Request.TokenTypeHint, OpenIdConnectConstants.TokenTypeHints.AccessToken)) - { - context.Reject( - error: OpenIddictConstants.Errors.UnsupportedTokenType, - description: "The specified 'token_type_hint' parameter is not supported."); - - return; - } - } - - // Skip client authentication if the client identifier is missing or reject - // the revocation request if client identification is set as required. - // Note: the OpenID Connect server middleware will automatically ensure that - // the calling application cannot revoke a refresh token if it's not - // the intended audience, even if client authentication was skipped. - if (string.IsNullOrEmpty(context.ClientId)) - { - // Reject the request if client identification is mandatory. - if (!options.AcceptAnonymousClients) - { - _logger.LogError("The revocation request was rejected becaused the " + - "mandatory client_id parameter was missing or empty."); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The mandatory 'client_id' parameter is missing."); - - return; - } - - _logger.LogDebug("The revocation request validation process was skipped " + - "because the client_id parameter was missing or empty."); - - context.Skip(); - - return; - } - - // Retrieve the application details corresponding to the requested client_id. - var application = await _applicationManager.FindByClientIdAsync(context.ClientId); - if (application == null) - { - _logger.LogError("The revocation request was rejected because the client " + - "application was not found: '{ClientId}'.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidClient, - description: "The specified 'client_id' parameter is invalid."); - - return; - } - - // Reject the request if the application is not allowed to use the revocation endpoint. - if (!options.IgnoreEndpointPermissions && - !await _applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Revocation)) - { - _logger.LogError("The revocation request was rejected because the application '{ClientId}' " + - "was not allowed to use the revocation endpoint.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.UnauthorizedClient, - description: "This client application is not allowed to use the revocation endpoint."); - - return; - } - - // Reject revocation requests containing a client_secret if the application is a public client. - if (await _applicationManager.IsPublicAsync(application)) - { - if (!string.IsNullOrEmpty(context.ClientSecret)) - { - _logger.LogError("The revocation request was rejected because the public application " + - "'{ClientId}' was not allowed to use this endpoint.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The 'client_secret' parameter is not valid for this client application."); - - return; - } - - _logger.LogDebug("The revocation request validation process was not fully validated because " + - "the client '{ClientId}' was a public application.", context.ClientId); - - // If client authentication cannot be enforced, call context.Skip() to inform - // the OpenID Connect server middleware that the caller cannot be fully trusted. - context.Skip(); - - return; - } - - // Confidential and hybrid applications MUST authenticate - // to protect them from impersonation attacks. - if (string.IsNullOrEmpty(context.ClientSecret)) - { - _logger.LogError("The revocation request was rejected because the confidential or hybrid application " + - "'{ClientId}' didn't specify a client secret.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidClient, - description: "The 'client_secret' parameter required for this client application is missing."); - - return; - } - - if (!await _applicationManager.ValidateClientSecretAsync(application, context.ClientSecret)) - { - _logger.LogError("The revocation request was rejected because the confidential or hybrid application " + - "'{ClientId}' didn't specify valid client credentials.", context.ClientId); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidClient, - description: "The specified client credentials are invalid."); - - return; - } - - context.Validate(); - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ValidateRevocationRequest(context)); - } - - public override async Task HandleRevocationRequest([NotNull] HandleRevocationRequestContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null."); - - // If the received token is not an authorization code or a refresh token, - // return an error to indicate that the token cannot be revoked. - if (context.Ticket.IsIdentityToken()) - { - _logger.LogError("The revocation request was rejected because identity tokens are not revocable."); - - context.Reject( - error: OpenIddictConstants.Errors.UnsupportedTokenType, - description: "The specified token cannot be revoked."); - - return; - } - - // If the received token is an access token, return an error if reference tokens are not enabled. - if (!options.UseReferenceTokens && context.Ticket.IsAccessToken()) - { - _logger.LogError("The revocation request was rejected because the access token was not revocable."); - - context.Reject( - error: OpenIddictConstants.Errors.UnsupportedTokenType, - description: "The specified token cannot be revoked."); - - return; - } - - // Extract the token identifier from the authentication ticket. - var identifier = context.Ticket.GetInternalTokenId(); - Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a token identifier."); - - var token = await _tokenManager.FindByIdAsync(identifier); - if (token == null || await _tokenManager.IsRevokedAsync(token)) - { - _logger.LogInformation("The token '{Identifier}' was not revoked because " + - "it was already marked as invalid.", identifier); - - context.Revoked = true; - - return; - } - - // Try to revoke the token. If an exception is thrown, - // log and swallow it to ensure that a valid response - // will be returned to the client application. - try - { - await _tokenManager.RevokeAsync(token); - } - - catch (Exception exception) - { - _logger.LogWarning(exception, "An exception occurred while trying to revoke the authorization " + - "associated with the token '{Identifier}'.", identifier); - - return; - } - - _logger.LogInformation("The token '{Identifier}' was successfully revoked.", identifier); - - context.Revoked = true; - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.HandleRevocationRequest(context)); - } - - public override Task ApplyRevocationResponse([NotNull] ApplyRevocationResponseContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ApplyRevocationResponse(context)); - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerProvider.Serialization.cs b/src/OpenIddict.Server/OpenIddictServerProvider.Serialization.cs deleted file mode 100644 index 30044732e..000000000 --- a/src/OpenIddict.Server/OpenIddictServerProvider.Serialization.cs +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Diagnostics; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Primitives; -using AspNet.Security.OpenIdConnect.Server; -using JetBrains.Annotations; - -namespace OpenIddict.Server -{ - /// - /// Provides the logic necessary to extract, validate and handle OpenID Connect requests. - /// - internal sealed partial class OpenIddictServerProvider : OpenIdConnectServerProvider - { - public override async Task DeserializeAccessToken([NotNull] DeserializeAccessTokenContext context) - { - var options = (OpenIddictServerOptions) context.Options; - if (options.DisableTokenStorage) - { - return; - } - - context.Ticket = await ReceiveTokenAsync( - OpenIdConnectConstants.TokenUsages.AccessToken, - context.AccessToken, options, - context.Request, context.DataFormat); - - // Prevent the OpenID Connect server middleware from using - // its default logic to deserialize reference access tokens. - if (options.UseReferenceTokens) - { - context.HandleDeserialization(); - } - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.DeserializeAccessToken(context)); - } - - public override async Task DeserializeAuthorizationCode([NotNull] DeserializeAuthorizationCodeContext context) - { - var options = (OpenIddictServerOptions) context.Options; - if (options.DisableTokenStorage) - { - return; - } - - context.Ticket = await ReceiveTokenAsync( - OpenIdConnectConstants.TokenUsages.AuthorizationCode, - context.AuthorizationCode, options, - context.Request, context.DataFormat); - - // Prevent the OpenID Connect server middleware from using its default logic. - context.HandleDeserialization(); - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.DeserializeAuthorizationCode(context)); - } - - public override Task DeserializeIdentityToken(DeserializeIdentityTokenContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.DeserializeIdentityToken(context)); - - public override async Task DeserializeRefreshToken([NotNull] DeserializeRefreshTokenContext context) - { - var options = (OpenIddictServerOptions) context.Options; - if (options.DisableTokenStorage) - { - return; - } - - context.Ticket = await ReceiveTokenAsync( - OpenIdConnectConstants.TokenUsages.RefreshToken, - context.RefreshToken, options, - context.Request, context.DataFormat); - - // Prevent the OpenID Connect server middleware from using its default logic. - context.HandleDeserialization(); - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.DeserializeRefreshToken(context)); - } - - public override async Task SerializeAccessToken([NotNull] SerializeAccessTokenContext context) - { - var options = (OpenIddictServerOptions) context.Options; - if (options.DisableTokenStorage) - { - return; - } - - var token = await CreateTokenAsync( - OpenIdConnectConstants.TokenUsages.AccessToken, - context.Ticket, options, context.Request, context.DataFormat); - - // If a reference token was returned by CreateTokenAsync(), - // force the OpenID Connect server middleware to use it. - if (!string.IsNullOrEmpty(token)) - { - context.AccessToken = token; - context.HandleSerialization(); - } - - // Otherwise, let the OpenID Connect server middleware - // serialize the token using its default internal logic. - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.SerializeAccessToken(context)); - } - - public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context) - { - var options = (OpenIddictServerOptions) context.Options; - if (options.DisableTokenStorage) - { - return; - } - - Debug.Assert(context.Request.IsAuthorizationRequest(), "The request should be an authorization request."); - - var token = await CreateTokenAsync( - OpenIdConnectConstants.TokenUsages.AuthorizationCode, - context.Ticket, options, context.Request, context.DataFormat); - - // If a reference token was returned by CreateTokenAsync(), - // force the OpenID Connect server middleware to use it. - if (!string.IsNullOrEmpty(token)) - { - context.AuthorizationCode = token; - context.HandleSerialization(); - } - - // Otherwise, let the OpenID Connect server middleware - // serialize the token using its default internal logic. - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.SerializeAuthorizationCode(context)); - } - - public override Task SerializeIdentityToken(SerializeIdentityTokenContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.SerializeIdentityToken(context)); - - public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) - { - var options = (OpenIddictServerOptions) context.Options; - if (options.DisableTokenStorage) - { - return; - } - - Debug.Assert(context.Request.IsTokenRequest(), "The request should be a token request."); - - var token = await CreateTokenAsync( - OpenIdConnectConstants.TokenUsages.RefreshToken, - context.Ticket, options, context.Request, context.DataFormat); - - // If a reference token was returned by CreateTokenAsync(), - // force the OpenID Connect server middleware to use it. - if (!string.IsNullOrEmpty(token)) - { - context.RefreshToken = token; - context.HandleSerialization(); - } - - // Otherwise, let the OpenID Connect server middleware - // serialize the token using its default internal logic. - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.SerializeRefreshToken(context)); - } - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerProvider.Session.cs b/src/OpenIddict.Server/OpenIddictServerProvider.Session.cs deleted file mode 100644 index 68b351099..000000000 --- a/src/OpenIddict.Server/OpenIddictServerProvider.Session.cs +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.IO; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Primitives; -using AspNet.Security.OpenIdConnect.Server; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json; -using Newtonsoft.Json.Bson; -using Newtonsoft.Json.Linq; -using OpenIddict.Abstractions; - -namespace OpenIddict.Server -{ - /// - /// Provides the logic necessary to extract, validate and handle OpenID Connect requests. - /// - internal sealed partial class OpenIddictServerProvider : OpenIdConnectServerProvider - { - public override async Task ExtractLogoutRequest([NotNull] ExtractLogoutRequestContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - // If a request_id parameter can be found in the logout request, - // restore the complete logout request from the distributed cache. - if (!string.IsNullOrEmpty(context.Request.RequestId)) - { - // Return an error if request caching support was not enabled. - if (!options.EnableRequestCaching) - { - _logger.LogError("The logout request was rejected because request caching support was not enabled."); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The 'request_id' parameter is not supported."); - - return; - } - - // Note: the cache key is always prefixed with a specific marker - // to avoid collisions with the other types of cached requests. - var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId; - - var payload = await options.Cache.GetAsync(key); - if (payload == null) - { - _logger.LogError("The logout request was rejected because an unknown " + - "or invalid request_id parameter was specified."); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The specified 'request_id' parameter is invalid."); - - return; - } - - // Restore the logout request parameters from the serialized payload. - using (var reader = new BsonDataReader(new MemoryStream(payload))) - { - foreach (var parameter in JObject.Load(reader)) - { - // Avoid overriding the current request parameters. - if (context.Request.HasParameter(parameter.Key)) - { - continue; - } - - context.Request.SetParameter(parameter.Key, parameter.Value); - } - } - } - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ExtractLogoutRequest(context)); - } - - public override async Task ValidateLogoutRequest([NotNull] ValidateLogoutRequestContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - // If an optional post_logout_redirect_uri was provided, validate it. - if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri)) - { - if (!Uri.TryCreate(context.PostLogoutRedirectUri, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString()) - { - _logger.LogError("The logout request was rejected because the specified post_logout_redirect_uri was not " + - "a valid absolute URL: {PostLogoutRedirectUri}.", context.PostLogoutRedirectUri); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The 'post_logout_redirect_uri' parameter must be a valid absolute URL."); - - return; - } - - if (!string.IsNullOrEmpty(uri.Fragment)) - { - _logger.LogError("The logout request was rejected because the 'post_logout_redirect_uri' contained " + - "a URL fragment: {PostLogoutRedirectUri}.", context.PostLogoutRedirectUri); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The 'post_logout_redirect_uri' parameter must not include a fragment."); - - return; - } - - async Task ValidatePostLogoutRedirectUriAsync(string address) - { - var applications = await _applicationManager.FindByPostLogoutRedirectUriAsync(address); - if (applications.IsDefaultOrEmpty) - { - return false; - } - - if (options.IgnoreEndpointPermissions) - { - return true; - } - - foreach (var application in applications) - { - if (await _applicationManager.HasPermissionAsync( - application, OpenIddictConstants.Permissions.Endpoints.Logout)) - { - return true; - } - } - - return false; - } - - if (!await ValidatePostLogoutRedirectUriAsync(context.PostLogoutRedirectUri)) - { - _logger.LogError("The logout request was rejected because no application with the specified " + - "post_logout_redirect_uri and with a logout endpoint permission was found: " + - "{PostLogoutRedirectUri}.", context.PostLogoutRedirectUri); - - context.Reject( - error: OpenIddictConstants.Errors.InvalidRequest, - description: "The specified 'post_logout_redirect_uri' parameter is not valid."); - - return; - } - } - - context.Validate(); - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ValidateLogoutRequest(context)); - } - - public override async Task HandleLogoutRequest([NotNull] HandleLogoutRequestContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - // If no request_id parameter can be found in the current request, assume the OpenID Connect - // request was not serialized yet and store the entire payload in the distributed cache - // to make it easier to flow across requests and internal/external logout workflows. - if (options.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId)) - { - // Generate a 256-bit request identifier using a crypto-secure random number generator. - var bytes = new byte[256 / 8]; - options.RandomNumberGenerator.GetBytes(bytes); - context.Request.RequestId = Base64UrlEncoder.Encode(bytes); - - // Store the serialized logout request parameters in the distributed cache. - var stream = new MemoryStream(); - using (var writer = new BsonDataWriter(stream)) - { - writer.CloseOutput = false; - - var serializer = JsonSerializer.CreateDefault(); - serializer.Serialize(writer, context.Request); - } - - // Note: the cache key is always prefixed with a specific marker - // to avoid collisions with the other types of cached requests. - var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId; - - await options.Cache.SetAsync(key, stream.ToArray(), options.RequestCachingPolicy); - - // Create a new logout request containing only the request_id parameter. - var address = QueryHelpers.AddQueryString( - uri: context.HttpContext.Request.Scheme + "://" + context.HttpContext.Request.Host + - context.HttpContext.Request.PathBase + context.HttpContext.Request.Path, - name: OpenIddictConstants.Parameters.RequestId, value: context.Request.RequestId); - - context.HttpContext.Response.Redirect(address); - - // Mark the response as handled - // to skip the rest of the pipeline. - context.HandleResponse(); - - return; - } - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.HandleLogoutRequest(context)); - } - - public override async Task ApplyLogoutResponse([NotNull] ApplyLogoutResponseContext context) - { - var options = (OpenIddictServerOptions) context.Options; - - // Note: as this stage, the request associated with the context may be null if an error - // occurred very early in the pipeline (e.g an invalid HTTP verb was used by the caller). - - // Remove the logout request from the distributed cache. - if (options.EnableRequestCaching && !string.IsNullOrEmpty(context.Request?.RequestId)) - { - // Note: the cache key is always prefixed with a specific marker - // to avoid collisions with the other types of cached requests. - var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId; - - // Note: the ApplyLogoutResponse event is called for both successful - // and errored logout responses but discrimination is not necessary here, - // as the logout request must be removed from the distributed cache in both cases. - await options.Cache.RemoveAsync(key); - } - - if (!options.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) && - string.IsNullOrEmpty(context.PostLogoutRedirectUri)) - { - // Determine if the status code pages middleware has been enabled for this request. - // If it was not registered or enabled, let the OpenID Connect server middleware render - // a default error page instead of delegating the rendering to the status code middleware. - var feature = context.HttpContext.Features.Get(); - if (feature != null && feature.Enabled) - { - // Replace the default status code by a 400 response. - context.HttpContext.Response.StatusCode = 400; - - // Mark the request as fully handled to prevent the OpenID Connect server middleware - // from displaying the default error page and to allow the status code pages middleware - // to rewrite the response using the logic defined by the developer when registering it. - context.HandleResponse(); - - return; - } - } - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ApplyLogoutResponse(context)); - } - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerProvider.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerProvider.Userinfo.cs deleted file mode 100644 index 0a7589d1d..000000000 --- a/src/OpenIddict.Server/OpenIddictServerProvider.Userinfo.cs +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Server; -using JetBrains.Annotations; - -namespace OpenIddict.Server -{ - /// - /// Provides the logic necessary to extract, validate and handle OpenID Connect requests. - /// - internal sealed partial class OpenIddictServerProvider : OpenIdConnectServerProvider - { - public override Task ExtractUserinfoRequest([NotNull] ExtractUserinfoRequestContext context) - { - // Note: when enabling the userinfo endpoint, OpenIddict users are intended - // to handle the userinfo requests in their own code (e.g in a MVC controller). - // To avoid validating the access token twice, the default logic enforced by - // the OpenID Connect server is bypassed using the ExtractUserinfoRequest event, - // which is invoked before the access token is extracted from the userinfo request. - - // Invoke the rest of the pipeline to allow - // the user code to handle the userinfo request. - context.SkipHandler(); - - return _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ExtractUserinfoRequest(context)); - } - - public override Task ValidateUserinfoRequest([NotNull] ValidateUserinfoRequestContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ValidateUserinfoRequest(context)); - - public override Task HandleUserinfoRequest([NotNull] HandleUserinfoRequestContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.HandleUserinfoRequest(context)); - - public override Task ApplyUserinfoResponse([NotNull] ApplyUserinfoResponseContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ApplyUserinfoResponse(context)); - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerProvider.cs b/src/OpenIddict.Server/OpenIddictServerProvider.cs index a6c5c92f7..af720c922 100644 --- a/src/OpenIddict.Server/OpenIddictServerProvider.cs +++ b/src/OpenIddict.Server/OpenIddictServerProvider.cs @@ -5,238 +5,137 @@ */ using System; -using System.Diagnostics; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Primitives; -using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; -using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; -using OpenIddict.Abstractions; +using Microsoft.Extensions.Options; +using static OpenIddict.Server.OpenIddictServerEvents; namespace OpenIddict.Server { - /// - /// Provides the logic necessary to extract, validate and handle OpenID Connect requests. - /// - internal sealed partial class OpenIddictServerProvider : OpenIdConnectServerProvider + public class OpenIddictServerProvider : IOpenIddictServerProvider { - private readonly ILogger _logger; - private readonly IOpenIddictServerEventDispatcher _eventDispatcher; - private readonly IOpenIddictApplicationManager _applicationManager; - private readonly IOpenIddictAuthorizationManager _authorizationManager; - private readonly IOpenIddictScopeManager _scopeManager; - private readonly IOpenIddictTokenManager _tokenManager; + private readonly ILogger _logger; + private readonly IOptionsMonitor _options; + private readonly IServiceProvider _provider; /// /// Creates a new instance of the class. - /// Note: this API supports the OpenIddict infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future minor releases. /// public OpenIddictServerProvider( [NotNull] ILogger logger, - [NotNull] IOpenIddictServerEventDispatcher eventDispatcher, - [NotNull] IOpenIddictApplicationManager applicationManager, - [NotNull] IOpenIddictAuthorizationManager authorizationManager, - [NotNull] IOpenIddictScopeManager scopeManager, - [NotNull] IOpenIddictTokenManager tokenManager) + [NotNull] IOptionsMonitor options, + [NotNull] IServiceProvider provider) { _logger = logger; - _eventDispatcher = eventDispatcher; - _applicationManager = applicationManager; - _authorizationManager = authorizationManager; - _scopeManager = scopeManager; - _tokenManager = tokenManager; + _options = options; + _provider = provider; } - public override Task MatchEndpoint([NotNull] MatchEndpointContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.MatchEndpoint(context)); - - public override Task ProcessChallengeResponse([NotNull] ProcessChallengeResponseContext context) - { - Debug.Assert(context.Request.IsAuthorizationRequest() || - context.Request.IsTokenRequest(), - "The request should be an authorization or token request."); - - // Add the custom properties that are marked as public - // as authorization or token response properties. - var parameters = GetParameters(context.Request, context.Properties); - foreach (var (property, parameter, value) in parameters) + public ValueTask CreateTransactionAsync() + => new ValueTask(new OpenIddictServerTransaction { - context.Response.AddParameter(parameter, value); - } + Logger = _logger, + Options = _options.CurrentValue + }); - return _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ProcessChallengeResponse(context)); - } - - public override async Task ProcessSigninResponse([NotNull] ProcessSigninResponseContext context) + public async Task DispatchAsync([NotNull] TContext context) where TContext : BaseContext { - var options = (OpenIddictServerOptions) context.Options; - - Debug.Assert(context.Request.IsAuthorizationRequest() || - context.Request.IsTokenRequest(), - "The request should be an authorization or token request."); - - // While null/unauthenticated identities can be validly represented and are allowed by - // the OpenID Connect server handler, this most likely indicates that the developer - // has not correctly set the authentication type associated with the claims identity, - // which may later cause issues when validating opaque access tokens, as the resulting - // principal would be considered unauthenticated by the ASP.NET Core authorization stack. - if (context.Ticket.Principal.Identity == null || !context.Ticket.Principal.Identity.IsAuthenticated) + if (context == null) { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The specified principal doesn't contain a valid or authenticated identity.") - .Append("Make sure that both 'ClaimsPrincipal.Identity' and 'ClaimsPrincipal.Identity.AuthenticationType' ") - .Append("are not null and that 'ClaimsPrincipal.Identity.IsAuthenticated' returns 'true'.") - .ToString()); + throw new ArgumentNullException(nameof(context)); } - if (context.Request.IsTokenRequest() && (context.Request.IsAuthorizationCodeGrantType() || - context.Request.IsRefreshTokenGrantType())) + await foreach (var handler in GetHandlersAsync()) { - // Note: when handling a grant_type=authorization_code or refresh_token request, - // the OpenID Connect server middleware allows creating authentication tickets - // that are completely disconnected from the original code or refresh token ticket. - // This scenario is deliberately not supported in OpenIddict and all the tickets - // must be linked. To ensure the properties are flowed from the authorization code - // or the refresh token to the new ticket, they are manually restored if necessary. - if (string.IsNullOrEmpty(context.Ticket.GetInternalTokenId())) + await handler.HandleAsync(context); + + switch (context) { - // Retrieve the original authentication ticket from the request properties. - var ticket = context.Request.GetProperty( - OpenIddictConstants.Properties.AuthenticationTicket); - Debug.Assert(ticket != null, "The authentication ticket shouldn't be null."); + case BaseRequestContext notification when notification.IsRequestHandled: + _logger.LogDebug("The request was handled in user code."); + return; - foreach (var property in ticket.Properties.Items) - { - // Don't override the properties that have been - // manually set on the new authentication ticket. - if (context.Ticket.HasProperty(property.Key)) - { - continue; - } - - context.Ticket.AddProperty(property.Key, property.Value); - } + case BaseRequestContext notification when notification.IsRequestSkipped: + _logger.LogDebug("The default request handling was skipped from user code."); + return; - // Always include the "openid" scope when the developer doesn't explicitly call SetScopes. - // Note: the application is allowed to specify a different "scopes": in this case, - // don't replace the "scopes" property stored in the authentication ticket. - if (context.Request.HasScope(OpenIddictConstants.Scopes.OpenId) && !context.Ticket.HasScope()) - { - context.Ticket.SetScopes(OpenIddictConstants.Scopes.OpenId); - } + case BaseValidatingContext notification when notification.IsRejected: + _logger.LogDebug("The request was rejected in user code."); + return; - context.IncludeIdentityToken = context.Ticket.HasScope(OpenIddictConstants.Scopes.OpenId); - } + case BaseValidatingTicketContext notification when notification.IsHandled: + _logger.LogDebug("Authentication was handled in user code."); + return; - context.IncludeRefreshToken = context.Ticket.HasScope(OpenIddictConstants.Scopes.OfflineAccess); + case BaseDeserializingContext notification when notification.IsHandled: + _logger.LogDebug("Token deserialization was handled in user code."); + return; - // Always include a refresh token for grant_type=refresh_token requests if - // rolling tokens are enabled and if the offline_access scope was specified. - if (context.Request.IsRefreshTokenGrantType()) - { - context.IncludeRefreshToken &= options.UseRollingTokens; + case BaseSerializingContext notification when notification.IsHandled: + _logger.LogDebug("Token serialization was handled in user code."); + return; + + default: continue; } + } - // If token revocation was explicitly disabled, none of the following security routines apply. - if (!options.DisableTokenStorage) - { - var token = await _tokenManager.FindByIdAsync(context.Ticket.GetInternalTokenId()); - if (token == null) - { - context.Reject( - error: OpenIddictConstants.Errors.InvalidGrant, - description: context.Request.IsAuthorizationCodeGrantType() ? - "The specified authorization code is no longer valid." : - "The specified refresh token is no longer valid."); + async IAsyncEnumerable> GetHandlersAsync() + { + var descriptors = new List( + capacity: _options.CurrentValue.CustomHandlers.Count + + _options.CurrentValue.DefaultHandlers.Count); - return; - } + descriptors.AddRange(_options.CurrentValue.CustomHandlers); + descriptors.AddRange(_options.CurrentValue.DefaultHandlers); - // If rolling tokens are enabled or if the request is a grant_type=authorization_code request, - // mark the authorization code or the refresh token as redeemed to prevent future reuses. - // If the operation fails, return an error indicating the code/token is no longer valid. - // See https://tools.ietf.org/html/rfc6749#section-6 for more information. - if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType()) + foreach (var descriptor in descriptors.OrderBy(descriptor => descriptor.Order)) + { + if (descriptor.ContextType != typeof(TContext) || !await IsActiveAsync(descriptor)) { - if (!await TryRedeemTokenAsync(token)) - { - context.Reject( - error: OpenIddictConstants.Errors.InvalidGrant, - description: context.Request.IsAuthorizationCodeGrantType() ? - "The specified authorization code is no longer valid." : - "The specified refresh token is no longer valid."); - - return; - } + continue; } - if (context.Request.IsRefreshTokenGrantType()) + var handler = descriptor.ServiceDescriptor.ImplementationInstance != null ? + descriptor.ServiceDescriptor.ImplementationInstance as IOpenIddictServerHandler : + _provider.GetService(descriptor.ServiceDescriptor.ServiceType) as IOpenIddictServerHandler; + + if (handler == null) { - // When rolling tokens are enabled, try to revoke all the previously issued tokens - // associated with the authorization if the request is a refresh_token request. - // If the operation fails, silently ignore the error and keep processing the request: - // this may indicate that one of the revoked tokens was modified by a concurrent request. - if (options.UseRollingTokens) - { - await TryRevokeTokensAsync(context.Ticket); - } - - // When rolling tokens are disabled, try to extend the expiration date - // of the existing token instead of returning a new refresh token - // with a new expiration date if sliding expiration was not disabled. - // If the operation fails, silently ignore the error and keep processing - // the request: this may indicate that a concurrent refresh token request - // already updated the expiration date associated with the refresh token. - if (!options.UseRollingTokens && options.UseSlidingExpiration) - { - await TryExtendRefreshTokenAsync(token, context.Ticket, options); - } + throw new InvalidOperationException(new StringBuilder() + .AppendLine($"The event handler of type '{descriptor.ServiceDescriptor.ServiceType}' couldn't be resolved.") + .AppendLine("This may indicate that it was not properly registered in the dependency injection container.") + .Append("To register an event handler, use 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .ToString()); } - } - } - // If no authorization was explicitly attached to the authentication ticket, - // create an ad hoc authorization if an authorization code or a refresh token - // is going to be returned to the client application as part of the response. - if (!options.DisableAuthorizationStorage && - string.IsNullOrEmpty(context.Ticket.GetInternalAuthorizationId()) && - (context.IncludeAuthorizationCode || context.IncludeRefreshToken)) - { - await CreateAuthorizationAsync(context.Ticket, options, context.Request); + yield return handler; + } } - // Add the custom properties that are marked as public as authorization or - // token response properties and remove them from the authentication ticket - // so they are not persisted in the authorization code/access/refresh token. - // Note: make sure the foreach statement iterates on a copy of the ticket - // as the property collection is modified when the property is removed. - var parameters = GetParameters(context.Request, context.Ticket.Properties); - foreach (var (property, parameter, value) in parameters.ToList()) + async Task IsActiveAsync(OpenIddictServerHandlerDescriptor descriptor) { - context.Response.AddParameter(parameter, value); - context.Ticket.RemoveProperty(property); - } - - await _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ProcessSigninResponse(context)); - } + for (var index = 0; index < descriptor.FilterTypes.Length; index++) + { + if (!(_provider.GetService(descriptor.FilterTypes[index]) is IOpenIddictServerHandlerFilter filter)) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine($"The event handler filter of type '{descriptor.FilterTypes[index]}' couldn't be resolved.") + .AppendLine("This may indicate that it was not properly registered in the dependency injection container.") + .ToString()); + } - public override Task ProcessSignoutResponse([NotNull] ProcessSignoutResponseContext context) - { - Debug.Assert(context.Request.IsLogoutRequest(), "The request should be a logout request."); + if (!await filter.IsActiveAsync(context)) + { + return false; + } + } - // Add the custom properties that are marked as public as logout response properties. - var parameters = GetParameters(context.Request, context.Properties); - foreach (var (property, parameter, value) in parameters) - { - context.Response.AddParameter(parameter, value); + return true; } - - return _eventDispatcher.DispatchAsync(new OpenIddictServerEvents.ProcessSignoutResponse(context)); } } } diff --git a/src/OpenIddict.Server/OpenIddictServerTransaction.cs b/src/OpenIddict.Server/OpenIddictServerTransaction.cs new file mode 100644 index 000000000..1e7e5afea --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerTransaction.cs @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using OpenIddict.Abstractions; + +namespace OpenIddict.Server +{ + /// + /// Represents the context associated with an OpenID Connect server request. + /// + public class OpenIddictServerTransaction + { + /// + /// Gets or sets the type of the endpoint processing the current request. + /// + public OpenIddictServerEndpointType EndpointType { get; set; } + + /// + /// Gets or sets the logger associated with the current request. + /// + public ILogger Logger { get; set; } + + /// + /// Gets or sets the options associated with the current request. + /// + public OpenIddictServerOptions Options { get; set; } + + /// + /// Gets the additional properties associated with the current request. + /// + public IDictionary Properties { get; } + = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets or sets the current OpenID Connect request. + /// + public OpenIddictRequest Request { get; set; } + + /// + /// Gets or sets the current OpenID Connect response being returned. + /// + public OpenIddictResponse Response { get; set; } + } +} diff --git a/src/OpenIddict.Server/Properties/AssemblyInfo.cs b/src/OpenIddict.Server/Properties/AssemblyInfo.cs deleted file mode 100644 index c05ea5c5b..000000000 --- a/src/OpenIddict.Server/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("OpenIddict.Server.Tests, PublicKey=0024000004800000140200000602000000240000525341310010000001000100613f2880fc9f71b5e8f968801c1a4923e1df760bd3f9d2d752f83c01cabc4853e7f32bba18e9e88ca1285d96655008148d60c43d28d82a292c6bf0c8a761fd5d345e45ec1c044b6eeca140593779e6c9e8b59eb1b1cc905dc81e559a9fdf24c77d333c53cc8c7f2d46a6df3a74c426e4afc97bab4117a87a882552c8f41e9f4757bb40a1255cf720f85ce50bac763a6104b03d6927ef05f5dcc316450eda528eae7f003af8c6463daa9505fae121d0c8294eb927995a4dd96b9397c16a479c865322af27c0f1b493a5dc03305bd5d46ac376de620cb050b40f9fb1cbb0a2004242ad30aff30e203fb68a104eed90d80def2e04f1c73e01937d9f1359108904d13d5226ac717880f51070066252ceb0b0acdb6705fb76515f3cebb2fc497572ab3c66718fbdb1306f39125a6cb6f40006db495a21a61b5273ddcbc83e983a2e59b04ebbcde41aad46ff4292080b1ede89878ac95b26d68227cd6077994b397255e91b09d25de64f0f7cf58cd3f96460561056eaf48ff1fb1d9f6faa3741bc756b930d761dfe0bde2d8d4c79351888688dd2d0c2939b8e0619a3f668816fbbab070c3139e3f3a5b2961f7d99f2af95fde9c52958644e575a3d0f2a1de8d5fef0b8c9766b415e7566eaba2ad0c775089c1f5148008509700fcebdd001f1b36a1db83be5b2b66c18342d3230e5f995f1283335dbee7388aa3584206fba97112775af")] \ No newline at end of file diff --git a/src/OpenIddict.Validation/IOpenIddictValidationEvent.cs b/src/OpenIddict.Validation/IOpenIddictValidationEvent.cs deleted file mode 100644 index e71d0f8f4..000000000 --- a/src/OpenIddict.Validation/IOpenIddictValidationEvent.cs +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -namespace OpenIddict.Validation -{ - /// - /// Represents an OpenIddict validation event. - /// - public interface IOpenIddictValidationEvent { } -} diff --git a/src/OpenIddict.Validation/IOpenIddictValidationEventDispatcher.cs b/src/OpenIddict.Validation/IOpenIddictValidationEventDispatcher.cs deleted file mode 100644 index 8eff3d1ab..000000000 --- a/src/OpenIddict.Validation/IOpenIddictValidationEventDispatcher.cs +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Threading.Tasks; -using JetBrains.Annotations; - -namespace OpenIddict.Validation -{ - /// - /// Dispatches events by invoking the corresponding handlers. - /// - public interface IOpenIddictValidationEventDispatcher - { - /// - /// Publishes a new event. - /// - /// The type of the event to publish. - /// The event to publish. - /// A that can be used to monitor the asynchronous operation. - Task DispatchAsync([NotNull] TEvent notification) where TEvent : class, IOpenIddictValidationEvent; - } -} diff --git a/src/OpenIddict.Validation/IOpenIddictValidationEventHandler.cs b/src/OpenIddict.Validation/IOpenIddictValidationEventHandler.cs deleted file mode 100644 index 2840ee43c..000000000 --- a/src/OpenIddict.Validation/IOpenIddictValidationEventHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Threading.Tasks; -using JetBrains.Annotations; - -namespace OpenIddict.Validation -{ - /// - /// Represents a handler able to process events. - /// - /// The type of the events handled by this instance. - public interface IOpenIddictValidationEventHandler where TEvent : class, IOpenIddictValidationEvent - { - /// - /// Processes the event. - /// - /// The event to process. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result determines whether next handlers in the pipeline are invoked. - /// - Task HandleAsync([NotNull] TEvent notification); - } -} diff --git a/src/OpenIddict.Validation/OpenIddict.Validation.csproj b/src/OpenIddict.Validation/OpenIddict.Validation.csproj deleted file mode 100644 index 5aef6f203..000000000 --- a/src/OpenIddict.Validation/OpenIddict.Validation.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - netstandard2.0 - - - - OpenIddict token validation middleware for ASP.NET Core. - Kévin Chalet;Chino Chang - aspnetcore;authentication;jwt;openidconnect;openiddict;security - - - - - - - - - - - - - - - - - diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs deleted file mode 100644 index 9d19d8683..000000000 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.ComponentModel; -using System.Linq; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Microsoft.AspNetCore.DataProtection; -using OpenIddict.Extensions; -using OpenIddict.Validation; - -namespace Microsoft.Extensions.DependencyInjection -{ - /// - /// Exposes the necessary methods required to configure the OpenIddict validation services. - /// - public class OpenIddictValidationBuilder - { - /// - /// Initializes a new instance of . - /// - /// The services collection. - public OpenIddictValidationBuilder([NotNull] IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - Services = services; - } - - /// - /// Gets the services collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public IServiceCollection Services { get; } - - /// - /// Registers an inline event handler for the specified event type. - /// - /// The handler delegate. - /// The . - [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictValidationBuilder AddEventHandler( - [NotNull] Func> handler) - where TEvent : class, IOpenIddictValidationEvent - { - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } - - Services.AddSingleton>( - new OpenIddictValidationEventHandler(handler)); - - return this; - } - - /// - /// Registers an event handler that will be invoked for all the events listed by the implemented interfaces. - /// - /// The type of the handler. - /// The lifetime of the registered service. - /// The . - [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictValidationBuilder AddEventHandler(ServiceLifetime lifetime = ServiceLifetime.Scoped) - => AddEventHandler(typeof(THandler), lifetime); - - /// - /// Registers an event handler that will be invoked for all the events listed by the implemented interfaces. - /// - /// The type of the handler. - /// The lifetime of the registered service. - /// The . - [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictValidationBuilder AddEventHandler([NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) - { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - if (lifetime == ServiceLifetime.Transient) - { - throw new ArgumentException("Handlers cannot be registered as transient services.", nameof(lifetime)); - } - - if (type.IsGenericTypeDefinition) - { - throw new ArgumentException("The specified type is invalid.", nameof(type)); - } - - var services = OpenIddictHelpers.FindGenericBaseTypes(type, typeof(IOpenIddictValidationEventHandler<>)).ToArray(); - if (services.Length == 0) - { - throw new ArgumentException("The specified type is invalid.", nameof(type)); - } - - foreach (var service in services) - { - Services.Add(new ServiceDescriptor(service, type, lifetime)); - } - - return this; - } - - /// - /// Amends the default OpenIddict validation configuration. - /// - /// The delegate used to configure the OpenIddict options. - /// This extension can be safely called multiple times. - /// The . - public OpenIddictValidationBuilder Configure([NotNull] Action configuration) - { - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - Services.Configure(OpenIddictValidationDefaults.AuthenticationScheme, configuration); - - return this; - } - - /// - /// Registers the specified values as valid audiences. Setting the audiences is recommended - /// when the authorization server issues access tokens for multiple distinct resource servers. - /// - /// The audiences valid for this resource server. - /// The . - public OpenIddictValidationBuilder AddAudiences([NotNull] params string[] audiences) - { - if (audiences == null) - { - throw new ArgumentNullException(nameof(audiences)); - } - - if (audiences.Any(audience => string.IsNullOrEmpty(audience))) - { - throw new ArgumentException("Audiences cannot be null or empty.", nameof(audiences)); - } - - return Configure(options => options.Audiences.UnionWith(audiences)); - } - - /// - /// Enables authorization validation so that a database call is made for each API request - /// to ensure the authorization associated with the access token is still valid. - /// Note: enabling this option may have an impact on performance. - /// - /// The . - public OpenIddictValidationBuilder EnableAuthorizationValidation() - => Configure(options => options.EnableAuthorizationValidation = true); - - /// - /// Configures OpenIddict not to return the authentication error - /// details as part of the standard WWW-Authenticate response header. - /// - /// The . - public OpenIddictValidationBuilder RemoveErrorDetails() - => Configure(options => options.IncludeErrorDetails = false); - - /// - /// Sets the realm, which is used to compute the WWW-Authenticate response header. - /// - /// The realm. - /// The . - public OpenIddictValidationBuilder SetRealm([NotNull] string realm) - { - if (string.IsNullOrEmpty(realm)) - { - throw new ArgumentException("The realm cannot be null or empty.", nameof(realm)); - } - - return Configure(options => options.Realm = realm); - } - - /// - /// Configures OpenIddict to use a specific data protection provider - /// instead of relying on the default instance provided by the DI container. - /// - /// The data protection provider used to create token protectors. - /// The . - public OpenIddictValidationBuilder UseDataProtectionProvider([NotNull] IDataProtectionProvider provider) - { - if (provider == null) - { - throw new ArgumentNullException(nameof(provider)); - } - - return Configure(options => options.DataProtectionProvider = provider); - } - - /// - /// Configures the OpenIddict validation handler to use reference tokens. - /// - /// The . - public OpenIddictValidationBuilder UseReferenceTokens() - => Configure(options => options.UseReferenceTokens = true); - - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. - [EditorBrowsable(EditorBrowsableState.Never)] - public override bool Equals([CanBeNull] object obj) => base.Equals(obj); - - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. - [EditorBrowsable(EditorBrowsableState.Never)] - public override int GetHashCode() => base.GetHashCode(); - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - [EditorBrowsable(EditorBrowsableState.Never)] - public override string ToString() => base.ToString(); - } -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs deleted file mode 100644 index 19c971e3d..000000000 --- a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Text; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Options; - -namespace OpenIddict.Validation -{ - /// - /// Contains the methods required to ensure that the OpenIddict validation configuration is valid. - /// - public class OpenIddictValidationConfiguration : IConfigureOptions, - IPostConfigureOptions, - IPostConfigureOptions - { - private readonly IDataProtectionProvider _dataProtectionProvider; - - /// - /// Creates a new instance of the class. - /// - public OpenIddictValidationConfiguration([NotNull] IDataProtectionProvider dataProtectionProvider) - => _dataProtectionProvider = dataProtectionProvider; - - /// - /// Registers the OpenIddict validation handler in the global authentication options. - /// - /// The options instance to initialize. - public void Configure([NotNull] AuthenticationOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - // If a handler was already registered and the type doesn't correspond to the OpenIddict handler, throw an exception. - if (options.SchemeMap.TryGetValue(OpenIddictValidationDefaults.AuthenticationScheme, out var builder) && - builder.HandlerType != typeof(OpenIddictValidationHandler)) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The OpenIddict validation handler cannot be registered as an authentication scheme.") - .AppendLine("This may indicate that an instance of the OAuth validation or JWT bearer handler was registered.") - .Append("Make sure that neither 'services.AddAuthentication().AddOAuthValidation()' nor ") - .Append("'services.AddAuthentication().AddJwtBearer()' are called from 'ConfigureServices'.") - .ToString()); - } - - options.AddScheme(OpenIddictValidationDefaults.AuthenticationScheme, displayName: null); - } - - /// - /// Ensures that the authentication configuration is in a consistent and valid state. - /// - /// The authentication scheme associated with the handler instance. - /// The options instance to initialize. - public void PostConfigure([CanBeNull] string name, [NotNull] AuthenticationOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - bool TryValidate(string scheme) - { - // If the scheme was not set or if it cannot be found in the map, return true. - if (string.IsNullOrEmpty(scheme) || !options.SchemeMap.TryGetValue(scheme, out var builder)) - { - return true; - } - - return builder.HandlerType != typeof(OpenIddictValidationHandler); - } - - if (!TryValidate(options.DefaultSignInScheme) || !TryValidate(options.DefaultSignOutScheme)) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The OpenIddict validation handler cannot be used as the default sign-in/out scheme handler.") - .Append("Make sure that neither DefaultSignInScheme nor DefaultSignOutScheme ") - .Append("point to an instance of the OpenIddict validation handler.") - .ToString()); - } - } - - /// - /// Populates the default OpenIddict validation options and ensures - /// that the configuration is in a consistent and valid state. - /// - /// The authentication scheme associated with the handler instance. - /// The options instance to initialize. - public void PostConfigure([NotNull] string name, [NotNull] OpenIddictValidationOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException("The options instance name cannot be null or empty.", nameof(name)); - } - - if (options.EventsType == null || options.EventsType != typeof(OpenIddictValidationProvider)) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("OpenIddict can only be used with its built-in validation provider.") - .AppendLine("This error may indicate that 'OpenIddictValidationOptions.EventsType' was manually set.") - .Append("To execute custom request handling logic, consider registering an event handler using ") - .Append("the generic 'services.AddOpenIddict().AddValidation().AddEventHandler()' method.") - .ToString()); - } - - if (options.DataProtectionProvider == null) - { - options.DataProtectionProvider = _dataProtectionProvider; - } - - if (options.UseReferenceTokens && options.AccessTokenFormat == null) - { - var protector = options.DataProtectionProvider.CreateProtector( - "OpenIdConnectServerHandler", - nameof(options.AccessTokenFormat), - nameof(options.UseReferenceTokens), "ASOS"); - - options.AccessTokenFormat = new TicketDataFormat(protector); - } - } - } -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationDefaults.cs b/src/OpenIddict.Validation/OpenIddictValidationDefaults.cs deleted file mode 100644 index e72d1d2ed..000000000 --- a/src/OpenIddict.Validation/OpenIddictValidationDefaults.cs +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using AspNet.Security.OAuth.Validation; -using Microsoft.AspNetCore.Authentication; - -namespace OpenIddict.Validation -{ - /// - /// Exposes the default values used by the OpenIddict validation handler. - /// - public static class OpenIddictValidationDefaults - { - /// - /// Default value for . - /// - public const string AuthenticationScheme = OAuthValidationDefaults.AuthenticationScheme; - } -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvent.cs b/src/OpenIddict.Validation/OpenIddictValidationEvent.cs deleted file mode 100644 index c942dc565..000000000 --- a/src/OpenIddict.Validation/OpenIddictValidationEvent.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using JetBrains.Annotations; - -namespace OpenIddict.Validation -{ - /// - /// Represents an OpenIddict validation event. - /// - /// The type of the context instance associated with the event. - public class OpenIddictValidationEvent : IOpenIddictValidationEvent where TContext : class - { - /// - /// Creates a new instance of . - /// - /// The context instance associated with the event. - public OpenIddictValidationEvent([NotNull] TContext context) - => Context = context ?? throw new ArgumentNullException(nameof(context)); - - /// - /// Gets the context instance associated with the event. - /// - public TContext Context { get; } - } -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEventDispatcher.cs b/src/OpenIddict.Validation/OpenIddictValidationEventDispatcher.cs deleted file mode 100644 index 58fd7171a..000000000 --- a/src/OpenIddict.Validation/OpenIddictValidationEventDispatcher.cs +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; - -namespace OpenIddict.Validation -{ - /// - /// Dispatches events by invoking the corresponding notification handlers. - /// - public class OpenIddictValidationEventDispatcher : IOpenIddictValidationEventDispatcher - { - private readonly IServiceProvider _provider; - - /// - /// Creates a new instance of the class. - /// - public OpenIddictValidationEventDispatcher([NotNull] IServiceProvider provider) - => _provider = provider; - - /// - /// Publishes a new event. - /// - /// The type of the event to publish. - /// The event to publish. - /// A that can be used to monitor the asynchronous operation. - public async Task DispatchAsync([NotNull] TEvent notification) where TEvent : class, IOpenIddictValidationEvent - { - if (notification == null) - { - throw new ArgumentNullException(nameof(notification)); - } - - foreach (var handler in _provider.GetServices>()) - { - switch (await handler.HandleAsync(notification)) - { - case OpenIddictValidationEventState.Unhandled: continue; - case OpenIddictValidationEventState.Handled: return; - - default: throw new InvalidOperationException("The specified event state is not valid."); - } - } - } - } -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEventHandler.cs b/src/OpenIddict.Validation/OpenIddictValidationEventHandler.cs deleted file mode 100644 index b3152c758..000000000 --- a/src/OpenIddict.Validation/OpenIddictValidationEventHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Threading.Tasks; -using JetBrains.Annotations; - -namespace OpenIddict.Validation -{ - /// - /// Represents a handler able to process events. - /// - /// The type of the events handled by this instance. - public class OpenIddictValidationEventHandler : IOpenIddictValidationEventHandler - where TEvent : class, IOpenIddictValidationEvent - { - private readonly Func> _handler; - - /// - /// Creates a new event using the specified handler delegate. - /// - /// The event handler delegate. - public OpenIddictValidationEventHandler([NotNull] Func> handler) - => _handler = handler ?? throw new ArgumentNullException(nameof(handler)); - - /// - /// Processes the event. - /// - /// The event to process. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result determines whether next handlers in the pipeline are invoked. - /// - public Task HandleAsync(TEvent notification) - => _handler(notification ?? throw new ArgumentNullException(nameof(notification))); - } -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEventState.cs b/src/OpenIddict.Validation/OpenIddictValidationEventState.cs deleted file mode 100644 index b3b475690..000000000 --- a/src/OpenIddict.Validation/OpenIddictValidationEventState.cs +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -namespace OpenIddict.Validation -{ - /// - /// Represents the state of an event triggered by the OpenIddict - /// validation components and processed by user-defined handlers. - /// - public enum OpenIddictValidationEventState - { - /// - /// Marks the event as unhandled, allowing the event service to invoke the - /// other event handlers registered in the dependency injection container. - /// Using this value is recommended for event handlers that don't produce - /// an immediate response (i.e that don't call context.HandleResponse(), - /// context.Fail(), context.NoResult() or context.Success()). - /// - Unhandled = 0, - - /// - /// Marks the event as fully handled, preventing the event service from invoking - /// other event handlers registered in the dependency injection container. - /// Using this value is recommended for event handlers that produce an - /// immediate response (i.e that call context.HandleResponse(), - /// context.Fail(), context.NoResult() or context.Success()). - /// - Handled = 1 - } -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs deleted file mode 100644 index 827591058..000000000 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using AspNet.Security.OAuth.Validation; -using JetBrains.Annotations; - -namespace OpenIddict.Validation -{ - /// - /// Contains common events used by the OpenIddict validation handler. - /// - public static class OpenIddictValidationEvents - { - /// - /// Invoked when a challenge response is returned to the caller. - /// - public sealed class ApplyChallenge : OpenIddictValidationEvent - { - /// - /// Creates a new instance of . - /// - /// The context instance associated with the notification. - public ApplyChallenge([NotNull] ApplyChallengeContext context) : base(context) { } - } - - /// - /// Invoked when a ticket is to be created from an introspection response. - /// - public sealed class CreateTicket : OpenIddictValidationEvent - { - /// - /// Creates a new instance of . - /// - /// The context instance associated with the notification. - public CreateTicket([NotNull] CreateTicketContext context) : base(context) { } - } - - /// - /// Invoked when a token is to be decrypted. - /// - public sealed class DecryptToken : OpenIddictValidationEvent - { - /// - /// Creates a new instance of . - /// - /// The context instance associated with the notification. - public DecryptToken([NotNull] DecryptTokenContext context) : base(context) { } - } - - /// - /// Invoked when a token is to be parsed from a newly-received request. - /// - public sealed class RetrieveToken : OpenIddictValidationEvent - { - /// - /// Creates a new instance of . - /// - /// The context instance associated with the notification. - public RetrieveToken([NotNull] RetrieveTokenContext context) : base(context) { } - } - - /// - /// Invoked when a token is to be validated, before final processing. - /// - public sealed class ValidateToken : OpenIddictValidationEvent - { - /// - /// Creates a new instance of . - /// - /// The context instance associated with the notification. - public ValidateToken([NotNull] ValidateTokenContext context) : base(context) { } - } - } -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs deleted file mode 100644 index d3d4ca5e3..000000000 --- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using AspNet.Security.OAuth.Validation; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; -using OpenIddict.Validation; - -namespace Microsoft.Extensions.DependencyInjection -{ - /// - /// Exposes extensions allowing to register the OpenIddict validation services. - /// - public static class OpenIddictValidationExtensions - { - /// - /// Registers the OpenIddict token validation services in the DI container. - /// Note: the validation handler only works with the default token format - /// or reference tokens and cannot be used with JWT tokens. To validate - /// JWT tokens, use the JWT bearer handler shipping with ASP.NET Core. - /// - /// The services builder used by OpenIddict to register new services. - /// This extension can be safely called multiple times. - /// The . - public static OpenIddictValidationBuilder AddValidation([NotNull] this OpenIddictBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - builder.Services.AddAuthentication(); - builder.Services.AddLogging(); - builder.Services.AddOptions(); - - builder.Services.TryAddScoped(); - builder.Services.TryAddScoped(); - builder.Services.TryAddScoped(); - - // Register the options initializers used by the OAuth validation handler and OpenIddict. - // Note: TryAddEnumerable() is used here to ensure the initializers are only registered once. - builder.Services.TryAddEnumerable(new[] - { - ServiceDescriptor.Singleton, OpenIddictValidationConfiguration>(), - ServiceDescriptor.Singleton, OpenIddictValidationConfiguration>(), - ServiceDescriptor.Singleton, OpenIddictValidationConfiguration>(), - ServiceDescriptor.Singleton, OAuthValidationInitializer>() - }); - - return new OpenIddictValidationBuilder(builder.Services); - } - - /// - /// Registers the OpenIddict token validation services in the DI container. - /// Note: the validation handler only works with the default token format - /// or reference tokens and cannot be used with JWT tokens. To validate - /// JWT tokens, use the JWT bearer handler shipping with ASP.NET Core. - /// - /// The services builder used by OpenIddict to register new services. - /// The configuration delegate used to configure the validation services. - /// This extension can be safely called multiple times. - /// The . - public static OpenIddictBuilder AddValidation( - [NotNull] this OpenIddictBuilder builder, - [NotNull] Action configuration) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - configuration(builder.AddValidation()); - - return builder; - } - } -} \ No newline at end of file diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandler.cs b/src/OpenIddict.Validation/OpenIddictValidationHandler.cs deleted file mode 100644 index c31d28126..000000000 --- a/src/OpenIddict.Validation/OpenIddictValidationHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Text.Encodings.Web; -using AspNet.Security.OAuth.Validation; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace OpenIddict.Validation -{ - /// - /// Provides the logic necessary to extract and validate tokens from HTTP requests. - /// - public class OpenIddictValidationHandler : OAuthValidationHandler - { - /// - /// Creates a new instance of the class. - /// - public OpenIddictValidationHandler( - [NotNull] IOptionsMonitor options, - [NotNull] ILoggerFactory logger, - [NotNull] UrlEncoder encoder, - [NotNull] ISystemClock clock) - : base(options, logger, encoder, clock) - { - } - } -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationHelpers.cs b/src/OpenIddict.Validation/OpenIddictValidationHelpers.cs deleted file mode 100644 index 2d239e146..000000000 --- a/src/OpenIddict.Validation/OpenIddictValidationHelpers.cs +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Authentication; - -namespace OpenIddict.Validation -{ - /// - /// Defines a set of commonly used helpers. - /// Note: this API supports the OpenIddict infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future minor releases. - /// - internal static class OpenIddictValidationHelpers - { - /// - /// Gets a given property from the authentication properties. - /// - /// The authentication properties. - /// The specific property to look for. - /// The value corresponding to the property, or null if the property cannot be found. - public static string GetProperty([NotNull] this AuthenticationProperties properties, [NotNull] string property) - { - if (properties == null) - { - throw new ArgumentNullException(nameof(properties)); - } - - if (string.IsNullOrEmpty(property)) - { - throw new ArgumentException("The property name cannot be null or empty.", nameof(property)); - } - - if (!properties.Items.TryGetValue(property, out string value)) - { - return null; - } - - return value; - } - - /// - /// Sets the specified property in the authentication properties. - /// - /// The authentication properties. - /// The property name. - /// The property value. - /// The so that multiple calls can be chained. - public static AuthenticationProperties SetProperty( - [NotNull] this AuthenticationProperties properties, - [NotNull] string property, [CanBeNull] string value) - { - if (properties == null) - { - throw new ArgumentNullException(nameof(properties)); - } - - if (string.IsNullOrEmpty(property)) - { - throw new ArgumentException("The property name cannot be null or empty.", nameof(property)); - } - - if (string.IsNullOrEmpty(value)) - { - properties.Items.Remove(property); - } - - else - { - properties.Items[property] = value; - } - - return properties; - } - } -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs deleted file mode 100644 index 3b98216b4..000000000 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using AspNet.Security.OAuth.Validation; - -namespace OpenIddict.Validation -{ - /// - /// Provides various settings needed to configure the OpenIddict validation handler. - /// - public class OpenIddictValidationOptions : OAuthValidationOptions - { - /// - /// Creates a new instance of the class. - /// - public OpenIddictValidationOptions() - { - Events = null; - EventsType = typeof(OpenIddictValidationProvider); - } - - /// - /// Gets or sets a boolean indicating whether a database call is made - /// to validate the authorization associated with the received tokens. - /// - public bool EnableAuthorizationValidation { get; set; } - - /// - /// Gets or sets a boolean indicating whether reference tokens are used. - /// - public bool UseReferenceTokens { get; set; } - } -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationProvider.cs b/src/OpenIddict.Validation/OpenIddictValidationProvider.cs deleted file mode 100644 index 8b9d961e2..000000000 --- a/src/OpenIddict.Validation/OpenIddictValidationProvider.cs +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Diagnostics; -using System.Text; -using System.Threading.Tasks; -using AspNet.Security.OAuth.Validation; -using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; -using OpenIddict.Abstractions; - -namespace OpenIddict.Validation -{ - /// - /// Provides the logic necessary to extract and validate tokens from HTTP requests. - /// Note: this API supports the OpenIddict infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future minor releases. - /// - internal sealed class OpenIddictValidationProvider : OAuthValidationEvents - { - private readonly IOpenIddictValidationEventDispatcher _eventDispatcher; - - /// - /// Creates a new instance of the class. - /// Note: this API supports the OpenIddict infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future minor releases. - /// - public OpenIddictValidationProvider([NotNull] IOpenIddictValidationEventDispatcher eventDispatcher) - => _eventDispatcher = eventDispatcher; - - public override Task ApplyChallenge([NotNull] ApplyChallengeContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictValidationEvents.ApplyChallenge(context)); - - public override Task CreateTicket([NotNull] CreateTicketContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictValidationEvents.CreateTicket(context)); - - public override async Task DecryptToken([NotNull] DecryptTokenContext context) - { - var options = (OpenIddictValidationOptions) context.Options; - if (options.UseReferenceTokens) - { - // Note: the token manager is deliberately not injected using constructor injection - // to allow using the validation handler without having to register the core services. - var manager = context.HttpContext.RequestServices.GetService(); - if (manager == null) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The core services must be registered when enabling reference tokens support.") - .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") - .Append("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") - .ToString()); - } - - // Retrieve the token entry from the database. If it - // cannot be found, assume the token is not valid. - var token = await manager.FindByReferenceIdAsync(context.Token); - if (token == null) - { - context.Fail("Authentication failed because the access token cannot be found in the database."); - - return; - } - - // Optimization: avoid extracting/decrypting the token payload if the token is not an access token. - var type = await manager.GetTypeAsync(token); - if (string.IsNullOrEmpty(type)) - { - context.Fail("Authentication failed because the token type associated with the entry is missing."); - - return; - } - - if (!string.Equals(type, OpenIddictConstants.TokenTypes.AccessToken, StringComparison.OrdinalIgnoreCase)) - { - context.Fail("Authentication failed because the specified token is not an access token."); - - return; - } - - // Extract the encrypted payload from the token. If it's null or empty, - // assume the token is not a reference token and consider it as invalid. - var payload = await manager.GetPayloadAsync(token); - if (string.IsNullOrEmpty(payload)) - { - context.Fail("Authentication failed because the access token is not a reference token."); - - return; - } - - var ticket = context.DataFormat.Unprotect(payload); - if (ticket == null) - { - context.Fail("Authentication failed because the reference token cannot be decrypted. " + - "This may indicate that the token entry is corrupted or tampered."); - - return; - } - - // Dynamically set the creation and expiration dates. - ticket.Properties.IssuedUtc = await manager.GetCreationDateAsync(token); - ticket.Properties.ExpiresUtc = await manager.GetExpirationDateAsync(token); - - // Restore the token and authorization identifiers attached with the database entry. - ticket.Properties.SetProperty(OpenIddictConstants.Properties.InternalTokenId, await manager.GetIdAsync(token)); - ticket.Properties.SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, - await manager.GetAuthorizationIdAsync(token)); - - context.Principal = ticket.Principal; - context.Properties = ticket.Properties; - context.Success(); - } - - await _eventDispatcher.DispatchAsync(new OpenIddictValidationEvents.DecryptToken(context)); - } - - public override Task RetrieveToken([NotNull] RetrieveTokenContext context) - => _eventDispatcher.DispatchAsync(new OpenIddictValidationEvents.RetrieveToken(context)); - - public override async Task ValidateToken([NotNull] ValidateTokenContext context) - { - var options = (OpenIddictValidationOptions) context.Options; - if (options.UseReferenceTokens) - { - // Note: the token manager is deliberately not injected using constructor injection - // to allow using the validation handler without having to register the core services. - var manager = context.HttpContext.RequestServices.GetService(); - if (manager == null) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The core services must be registered when enabling reference tokens support.") - .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") - .Append("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") - .ToString()); - } - - var identifier = context.Properties.GetProperty(OpenIddictConstants.Properties.InternalTokenId); - Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a token identifier."); - - // Ensure the access token is still valid (i.e was not marked as revoked). - var token = await manager.FindByIdAsync(identifier); - if (token == null || !await manager.IsValidAsync(token)) - { - context.Fail("Authentication failed because the access token was no longer valid."); - - return; - } - } - - if (options.EnableAuthorizationValidation) - { - // Note: the authorization manager is deliberately not injected using constructor injection - // to allow using the validation handler without having to register the OpenIddict core services. - var manager = context.HttpContext.RequestServices.GetService(); - if (manager == null) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The core services must be registered when enabling authorization validation.") - .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") - .Append("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") - .ToString()); - } - - var identifier = context.Properties.GetProperty(OpenIddictConstants.Properties.InternalAuthorizationId); - if (!string.IsNullOrEmpty(identifier)) - { - var authorization = await manager.FindByIdAsync(identifier); - if (authorization == null || !await manager.IsValidAsync(authorization)) - { - context.Fail("Authentication failed because the authorization " + - "associated with the access token was not longer valid."); - - return; - } - } - } - - await _eventDispatcher.DispatchAsync(new OpenIddictValidationEvents.ValidateToken(context)); - } - } -} diff --git a/src/OpenIddict.Validation/Properties/AssemblyInfo.cs b/src/OpenIddict.Validation/Properties/AssemblyInfo.cs deleted file mode 100644 index 470d45988..000000000 --- a/src/OpenIddict.Validation/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("OpenIddict.Validation.Tests, PublicKey=0024000004800000140200000602000000240000525341310010000001000100613f2880fc9f71b5e8f968801c1a4923e1df760bd3f9d2d752f83c01cabc4853e7f32bba18e9e88ca1285d96655008148d60c43d28d82a292c6bf0c8a761fd5d345e45ec1c044b6eeca140593779e6c9e8b59eb1b1cc905dc81e559a9fdf24c77d333c53cc8c7f2d46a6df3a74c426e4afc97bab4117a87a882552c8f41e9f4757bb40a1255cf720f85ce50bac763a6104b03d6927ef05f5dcc316450eda528eae7f003af8c6463daa9505fae121d0c8294eb927995a4dd96b9397c16a479c865322af27c0f1b493a5dc03305bd5d46ac376de620cb050b40f9fb1cbb0a2004242ad30aff30e203fb68a104eed90d80def2e04f1c73e01937d9f1359108904d13d5226ac717880f51070066252ceb0b0acdb6705fb76515f3cebb2fc497572ab3c66718fbdb1306f39125a6cb6f40006db495a21a61b5273ddcbc83e983a2e59b04ebbcde41aad46ff4292080b1ede89878ac95b26d68227cd6077994b397255e91b09d25de64f0f7cf58cd3f96460561056eaf48ff1fb1d9f6faa3741bc756b930d761dfe0bde2d8d4c79351888688dd2d0c2939b8e0619a3f668816fbbab070c3139e3f3a5b2961f7d99f2af95fde9c52958644e575a3d0f2a1de8d5fef0b8c9766b415e7566eaba2ad0c775089c1f5148008509700fcebdd001f1b36a1db83be5b2b66c18342d3230e5f995f1283335dbee7388aa3584206fba97112775af")] \ No newline at end of file diff --git a/src/OpenIddict/OpenIddict.csproj b/src/OpenIddict/OpenIddict.csproj index e1009dfd5..6571f9111 100644 --- a/src/OpenIddict/OpenIddict.csproj +++ b/src/OpenIddict/OpenIddict.csproj @@ -1,23 +1,17 @@  - - - netstandard2.0 + netstandard2.0;netstandard2.1 - Easy-to-use OpenID Connect server for ASP.NET Core. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security + Easy-to-use OpenID Connect server for ASP.NET 4.x/OWIN and ASP.NET Core. - - diff --git a/test/OpenIddict.Abstractions.Tests/OpenIddict.Abstractions.Tests.csproj b/test/OpenIddict.Abstractions.Tests/OpenIddict.Abstractions.Tests.csproj index 0b7b3c1aa..c3e81cf83 100644 --- a/test/OpenIddict.Abstractions.Tests/OpenIddict.Abstractions.Tests.csproj +++ b/test/OpenIddict.Abstractions.Tests/OpenIddict.Abstractions.Tests.csproj @@ -1,10 +1,8 @@  - - - netcoreapp2.0;net461 - netcoreapp2.0 + netcoreapp3.0;net461 + netcoreapp3.0 @@ -12,15 +10,8 @@ - - + - - - - - - diff --git a/test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj b/test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj index 5179b5a24..07dc66f21 100644 --- a/test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj +++ b/test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj @@ -1,10 +1,8 @@  - - - netcoreapp2.0;net461 - netcoreapp2.0 + netcoreapp3.0;net461 + netcoreapp3.0 @@ -12,15 +10,8 @@ - - + - - - - - - diff --git a/test/OpenIddict.EntityFramework.Tests/OpenIddict.EntityFramework.Tests.csproj b/test/OpenIddict.EntityFramework.Tests/OpenIddict.EntityFramework.Tests.csproj index dbecd7e0a..1d58e6728 100644 --- a/test/OpenIddict.EntityFramework.Tests/OpenIddict.EntityFramework.Tests.csproj +++ b/test/OpenIddict.EntityFramework.Tests/OpenIddict.EntityFramework.Tests.csproj @@ -1,9 +1,8 @@  - - - net461 + netcoreapp3.0;net461 + netcoreapp3.0 @@ -11,15 +10,8 @@ - - + - - - - - - diff --git a/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkExtensionsTests.cs b/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkExtensionsTests.cs index a28b17773..2fd603dcd 100644 --- a/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkExtensionsTests.cs +++ b/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkExtensionsTests.cs @@ -5,7 +5,6 @@ */ using System; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; diff --git a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj index 05e830b03..1d0e1e67c 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj +++ b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj @@ -1,10 +1,7 @@  - - - netcoreapp2.0;net461 - netcoreapp2.0 + netcoreapp3.0 @@ -12,14 +9,7 @@ - - - - - - - diff --git a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs index a23197ec9..482a7f1a3 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs +++ b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs @@ -5,7 +5,6 @@ */ using System; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; diff --git a/test/OpenIddict.MongoDb.Tests/OpenIddict.MongoDb.Tests.csproj b/test/OpenIddict.MongoDb.Tests/OpenIddict.MongoDb.Tests.csproj index 68766cdf2..f322d411d 100644 --- a/test/OpenIddict.MongoDb.Tests/OpenIddict.MongoDb.Tests.csproj +++ b/test/OpenIddict.MongoDb.Tests/OpenIddict.MongoDb.Tests.csproj @@ -1,10 +1,8 @@  - - - netcoreapp2.0;net461 - netcoreapp2.0 + netcoreapp3.0;net461 + netcoreapp3.0 false false @@ -14,15 +12,8 @@ - - + - - - - - - diff --git a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbExtensionsTests.cs b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbExtensionsTests.cs index 4ee874171..32f839ffd 100644 --- a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbExtensionsTests.cs +++ b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbExtensionsTests.cs @@ -5,7 +5,6 @@ */ using System; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; diff --git a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs index 685f5e4ab..e47b9f9ce 100644 --- a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs +++ b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs @@ -6,7 +6,6 @@ using System; using System.Text; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Moq; diff --git a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs index 709212b81..b0cd554c6 100644 --- a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs +++ b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs @@ -6,7 +6,6 @@ using System; using System.Text; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Moq; diff --git a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs index 993021bfa..dd6a48ce1 100644 --- a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs +++ b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs @@ -6,7 +6,6 @@ using System; using System.Text; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Moq; diff --git a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs index 9993f2b0b..d4f69ceb5 100644 --- a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs +++ b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs @@ -6,7 +6,6 @@ using System; using System.Text; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Moq; diff --git a/test/OpenIddict.Mvc.Tests/OpenIddict.Mvc.Tests.csproj b/test/OpenIddict.Mvc.Tests/OpenIddict.Mvc.Tests.csproj deleted file mode 100644 index 462022e31..000000000 --- a/test/OpenIddict.Mvc.Tests/OpenIddict.Mvc.Tests.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - netcoreapp2.0;net461 - netcoreapp2.0 - - - - - - - - - - - - - - - - - - - - diff --git a/test/OpenIddict.Mvc.Tests/OpenIddictMvcBuilderTests.cs b/test/OpenIddict.Mvc.Tests/OpenIddictMvcBuilderTests.cs deleted file mode 100644 index dd5794852..000000000 --- a/test/OpenIddict.Mvc.Tests/OpenIddictMvcBuilderTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Xunit; - -namespace OpenIddict.Mvc.Tests -{ - public class OpenIddictMvcBuilderTests - { - [Fact] - public void Constructor_ThrowsAnExceptionForNullServices() - { - // Arrange - var services = (IServiceCollection) null; - - // Act and assert - var exception = Assert.Throws(() => new OpenIddictMvcBuilder(services)); - - Assert.Equal("services", exception.ParamName); - } - - [Fact] - public void Configure_OptionsAreCorrectlyAmended() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.Configure(configuration => configuration.DisableBindingExceptions = true); - - var options = GetOptions(services); - - // Assert - Assert.True(options.DisableBindingExceptions); - } - - private static IServiceCollection CreateServices() - => new ServiceCollection().AddOptions(); - - private static OpenIddictMvcBuilder CreateBuilder(IServiceCollection services) - => new OpenIddictMvcBuilder(services); - - private static OpenIddictMvcOptions GetOptions(IServiceCollection services) - { - var provider = services.BuildServiceProvider(); - return provider.GetRequiredService>().CurrentValue; - } - } -} diff --git a/test/OpenIddict.Mvc.Tests/OpenIddictMvcConfigurationTests.cs b/test/OpenIddict.Mvc.Tests/OpenIddictMvcConfigurationTests.cs deleted file mode 100644 index 3338ca423..000000000 --- a/test/OpenIddict.Mvc.Tests/OpenIddictMvcConfigurationTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Linq; -using AspNet.Security.OpenIdConnect.Primitives; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Xunit; - -namespace OpenIddict.Mvc.Tests -{ - public class OpenIddictConfigurationExtensionsTests - { - [Fact] - public void Configure_ThrowsAnExceptionForNullOptions() - { - // Arrange - var configuration = new OpenIddictMvcConfiguration(); - - // Act and assert - var exception = Assert.Throws(() => configuration.Configure(null)); - - Assert.Equal("options", exception.ParamName); - } - - [Fact] - public void Configure_RegistersModelBinderProvider() - { - // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - - var builder = new OpenIddictServerBuilder(services); - - // Act - builder.UseMvc(); - - var options = services.BuildServiceProvider().GetRequiredService>(); - - // Assert - Assert.Contains(options.Value.ModelBinderProviders, binder => binder is OpenIddictMvcBinderProvider); - } - - [Fact] - public void Configure_RegistersModelMetadataDetailsProviders() - { - // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - - var builder = new OpenIddictServerBuilder(services); - - // Act - builder.UseMvc(); - - var options = services.BuildServiceProvider().GetRequiredService>(); - - // Assert - Assert.Contains( - options.Value.ModelMetadataDetailsProviders.OfType(), - provider => provider.Type == typeof(OpenIdConnectRequest) && - provider.BindingSource == BindingSource.Special); - Assert.Contains( - options.Value.ModelMetadataDetailsProviders.OfType(), - provider => provider.Type == typeof(OpenIdConnectResponse) && - provider.BindingSource == BindingSource.Special); - - Assert.Contains( - options.Value.ModelMetadataDetailsProviders.OfType(), - provider => provider.Type == typeof(OpenIdConnectRequest)); - Assert.Contains( - options.Value.ModelMetadataDetailsProviders.OfType(), - provider => provider.Type == typeof(OpenIdConnectResponse)); - } - } -} diff --git a/test/OpenIddict.Mvc.Tests/OpenIddictMvcExtensionsTests.cs b/test/OpenIddict.Mvc.Tests/OpenIddictMvcExtensionsTests.cs deleted file mode 100644 index f05362b14..000000000 --- a/test/OpenIddict.Mvc.Tests/OpenIddictMvcExtensionsTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Xunit; - -namespace OpenIddict.Mvc.Tests -{ - public class OpenIddictMvcExtensionsTests - { - [Fact] - public void UseMvc_ThrowsAnExceptionForNullBuilder() - { - // Arrange - var builder = (OpenIddictServerBuilder) null; - - // Act and assert - var exception = Assert.Throws(() => builder.UseMvc()); - - Assert.Equal("builder", exception.ParamName); - } - - [Fact] - public void UseMvc_ThrowsAnExceptionForNullConfiguration() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictServerBuilder(services); - - // Act and assert - var exception = Assert.Throws(() => builder.UseMvc(configuration: null)); - - Assert.Equal("configuration", exception.ParamName); - } - - [Fact] - public void UseMvc_RegistersConfiguration() - { - // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - - var builder = new OpenIddictServerBuilder(services); - - // Act - builder.UseMvc(); - - // Assert - Assert.Contains(services, service => service.ServiceType == typeof(IConfigureOptions) && - service.ImplementationType == typeof(OpenIddictMvcConfiguration)); - } - } -} diff --git a/test/OpenIddict.Mvc.Tests/OpenIddictMvcModelBinderProviderTests.cs b/test/OpenIddict.Mvc.Tests/OpenIddictMvcModelBinderProviderTests.cs deleted file mode 100644 index 750e4e200..000000000 --- a/test/OpenIddict.Mvc.Tests/OpenIddictMvcModelBinderProviderTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Generic; -using AspNet.Security.OpenIdConnect.Primitives; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; -using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; -using Moq; -using Xunit; - -namespace OpenIddict.Mvc.Tests -{ - public class OpenIddictMvcModelBinderProviderTests - { - [Theory] - [InlineData(typeof(object))] - [InlineData(typeof(IList))] - [InlineData(typeof(int[]))] - public void GetBinder_ReturnsNullForUnsupportedTypes(Type type) - { - // Arrange - var provider = new OpenIddictMvcBinderProvider(); - - var metadata = new Mock(ModelMetadataIdentity.ForType(type)); - - var context = new Mock(); - context.Setup(mock => mock.Metadata) - .Returns(metadata.Object); - - // Act and assert - Assert.Null(provider.GetBinder(context.Object)); - } - - [Theory] - [InlineData(typeof(OpenIdConnectRequest))] - [InlineData(typeof(OpenIdConnectResponse))] - public void GetBinder_ReturnsNonNullForSupportedTypes(Type type) - { - // Arrange - var provider = new OpenIddictMvcBinderProvider(); - - var metadata = new Mock(ModelMetadataIdentity.ForType(type)); - - var context = new Mock(); - context.Setup(mock => mock.Metadata) - .Returns(metadata.Object); - - // Act and assert - Assert.NotNull((BinderTypeModelBinder) provider.GetBinder(context.Object)); - } - } -} diff --git a/test/OpenIddict.Mvc.Tests/OpenIddictMvcModelBinderTests.cs b/test/OpenIddict.Mvc.Tests/OpenIddictMvcModelBinderTests.cs deleted file mode 100644 index ae5de2441..000000000 --- a/test/OpenIddict.Mvc.Tests/OpenIddictMvcModelBinderTests.cs +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Primitives; -using AspNet.Security.OpenIdConnect.Server; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; -using Microsoft.Extensions.Options; -using Moq; -using Xunit; - -namespace OpenIddict.Mvc.Tests -{ - public class OpenIddictMvcModelBinderTests - { - [Theory] - [InlineData(typeof(object))] - [InlineData(typeof(IList))] - [InlineData(typeof(int[]))] - public async Task BindModelAsync_ThrowsAnExceptionForUnsupportedTypes(Type type) - { - // Arrange - var options = Mock.Of>( - mock => mock.CurrentValue == new OpenIddictMvcOptions()); - - var binder = new OpenIddictMvcBinder(options); - var provider = new EmptyModelMetadataProvider(); - - var context = new DefaultModelBindingContext - { - ModelMetadata = provider.GetMetadataForType(type) - }; - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return binder.BindModelAsync(context); - }); - - Assert.Equal("The specified model type is not supported by this binder.", exception.Message); - } - - [Fact] - public async Task BindModelAsync_ThrowsAnExceptionWhenRequestCannotBeFound() - { - // Arrange - var options = Mock.Of>( - mock => mock.CurrentValue == new OpenIddictMvcOptions()); - - var binder = new OpenIddictMvcBinder(options); - var provider = new EmptyModelMetadataProvider(); - - var context = new DefaultModelBindingContext - { - ActionContext = new ActionContext() - { - HttpContext = new DefaultHttpContext(), - }, - - ModelMetadata = provider.GetMetadataForType(typeof(OpenIdConnectRequest)) - }; - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return binder.BindModelAsync(context); - }); - - Assert.Equal(new StringBuilder() - .AppendLine("The OpenID Connect request cannot be retrieved from the ASP.NET context.") - .Append("Make sure that 'app.UseAuthentication()' is called before 'app.UseMvc()' ") - .Append("and that the action route corresponds to the endpoint path registered via ") - .Append("'services.AddOpenIddict().AddServer().Enable[...]Endpoint(...)'.") - .ToString(), exception.Message); - } - - [Fact] - public async Task BindModelAsync_ReturnsNullWhenRequestCannotBeFoundAndExceptionsAreDisabled() - { - // Arrange - var options = Mock.Of>( - mock => mock.CurrentValue == new OpenIddictMvcOptions - { - DisableBindingExceptions = true - }); - - var binder = new OpenIddictMvcBinder(options); - var provider = new EmptyModelMetadataProvider(); - - var context = new DefaultModelBindingContext - { - ActionContext = new ActionContext() - { - HttpContext = new DefaultHttpContext(), - }, - - ModelMetadata = provider.GetMetadataForType(typeof(OpenIdConnectRequest)) - }; - - // Act - await binder.BindModelAsync(context); - - // Assert - Assert.True(context.Result.IsModelSet); - Assert.Null(context.Result.Model); - Assert.Equal(BindingSource.Special, context.BindingSource); - } - - [Fact] - public async Task BindModelAsync_ReturnsNullWhenResponseCannotBeFound() - { - // Arrange - var options = Mock.Of>( - mock => mock.CurrentValue == new OpenIddictMvcOptions()); - - var binder = new OpenIddictMvcBinder(options); - var provider = new EmptyModelMetadataProvider(); - - var context = new DefaultModelBindingContext - { - ActionContext = new ActionContext() - { - HttpContext = new DefaultHttpContext(), - }, - - ModelMetadata = provider.GetMetadataForType(typeof(OpenIdConnectResponse)), - - ValidationState = new ValidationStateDictionary() - }; - - // Act - await binder.BindModelAsync(context); - - // Assert - Assert.True(context.Result.IsModelSet); - Assert.Null(context.Result.Model); - Assert.Equal(BindingSource.Special, context.BindingSource); - } - - [Fact] - public async Task BindModelAsync_ReturnsAmbientRequest() - { - // Arrange - var options = Mock.Of>( - mock => mock.CurrentValue == new OpenIddictMvcOptions()); - - var binder = new OpenIddictMvcBinder(options); - var provider = new EmptyModelMetadataProvider(); - - var request = new OpenIdConnectRequest(); - - var features = new FeatureCollection(); - features.Set(new OpenIdConnectServerFeature - { - Request = request - }); - - var context = new DefaultModelBindingContext - { - ActionContext = new ActionContext() - { - HttpContext = new DefaultHttpContext(features), - }, - - ModelMetadata = provider.GetMetadataForType(typeof(OpenIdConnectRequest)), - - ValidationState = new ValidationStateDictionary() - }; - - // Act - await binder.BindModelAsync(context); - - // Assert - Assert.True(context.Result.IsModelSet); - Assert.Same(request, context.Result.Model); - Assert.True(context.ValidationState[request].SuppressValidation); - Assert.Equal(BindingSource.Special, context.BindingSource); - } - - [Fact] - public async Task BindModelAsync_ReturnsAmbientResponse() - { - // Arrange - var options = Mock.Of>( - mock => mock.CurrentValue == new OpenIddictMvcOptions()); - - var binder = new OpenIddictMvcBinder(options); - var provider = new EmptyModelMetadataProvider(); - - var response = new OpenIdConnectResponse(); - - var features = new FeatureCollection(); - features.Set(new OpenIdConnectServerFeature - { - Response = response - }); - - var context = new DefaultModelBindingContext - { - ActionContext = new ActionContext() - { - HttpContext = new DefaultHttpContext(features), - }, - - ModelMetadata = provider.GetMetadataForType(typeof(OpenIdConnectResponse)), - - ValidationState = new ValidationStateDictionary() - }; - - // Act - await binder.BindModelAsync(context); - - // Assert - Assert.True(context.Result.IsModelSet); - Assert.Same(response, context.Result.Model); - Assert.True(context.ValidationState[response].SuppressValidation); - Assert.Equal(BindingSource.Special, context.BindingSource); - } - } -} diff --git a/test/OpenIddict.NHibernate.Tests/OpenIddict.NHibernate.Tests.csproj b/test/OpenIddict.NHibernate.Tests/OpenIddict.NHibernate.Tests.csproj index 755404e11..0f4334cf4 100644 --- a/test/OpenIddict.NHibernate.Tests/OpenIddict.NHibernate.Tests.csproj +++ b/test/OpenIddict.NHibernate.Tests/OpenIddict.NHibernate.Tests.csproj @@ -1,10 +1,8 @@  - - - netcoreapp2.0;net461 - netcoreapp2.0 + netcoreapp3.0;net461 + netcoreapp3.0 @@ -12,15 +10,8 @@ - - + - - - - - - diff --git a/test/OpenIddict.NHibernate.Tests/OpenIddictNHibernateExtensionsTests.cs b/test/OpenIddict.NHibernate.Tests/OpenIddictNHibernateExtensionsTests.cs index 50d06a323..76fd82c06 100644 --- a/test/OpenIddict.NHibernate.Tests/OpenIddictNHibernateExtensionsTests.cs +++ b/test/OpenIddict.NHibernate.Tests/OpenIddictNHibernateExtensionsTests.cs @@ -5,7 +5,6 @@ */ using System; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; diff --git a/test/OpenIddict.Server.Tests/Certificate.pfx b/test/OpenIddict.Server.Tests/Certificate.pfx deleted file mode 100644 index 8c05f9d4d414d5036c38f34f8755b711d4f8811d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2482 zcmY*adpOi-8~)9~jG=@X8WTe~hS6ZwsihX>Y>NC<>g0B7;H%2Ny|7*MT)LXl75H2CWrbxRg^g!5{B?SkmJ19R%Qs-hi}4MT4(21l<7_QTC; zhFRy@iAohZig$N!6`CqnNIX-OrcKMTbXD>*;`^6iMf9U}dzi`ET}#p0kiX zn_E1rxtYUy_&BM~tp_t8;H-q*<=GUVUmD{P%#_n9H?OTv{+@XM2xMHjW5uGKg=k~f z2amqit^(MKN6($FO@8qb&GXH6QDHUc+>>gj!9;Ki*6b?iJ(r zH$ZEI!bbNfD(YJuWL1T|K3soS0x?9m{LHl8*pM|f7COg9o59RbqmdSdK6}rtaRh9R zh{}vxI>PL>d)zjmv8lG`tf;if2r{wa$W8_EVo2ualvRc({_ry4P0H5&`!V`2R?x7n zECIoRGmhIk;mY^m$#nQpF|*J}ILgR|Dl*ACh6 zL!uIQ)(bJ6l*Wh8Uxz<1U+DP6qMhf7rZ3^W&FxmCsO=W3B3h(y1O7maO3mg8x!O4t zKh>*~vIt9#M3Jm3(tPFYugc5>Y8Tab566wYYRQ~>x{j^%@phz=B`d$Y94Vye7!1lm zzopGDR_vuVxEgH~4auUn9&@&8QJ0tb^4H8=1<);5biZd3-6yU|4q6N`N~-JIb>yM< zVktlMx6;*l+0X*qlbDdN-?+~l7>^DFYAZW?`HJ>)Z}YNMo*pE zNt~>i|Kvvc<-WJa%cdlR`>fD}F(Y~`{V>n}QjcNGzS-J#@`rY%vnDsh+@dO5en!(U zBuQnf(^`bUxn-`?1x6G7kq=!e>-s1wDH5P$Xw|O8MMP)D%|lPsblz)f_GG}M4?bft z#XHkP4<^QM~7@0#~Sb)_|tN`J76Lmi5c(cd#O9Qnt3${(~@3g)(|Ev(?M zyFX?&UT*HrAcuu!pB+n38XeYb6D#TdoQ{;|d}$5Ux4hGF_k?q+XwsA%srzbUd-ncY zHs{490^Vvv^0hSM$B5{EzDtyZCISwJ002OA`nQSEw8WbOdVmk`6QBjc6VL}i0GS>L zUyzYNMglnjI0HKL0WXkg0^T5WK_>~UI-sHpYQDrk1WFA7fkSAzVh~MtyenV>1b_i5 z5CVjQ@CD8Ta=CuvR={Zy(+J6mza>Mr6C7Vvxv6 z4lrB08stzB^NX~KaiDBm(~oLwK>5wBpDCpxp=^J2gt*jX%-HPSw7t&j8z~12-6E;F zuGMuD#gAP0;TvL4-+jalN_ZpJpl>Vd4O&>YtR2mghGh+@E(XL?>m@#BjV45z;K{sv z+E?{{8{VMDHV~k*>lYa%pY+7Ex8hEliD!J|>$%&BCm)83Ck^AZ9O6W7aOcl=M@HdFaVt)rR! zyK?k{iUuJENoGWb{Q(eu4`y(Tn+V3eoRcG(DE;G37Wt3uH!^aZI6Up0 zF7kD2SCx3NHcZ7k)zSI+l9YG0wo?wH7_!&t3!4a3&8iE~fbe2fhy zS$9U~H&Pkt&7&o*miw1n9P_^F$o!iz&-6P6=udX)0hsaC;;m`bydX)+;We?F-0{!x zH+}6=f3Gl5Tiz|K9*a|YT+57jPsq9A28Sa}H&!&n6hzuu`@Iy37LO1YrYWlj{5!9C zwtX#^$=Ix{w=0Qaa$Z;4CJs3FZSPTk9G+~FL^#}$O;E5OnIP&CWr;|HfSL#lii2a` uIJb^0)}Y-Z<4p2IzGW4c$JV97F}teVFIaxKqA_Or9lZuCV;KvZEdB|+V@&1% diff --git a/test/OpenIddict.Server.Tests/OpenIddict.Server.Tests.csproj b/test/OpenIddict.Server.Tests/OpenIddict.Server.Tests.csproj deleted file mode 100644 index 318aed83a..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddict.Server.Tests.csproj +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - netcoreapp2.0;net461 - $(TargetFrameworks);net47 - netcoreapp2.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - $(DefineConstants);SUPPORTS_ECDSA - - - - $(DefineConstants);SUPPORTS_CERTIFICATE_GENERATION - - - diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs deleted file mode 100644 index 7379d6d95..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs +++ /dev/null @@ -1,942 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.IdentityModel.Tokens.Jwt; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using Moq; -using OpenIddict.Abstractions; -using Xunit; -using static OpenIddict.Server.OpenIddictServerEvents; - -namespace OpenIddict.Server.Tests -{ - public class OpenIddictServerBuilderTests - { - [Fact] - public void Constructor_ThrowsAnExceptionForNullServices() - { - // Arrange - var services = (IServiceCollection) null; - - // Act and assert - var exception = Assert.Throws(() => new OpenIddictServerBuilder(services)); - - Assert.Equal("services", exception.ParamName); - } - - [Fact] - public void AddEventHandler_HandlerIsAttached() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AddEventHandler(notification => Task.FromResult(OpenIddictServerEventState.Handled)); - - // Assert - Assert.Contains(services, service => - service.ServiceType == typeof(IOpenIddictServerEventHandler) && - service.ImplementationInstance.GetType() == typeof(OpenIddictServerEventHandler)); - } - - [Fact] - public void AddEventHandler_ThrowsAnExceptionForUnsupportedLifetime() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act and assert - var exception = Assert.Throws(delegate - { - return builder.AddEventHandler(ServiceLifetime.Transient); - }); - - Assert.Equal("lifetime", exception.ParamName); - Assert.StartsWith("Handlers cannot be registered as transient services.", exception.Message); - } - - [Fact] - public void AddEventHandler_ThrowsAnExceptionForOpenGenericHandlerType() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act and assert - var exception = Assert.Throws(delegate - { - return builder.AddEventHandler(typeof(OpenIddictServerEventHandler<>)); - }); - - Assert.Equal("type", exception.ParamName); - Assert.StartsWith("The specified type is invalid.", exception.Message); - } - - [Fact] - public void AddEventHandler_ThrowsAnExceptionForNonHandlerType() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act and assert - var exception = Assert.Throws(delegate - { - return builder.AddEventHandler(typeof(object)); - }); - - Assert.Equal("type", exception.ParamName); - Assert.StartsWith("The specified type is invalid.", exception.Message); - } - - [Fact] - public void AddEventHandler_HandlerIsRegistered() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AddEventHandler(ServiceLifetime.Singleton); - - // Assert - Assert.Contains(services, service => - service.ServiceType == typeof(IOpenIddictServerEventHandler) && - service.ImplementationType == typeof(CustomHandler) && - service.Lifetime == ServiceLifetime.Singleton); - Assert.Contains(services, service => - service.ServiceType == typeof(IOpenIddictServerEventHandler) && - service.ImplementationType == typeof(CustomHandler) && - service.Lifetime == ServiceLifetime.Singleton); - } - - [Fact] - public void Configure_OptionsAreCorrectlyAmended() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.Configure(configuration => configuration.AccessTokenLifetime = TimeSpan.FromDays(1)); - - var options = GetOptions(services); - - // Assert - Assert.Equal(TimeSpan.FromDays(1), options.AccessTokenLifetime); - } - - [Fact] - public void AddDevelopmentSigningCertificate_ThrowsAnExceptionForNullSubject() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act and assert - var exception = Assert.Throws(delegate - { - builder.AddDevelopmentSigningCertificate(subject: null); - }); - - Assert.Equal("subject", exception.ParamName); - } - -#if SUPPORTS_CERTIFICATE_GENERATION - [Fact] - public void AddDevelopmentSigningCertificate_CanGenerateCertificate() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AddDevelopmentSigningCertificate(); - - var options = GetOptions(services); - - // Assert - Assert.Equal(1, options.SigningCredentials.Count); - Assert.Equal(SecurityAlgorithms.RsaSha256, options.SigningCredentials[0].Algorithm); - Assert.NotNull(options.SigningCredentials[0].Kid); - } -#else - [Fact] - public void AddDevelopmentSigningCertificate_ThrowsAnExceptionOnUnsupportedPlatforms() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - builder.AddDevelopmentSigningCertificate(); - - // Act and assert - var exception = Assert.Throws(delegate - { - return GetOptions(services); - }); - - Assert.Equal("X.509 certificate generation is not supported on this platform.", exception.Message); - } -#endif - - [Fact] - public void AddEphemeralSigningKey_SigningKeyIsCorrectlyAdded() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AddEphemeralSigningKey(); - - var options = GetOptions(services); - - // Assert - Assert.Equal(1, options.SigningCredentials.Count); - } - - [Theory] - [InlineData(SecurityAlgorithms.RsaSha256)] - [InlineData(SecurityAlgorithms.RsaSha384)] - [InlineData(SecurityAlgorithms.RsaSha512)] -#if SUPPORTS_ECDSA - [InlineData(SecurityAlgorithms.EcdsaSha256)] - [InlineData(SecurityAlgorithms.EcdsaSha384)] - [InlineData(SecurityAlgorithms.EcdsaSha512)] -#endif - public void AddEphemeralSigningKey_SigningCredentialsUseSpecifiedAlgorithm(string algorithm) - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AddEphemeralSigningKey(algorithm); - - var options = GetOptions(services); - var credentials = options.SigningCredentials[0]; - - // Assert - Assert.Equal(algorithm, credentials.Algorithm); - } - - [Fact] - public void AddEncryptingKey_EncryptingKeyIsCorrectlyAdded() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - var factory = Mock.Of(mock => - mock.IsSupportedAlgorithm(SecurityAlgorithms.Aes256KW, It.IsAny())); - - var key = Mock.Of(mock => mock.CryptoProviderFactory == factory); - - // Act - builder.AddEncryptingKey(key); - - var options = GetOptions(services); - - // Assert - Assert.Same(key, options.EncryptingCredentials[0].Key); - } - - [Theory] - [InlineData(SecurityAlgorithms.HmacSha256)] - [InlineData(SecurityAlgorithms.RsaSha256)] -#if SUPPORTS_ECDSA - [InlineData(SecurityAlgorithms.EcdsaSha256)] - [InlineData(SecurityAlgorithms.EcdsaSha384)] - [InlineData(SecurityAlgorithms.EcdsaSha512)] -#endif - public void AddSigningKey_SigningKeyIsCorrectlyAdded(string algorithm) - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - var factory = Mock.Of(mock => - mock.IsSupportedAlgorithm(algorithm, It.IsAny())); - - var key = Mock.Of(mock => mock.CryptoProviderFactory == factory); - - // Act - builder.AddSigningKey(key); - - var options = GetOptions(services); - - // Assert - Assert.Same(key, options.SigningCredentials[0].Key); - } - - [Fact] - public void AddSigningCertificate_SigningKeyIsCorrectlyAdded() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AddSigningCertificate( - assembly: typeof(OpenIddictServerBuilderTests).GetTypeInfo().Assembly, - resource: "OpenIddict.Server.Tests.Certificate.pfx", - password: "OpenIddict"); - - var options = GetOptions(services); - - // Assert - Assert.IsType(options.SigningCredentials[0].Key); - } - - [Fact] - public void AllowAuthorizationCodeFlow_CodeFlowIsAddedToGrantTypes() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AllowAuthorizationCodeFlow(); - - var options = GetOptions(services); - - // Assert - Assert.Contains(OpenIddictConstants.GrantTypes.AuthorizationCode, options.GrantTypes); - } - - [Fact] - public void AllowClientCredentialsFlow_ClientCredentialsFlowIsAddedToGrantTypes() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AllowClientCredentialsFlow(); - - var options = GetOptions(services); - - // Assert - Assert.Contains(OpenIddictConstants.GrantTypes.ClientCredentials, options.GrantTypes); - } - - [Fact] - public void AllowCustomFlow_CustomFlowIsAddedToGrantTypes() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant"); - - var options = GetOptions(services); - - // Assert - Assert.Contains("urn:ietf:params:oauth:grant-type:custom_grant", options.GrantTypes); - } - - [Fact] - public void AllowImplicitFlow_ImplicitFlowIsAddedToGrantTypes() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AllowImplicitFlow(); - - var options = GetOptions(services); - - // Assert - Assert.Contains(OpenIddictConstants.GrantTypes.Implicit, options.GrantTypes); - } - - [Fact] - public void AllowPasswordFlow_PasswordFlowIsAddedToGrantTypes() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AllowPasswordFlow(); - - var options = GetOptions(services); - - // Assert - Assert.Contains(OpenIddictConstants.GrantTypes.Password, options.GrantTypes); - } - - [Fact] - public void AllowRefreshTokenFlow_RefreshTokenFlowIsAddedToGrantTypes() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AllowRefreshTokenFlow(); - - var options = GetOptions(services); - - // Assert - Assert.Contains(OpenIddictConstants.GrantTypes.RefreshToken, options.GrantTypes); - } - - [Fact] - public void DisableAuthorizationStorage_AuthorizationStorageIsDisabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.DisableAuthorizationStorage(); - - var options = GetOptions(services); - - // Assert - Assert.True(options.DisableAuthorizationStorage); - } - - [Fact] - public void DisableConfigurationEndpoint_ConfigurationEndpointIsDisabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.DisableConfigurationEndpoint(); - - var options = GetOptions(services); - - // Assert - Assert.Equal(PathString.Empty, options.ConfigurationEndpointPath); - } - - [Fact] - public void DisableCryptographyEndpoint_CryptographyEndpointIsDisabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.DisableCryptographyEndpoint(); - - var options = GetOptions(services); - - // Assert - Assert.Equal(PathString.Empty, options.CryptographyEndpointPath); - } - - [Fact] - public void DisableSlidingExpiration_SlidingExpirationIsDisabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.DisableSlidingExpiration(); - - var options = GetOptions(services); - - // Assert - Assert.False(options.UseSlidingExpiration); - } - - [Fact] - public void DisableTokenStorage_TokenStorageIsDisabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.DisableTokenStorage(); - - var options = GetOptions(services); - - // Assert - Assert.True(options.DisableTokenStorage); - } - - [Fact] - public void EnableAuthorizationEndpoint_AuthorizationEndpointIsEnabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.EnableAuthorizationEndpoint("/endpoint-path"); - - var options = GetOptions(services); - - // Assert - Assert.Equal("/endpoint-path", options.AuthorizationEndpointPath); - } - - [Fact] - public void EnableIntrospectionEndpoint_IntrospectionEndpointIsEnabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.EnableIntrospectionEndpoint("/endpoint-path"); - - var options = GetOptions(services); - - // Assert - Assert.Equal("/endpoint-path", options.IntrospectionEndpointPath); - } - - [Fact] - public void EnableLogoutEndpoint_LogoutEndpointIsEnabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.EnableLogoutEndpoint("/endpoint-path"); - - var options = GetOptions(services); - - // Assert - Assert.Equal("/endpoint-path", options.LogoutEndpointPath); - } - - [Fact] - public void EnableRequestCaching_RequestCachingIsEnabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.EnableRequestCaching(); - - var options = GetOptions(services); - - // Assert - Assert.True(options.EnableRequestCaching); - } - - [Fact] - public void EnableRevocationEndpoint_RevocationEndpointIsEnabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.EnableRevocationEndpoint("/endpoint-path"); - - var options = GetOptions(services); - - // Assert - Assert.Equal("/endpoint-path", options.RevocationEndpointPath); - } - - [Fact] - public void DisableScopeValidation_ScopeValidationIsDisabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.DisableScopeValidation(); - - var options = GetOptions(services); - - // Assert - Assert.True(options.DisableScopeValidation); - } - - [Fact] - public void EnableTokenEndpoint_TokenEndpointIsEnabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.EnableTokenEndpoint("/endpoint-path"); - - var options = GetOptions(services); - - // Assert - Assert.Equal("/endpoint-path", options.TokenEndpointPath); - } - - [Fact] - public void EnableUserinfoEndpoint_UserinfoEndpointIsEnabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.EnableUserinfoEndpoint("/endpoint-path"); - - var options = GetOptions(services); - - // Assert - Assert.Equal("/endpoint-path", options.UserinfoEndpointPath); - } - - [Fact] - public void AcceptAnonymousClients_ClientIdentificationIsOptional() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AcceptAnonymousClients(); - - var options = GetOptions(services); - - // Assert - Assert.True(options.AcceptAnonymousClients); - } - - [Fact] - public void SetAccessTokenLifetime_DefaultAccessTokenLifetimeIsReplaced() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.SetAccessTokenLifetime(TimeSpan.FromMinutes(42)); - - var options = GetOptions(services); - - // Assert - Assert.Equal(TimeSpan.FromMinutes(42), options.AccessTokenLifetime); - } - - [Fact] - public void SetAccessTokenLifetime_AccessTokenLifetimeCanBeSetToNull() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.SetAccessTokenLifetime(null); - - var options = GetOptions(services); - - // Assert - Assert.Null(options.AccessTokenLifetime); - } - - [Fact] - public void SetAuthorizationCodeLifetime_DefaultAuthorizationCodeLifetimeIsReplaced() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.SetAuthorizationCodeLifetime(TimeSpan.FromMinutes(42)); - - var options = GetOptions(services); - - // Assert - Assert.Equal(TimeSpan.FromMinutes(42), options.AuthorizationCodeLifetime); - } - - [Fact] - public void SetAuthorizationCodeLifetime_AuthorizationCodeLifetimeCanBeSetToNull() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.SetAuthorizationCodeLifetime(null); - - var options = GetOptions(services); - - // Assert - Assert.Null(options.AuthorizationCodeLifetime); - } - - [Fact] - public void SetIdentityTokenLifetime_DefaultIdentityTokenLifetimeIsReplaced() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.SetIdentityTokenLifetime(TimeSpan.FromMinutes(42)); - - var options = GetOptions(services); - - // Assert - Assert.Equal(TimeSpan.FromMinutes(42), options.IdentityTokenLifetime); - } - - [Fact] - public void SetIdentityTokenLifetime_IdentityTokenLifetimeCanBeSetToNull() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.SetIdentityTokenLifetime(null); - - var options = GetOptions(services); - - // Assert - Assert.Null(options.IdentityTokenLifetime); - } - - [Fact] - public void SetRefreshTokenLifetime_DefaultRefreshTokenLifetimeIsReplaced() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.SetRefreshTokenLifetime(TimeSpan.FromMinutes(42)); - - var options = GetOptions(services); - - // Assert - Assert.Equal(TimeSpan.FromMinutes(42), options.RefreshTokenLifetime); - } - - [Fact] - public void SetRefreshTokenLifetime_RefreshTokenLifetimeCanBeSetToNull() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.SetRefreshTokenLifetime(null); - - var options = GetOptions(services); - - // Assert - Assert.Null(options.RefreshTokenLifetime); - } - - [Fact] - public void SetRequestCachingPolicy_ThrowsAnExceptionForNullPolicy() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act and assert - var exception = Assert.Throws(() => builder.SetRequestCachingPolicy(null)); - - Assert.Equal("policy", exception.ParamName); - } - - [Fact] - public void SetRequestCachingPolicy_PolicyIsUpdated() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - var policy = new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(42), - SlidingExpiration = TimeSpan.FromSeconds(42) - }; - - // Act - builder.SetRequestCachingPolicy(policy); - - var options = GetOptions(services); - - // Assert - Assert.Same(policy, options.RequestCachingPolicy); - } - - [Fact] - public void SetIssuer_ThrowsAnExceptionForNullIssuer() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act and assert - var exception = Assert.Throws(() => builder.SetIssuer(null)); - - Assert.Equal("address", exception.ParamName); - } - - [Fact] - public void SetIssuer_AddressIsReplaced() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.SetIssuer(new Uri("http://www.fabrikam.com/")); - - var options = GetOptions(services); - - // Assert - Assert.Equal(new Uri("http://www.fabrikam.com/"), options.Issuer); - } - - [Fact] - public void RegisterClaims_ClaimsAreAdded() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.RegisterClaims("custom_claim_1", "custom_claim_2"); - - var options = GetOptions(services); - - // Assert - Assert.Contains("custom_claim_1", options.Claims); - Assert.Contains("custom_claim_2", options.Claims); - } - - [Fact] - public void RegisterScopes_ScopesAreAdded() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.RegisterScopes("custom_scope_1", "custom_scope_2"); - - var options = GetOptions(services); - - // Assert - Assert.Contains("custom_scope_1", options.Scopes); - Assert.Contains("custom_scope_2", options.Scopes); - } - - [Fact] - public void RequireProofKeyForCodeExchange_PkceIsEnforced() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.RequireProofKeyForCodeExchange(); - - var options = GetOptions(services); - - // Assert - Assert.True(options.RequireProofKeyForCodeExchange); - } - - [Fact] - public void UseDataProtectionProvider_DefaultProviderIsReplaced() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.UseDataProtectionProvider(new EphemeralDataProtectionProvider()); - - var options = GetOptions(services); - - // Assert - Assert.IsType(options.DataProtectionProvider); - } - - [Fact] - public void UseJsonWebTokens_AccessTokenHandlerIsCorrectlySet() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.UseJsonWebTokens(); - - var options = GetOptions(services); - - // Assert - Assert.IsType(options.AccessTokenHandler); - } - - [Fact] - public void UseReferenceTokens_ReferenceTokensAreEnabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.UseReferenceTokens(); - - var options = GetOptions(services); - - // Assert - Assert.True(options.UseReferenceTokens); - } - - private static IServiceCollection CreateServices() - => new ServiceCollection().AddOptions(); - - private static OpenIddictServerBuilder CreateBuilder(IServiceCollection services) - => new OpenIddictServerBuilder(services); - - private static OpenIddictServerOptions GetOptions(IServiceCollection services) - { - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); - return options.Get(OpenIddictServerDefaults.AuthenticationScheme); - } - - public class CustomHandler : IOpenIddictServerEventHandler, - IOpenIddictServerEventHandler - { - public Task HandleAsync(ApplyAuthorizationResponse notification) - { - throw new NotImplementedException(); - } - - public Task HandleAsync(HandleAuthorizationRequest notification) - { - throw new NotImplementedException(); - } - } - } -} diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerConfigurationTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerConfigurationTests.cs deleted file mode 100644 index 8dd8d0f89..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerConfigurationTests.cs +++ /dev/null @@ -1,458 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Text; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Client; -using AspNet.Security.OpenIdConnect.Server; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using OpenIddict.Abstractions; -using Xunit; - -namespace OpenIddict.Server.Tests -{ - public class OpenIddictServerConfigurationTests - { - [Fact] - public void Configure_ThrowsAnExceptionForNullOptions() - { - // Arrange - var configuration = new OpenIddictServerConfiguration( - Mock.Of(), - Mock.Of()); - - // Act and assert - var exception = Assert.Throws(() => configuration.Configure(null)); - - Assert.Equal("options", exception.ParamName); - } - - [Fact] - public void Configure_ThrowsAnExceptionWhenSchemeIsAlreadyRegisteredWithDifferentHandlerType() - { - // Arrange - var options = new AuthenticationOptions(); - options.AddScheme(OpenIddictServerDefaults.AuthenticationScheme, builder => - { - builder.HandlerType = typeof(OpenIdConnectServerHandler); - }); - - var configuration = new OpenIddictServerConfiguration( - Mock.Of(), - Mock.Of()); - - // Act and assert - var exception = Assert.Throws(() => configuration.Configure(options)); - - Assert.Equal(new StringBuilder() - .AppendLine("The OpenIddict server handler cannot be registered as an authentication scheme.") - .AppendLine("This may indicate that an instance of the OpenID Connect server was registered.") - .Append("Make sure that 'services.AddAuthentication().AddOpenIdConnectServer()' is not used.") - .ToString(), exception.Message); - } - - [Theory] - [InlineData(new object[] { new string[] { OpenIddictServerDefaults.AuthenticationScheme, null, null, null, null, null } })] - [InlineData(new object[] { new string[] { null, OpenIddictServerDefaults.AuthenticationScheme, null, null, null, null } })] - [InlineData(new object[] { new string[] { null, null, OpenIddictServerDefaults.AuthenticationScheme, null, null, null } })] - [InlineData(new object[] { new string[] { null, null, null, OpenIddictServerDefaults.AuthenticationScheme, null, null } })] - [InlineData(new object[] { new string[] { null, null, null, null, OpenIddictServerDefaults.AuthenticationScheme, null } })] - [InlineData(new object[] { new string[] { null, null, null, null, null, OpenIddictServerDefaults.AuthenticationScheme } })] - public void PostConfigure_ThrowsAnExceptionWhenDefaultSchemesPointToServerHandler(string[] schemes) - { - // Arrange - var options = new AuthenticationOptions - { - DefaultAuthenticateScheme = schemes[0], - DefaultChallengeScheme = schemes[1], - DefaultForbidScheme = schemes[2], - DefaultScheme = schemes[3], - DefaultSignInScheme = schemes[4], - DefaultSignOutScheme = schemes[5] - }; - - options.AddScheme(OpenIddictServerDefaults.AuthenticationScheme, displayName: null); - - var configuration = new OpenIddictServerConfiguration( - Mock.Of(), - Mock.Of()); - - // Act and assert - var exception = Assert.Throws(() => configuration.PostConfigure(Options.DefaultName, options)); - - // Assert - Assert.Equal(new StringBuilder() - .AppendLine("The OpenIddict server handler cannot be used as the default scheme handler.") - .Append("Make sure that neither DefaultAuthenticateScheme, DefaultChallengeScheme, ") - .Append("DefaultForbidScheme, DefaultSignInScheme, DefaultSignOutScheme nor DefaultScheme ") - .Append("point to an instance of the OpenIddict server handler.") - .ToString(), exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenRandomNumberGeneratorIsNull() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.RandomNumberGenerator = null); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - // Assert - Assert.Equal("A random number generator must be registered.", exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenProviderTypeIsNull() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.ProviderType = null); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - // Assert - Assert.Equal(new StringBuilder() - .AppendLine("OpenIddict can only be used with its built-in server provider.") - .AppendLine("This error may indicate that 'OpenIddictServerOptions.ProviderType' was manually set.") - .Append("To execute custom request handling logic, consider registering an event handler using ") - .Append("the generic 'services.AddOpenIddict().AddServer().AddEventHandler()' method.") - .ToString(), exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenProviderTypeIsIncompatible() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.ProviderType = typeof(OpenIdConnectServerProvider)); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - // Assert - Assert.Equal(new StringBuilder() - .AppendLine("OpenIddict can only be used with its built-in server provider.") - .AppendLine("This error may indicate that 'OpenIddictServerOptions.ProviderType' was manually set.") - .Append("To execute custom request handling logic, consider registering an event handler using ") - .Append("the generic 'services.AddOpenIddict().AddServer().AddEventHandler()' method.") - .ToString(), exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenNoFlowIsEnabled() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - // Assert - Assert.Equal("At least one OAuth2/OpenID Connect flow must be enabled.", exception.Message); - } - - [Theory] - [InlineData(OpenIddictConstants.GrantTypes.AuthorizationCode)] - [InlineData(OpenIddictConstants.GrantTypes.Implicit)] - public async Task PostConfigure_ThrowsAnExceptionWhenAuthorizationEndpointIsDisabled(string flow) - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.GrantTypes.Add(flow)) - .Configure(options => options.AuthorizationEndpointPath = PathString.Empty); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - Assert.Equal("The authorization endpoint must be enabled to use the authorization code and implicit flows.", exception.Message); - } - - [Theory] - [InlineData(OpenIddictConstants.GrantTypes.AuthorizationCode)] - [InlineData(OpenIddictConstants.GrantTypes.ClientCredentials)] - [InlineData(OpenIddictConstants.GrantTypes.Password)] - [InlineData(OpenIddictConstants.GrantTypes.RefreshToken)] - public async Task PostConfigure_ThrowsAnExceptionWhenTokenEndpointIsDisabled(string flow) - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.EnableAuthorizationEndpoint("/connect/authorize") - .Configure(options => options.GrantTypes.Add(flow)) - .Configure(options => options.TokenEndpointPath = PathString.Empty); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - Assert.Equal("The token endpoint must be enabled to use the authorization code, " + - "client credentials, password and refresh token flows.", exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenCachingPolicyIsNullAndRequestCachingEnabled() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.EnableAuthorizationEndpoint("/connect/authorize") - .AllowImplicitFlow() - .EnableRequestCaching(); - - builder.Configure(options => options.RequestCachingPolicy = null); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - Assert.Equal("A caching policy must be specified when enabling request caching.", exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenTokenStorageIsDisabled() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.EnableAuthorizationEndpoint("/connect/authorize") - .EnableRevocationEndpoint("/connect/revocation") - .AllowImplicitFlow() - .DisableTokenStorage(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - Assert.Equal("The revocation endpoint cannot be enabled when token storage is disabled.", exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenUsingReferenceTokensWithTokenStorageDisabled() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.EnableAuthorizationEndpoint("/connect/authorize") - .AllowImplicitFlow() - .DisableTokenStorage() - .UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - Assert.Equal("Reference tokens cannot be used when disabling token storage.", exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenUsingSlidingExpirationWithoutRollingTokensAndWithTokenStorageDisabled() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.EnableAuthorizationEndpoint("/connect/authorize") - .AllowImplicitFlow() - .DisableTokenStorage(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - Assert.Equal("Sliding expiration must be disabled when turning off " + - "token storage if rolling tokens are not used.", exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenUsingReferenceTokensIfAnAccessTokenHandlerIsSet() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.EnableAuthorizationEndpoint("/connect/authorize") - .AllowImplicitFlow() - .UseReferenceTokens() - .UseJsonWebTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - Assert.Equal("Reference tokens cannot be used when configuring JWT as the access token format.", exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenNoSigningKeyIsRegisteredIfAnAccessTokenHandlerIsSet() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.EnableAuthorizationEndpoint("/connect/authorize") - .EnableTokenEndpoint("/connect/token") - .AllowAuthorizationCodeFlow() - .UseJsonWebTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - Assert.Equal(new StringBuilder() - .AppendLine("At least one signing key must be registered when using JWT as the access token format.") - .Append("Consider registering a certificate using 'services.AddOpenIddict().AddServer().AddSigningCertificate()' ") - .Append("or 'services.AddOpenIddict().AddServer().AddDevelopmentSigningCertificate()' or call ") - .Append("'services.AddOpenIddict().AddServer().AddEphemeralSigningKey()' to use an ephemeral key.") - .ToString(), exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenNoSigningKeyIsRegisteredIfTheImplicitFlowIsEnabled() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.EnableAuthorizationEndpoint("/connect/authorize") - .AllowImplicitFlow(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - Assert.Equal(new StringBuilder() - .AppendLine("At least one asymmetric signing key must be registered when enabling the implicit flow.") - .Append("Consider registering a certificate using 'services.AddOpenIddict().AddServer().AddSigningCertificate()' ") - .Append("or 'services.AddOpenIddict().AddServer().AddDevelopmentSigningCertificate()' or call ") - .Append("'services.AddOpenIddict().AddServer().AddEphemeralSigningKey()' to use an ephemeral key.") - .ToString(), exception.Message); - } - - private static TestServer CreateAuthorizationServer(Action configuration = null) - { - var builder = new WebHostBuilder(); - - builder.UseEnvironment("Testing"); - - builder.ConfigureLogging(options => options.AddDebug()); - - builder.ConfigureServices(services => - { - services.AddAuthentication(); - services.AddOptions(); - services.AddDistributedMemoryCache(); - - services.AddOpenIddict() - .AddCore(options => - { - options.SetDefaultApplicationEntity() - .SetDefaultAuthorizationEntity() - .SetDefaultScopeEntity() - .SetDefaultTokenEntity(); - }) - - .AddServer(options => configuration?.Invoke(options)); - }); - - builder.Configure(app => - { - app.UseAuthentication(); - - app.Run(context => context.ChallengeAsync(OpenIddictServerDefaults.AuthenticationScheme)); - }); - - return new TestServer(builder); - } - - public class OpenIddictApplication { } - public class OpenIddictAuthorization { } - public class OpenIddictScope { } - public class OpenIddictToken { } - } -} diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerEventDispatcherTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerEventDispatcherTests.cs deleted file mode 100644 index ef9ae50ef..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerEventDispatcherTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Moq; -using Xunit; - -namespace OpenIddict.Server.Tests -{ - public class OpenIddictServerEventDispatcherTests - { - [Fact] - public async Task DispatchAsync_ThrowsAnExceptionForNullNotification() - { - // Arrange - var provider = Mock.Of(); - var dispatcher = new OpenIddictServerEventDispatcher(provider); - - // Act and assert - var exception = await Assert.ThrowsAsync(() - => dispatcher.DispatchAsync(notification: null)); - - Assert.Equal("notification", exception.ParamName); - } - - [Fact] - public async Task DispatchAsync_InvokesHandlers() - { - // Arrange - var handlers = new List> - { - Mock.Of>(), - Mock.Of>() - }; - - var provider = new Mock(); - provider.Setup(mock => mock.GetService(typeof(IEnumerable>))) - .Returns(handlers); - - var dispatcher = new OpenIddictServerEventDispatcher(provider.Object); - - var notification = new Event(); - - // Act - await dispatcher.DispatchAsync(notification); - - // Assert - Mock.Get(handlers[0]).Verify(mock => mock.HandleAsync(notification), Times.Once()); - Mock.Get(handlers[1]).Verify(mock => mock.HandleAsync(notification), Times.Once()); - } - - [Fact] - public async Task DispatchAsync_StopsInvokingHandlersWhenHandledIsReturned() - { - // Arrange - var handlers = new List> - { - Mock.Of>( - mock => mock.HandleAsync(It.IsAny()) == Task.FromResult(OpenIddictServerEventState.Unhandled)), - Mock.Of>( - mock => mock.HandleAsync(It.IsAny()) == Task.FromResult(OpenIddictServerEventState.Unhandled)), - Mock.Of>( - mock => mock.HandleAsync(It.IsAny()) == Task.FromResult(OpenIddictServerEventState.Handled)), - Mock.Of>() - }; - - var provider = new Mock(); - provider.Setup(mock => mock.GetService(typeof(IEnumerable>))) - .Returns(handlers); - - var dispatcher = new OpenIddictServerEventDispatcher(provider.Object); - - var notification = new Event(); - - // Act - await dispatcher.DispatchAsync(notification); - - // Assert - Mock.Get(handlers[0]).Verify(mock => mock.HandleAsync(notification), Times.Once()); - Mock.Get(handlers[1]).Verify(mock => mock.HandleAsync(notification), Times.Once()); - Mock.Get(handlers[2]).Verify(mock => mock.HandleAsync(notification), Times.Once()); - Mock.Get(handlers[3]).Verify(mock => mock.HandleAsync(notification), Times.Never()); - } - - public class Event : IOpenIddictServerEvent { } - } -} diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerEventHandlerTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerEventHandlerTests.cs deleted file mode 100644 index eb24c800b..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerEventHandlerTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Threading.Tasks; -using Xunit; - -namespace OpenIddict.Server.Tests -{ - public class OpenIddictServerEventHandlerTests - { - [Fact] - public void Constructor_ThrowsAnExceptionForNullHandler() - { - // Arrange, act and assert - var exception = Assert.Throws(() - => new OpenIddictServerEventHandler(handler: null)); - - Assert.Equal("handler", exception.ParamName); - } - - [Fact] - public async Task HandleAsync_ThrowsAnExceptionForNullNotification() - { - // Arrange - var handler = new OpenIddictServerEventHandler( - notification => Task.FromResult(OpenIddictServerEventState.Handled)); - - // Act and assert - var exception = await Assert.ThrowsAsync(() - => handler.HandleAsync(notification: null)); - - Assert.Equal("notification", exception.ParamName); - } - - [Fact] - public async Task HandleAsync_InvokesInlineHandler() - { - // Arrange - var marker = false; - var handler = new OpenIddictServerEventHandler( - notification => - { - marker = true; - return Task.FromResult(OpenIddictServerEventState.Handled); - }); - - // Act - await handler.HandleAsync(new Event()); - - // Assert - Assert.True(marker); - } - - public class Event : IOpenIddictServerEvent { } - } -} diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs deleted file mode 100644 index ba8788939..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Text; -using AspNet.Security.OpenIdConnect.Server; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Xunit; - -namespace OpenIddict.Server.Tests -{ - public class OpenIddictServerExtensionsTests - { - [Fact] - public void AddServer_ThrowsAnExceptionForNullBuilder() - { - // Arrange - var builder = (OpenIddictBuilder) null; - - // Act and assert - var exception = Assert.Throws(() => builder.AddServer()); - - Assert.Equal("builder", exception.ParamName); - } - - [Fact] - public void AddServer_ThrowsAnExceptionForNullConfiguration() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act and assert - var exception = Assert.Throws(() => builder.AddServer(configuration: null)); - - Assert.Equal("configuration", exception.ParamName); - } - - [Fact] - public void AddServer_RegistersAuthenticationServices() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddServer(); - - // Assert - Assert.Contains(services, service => service.ServiceType == typeof(IAuthenticationService)); - } - - [Fact] - public void AddServer_RegistersCachingServices() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddServer(); - - // Assert - Assert.Contains(services, service => service.ServiceType == typeof(IDistributedCache)); - Assert.Contains(services, service => service.ServiceType == typeof(IMemoryCache)); - } - - [Fact] - public void AddServer_RegistersLoggingServices() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddServer(); - - // Assert - Assert.Contains(services, service => service.ServiceType == typeof(ILogger<>)); - } - - [Fact] - public void AddServer_RegistersOptionsServices() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddServer(); - - // Assert - Assert.Contains(services, service => service.ServiceType == typeof(IOptions<>)); - } - - [Fact] - public void AddServer_RegistersEventService() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddServer(); - - // Assert - Assert.Contains(services, service => service.Lifetime == ServiceLifetime.Scoped && - service.ServiceType == typeof(IOpenIddictServerEventDispatcher) && - service.ImplementationType == typeof(OpenIddictServerEventDispatcher)); - } - - [Fact] - public void AddServer_RegistersHandler() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddServer(); - - // Assert - Assert.Contains(services, service => service.Lifetime == ServiceLifetime.Scoped && - service.ServiceType == typeof(OpenIddictServerHandler) && - service.ImplementationType == typeof(OpenIddictServerHandler)); - } - - [Fact] - public void AddServer_RegistersProvider() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddServer(); - - // Assert - Assert.Contains(services, service => service.Lifetime == ServiceLifetime.Scoped && - service.ServiceType == typeof(OpenIddictServerProvider) && - service.ImplementationFactory != null); - } - - [Fact] - public void AddServer_ResolvingProviderThrowsAnExceptionWhenCoreServicesAreNotRegistered() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddServer(); - - // Assert - var provider = services.BuildServiceProvider(); - - var exception = Assert.Throws(() => provider.GetRequiredService()); - - Assert.Equal(new StringBuilder() - .AppendLine("The core services must be registered when enabling the OpenIddict server handler.") - .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") - .Append("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") - .ToString(), exception.Message); - } - - [Theory] - [InlineData(typeof(IPostConfigureOptions), typeof(OpenIddictServerConfiguration))] - [InlineData(typeof(IPostConfigureOptions), typeof(OpenIdConnectServerInitializer))] - public void AddServer_RegistersConfiguration(Type serviceType, Type implementationType) - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddServer(); - - // Assert - Assert.Contains(services, service => service.ServiceType == serviceType && - service.ImplementationType == implementationType); - } - - [Fact] - public void AddServer_RegistersAuthenticationScheme() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddServer(); - - // Assert - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>().Value; - - Assert.Contains(options.Schemes, scheme => scheme.Name == OpenIddictServerDefaults.AuthenticationScheme && - scheme.HandlerType == typeof(OpenIddictServerHandler)); - } - - [Fact] - public void AddServer_ThrowsAnExceptionWhenSchemeIsAlreadyRegisteredWithDifferentHandlerType() - { - // Arrange - var services = new ServiceCollection(); - services.AddAuthentication() - .AddOpenIdConnectServer(); - - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddServer(); - - // Assert - var provider = services.BuildServiceProvider(); - var exception = Assert.Throws(delegate - { - return provider.GetRequiredService>().Value; - }); - - Assert.Equal(new StringBuilder() - .AppendLine("The OpenIddict server handler cannot be registered as an authentication scheme.") - .AppendLine("This may indicate that an instance of the OpenID Connect server was registered.") - .Append("Make sure that 'services.AddAuthentication().AddOpenIdConnectServer()' is not used.") - .ToString(), exception.Message); - } - - [Fact] - public void AddServer_CanBeSafelyInvokedMultipleTimes() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act and assert - builder.AddServer(); - builder.AddServer(); - builder.AddServer(); - } - } -} diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Authentication.cs b/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Authentication.cs deleted file mode 100644 index 7c685c1b9..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Authentication.cs +++ /dev/null @@ -1,1060 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Immutable; -using System.IO; -using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Client; -using AspNet.Security.OpenIdConnect.Primitives; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Bson; -using OpenIddict.Abstractions; -using Xunit; - -namespace OpenIddict.Server.Tests -{ - public partial class OpenIddictServerProviderTests - { - [Fact] - public async Task ExtractAuthorizationRequest_UnsupportedRequestParameterIsRejected() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - Request = "eyJhbGciOiJub25lIn0.eyJpc3MiOiJodHRwOi8vd3d3LmZhYnJpa2FtLmNvbSIsImF1ZCI6Imh0" + - "dHA6Ly93d3cuY29udG9zby5jb20iLCJyZXNwb25zZV90eXBlIjoiY29kZSIsImNsaWVudF9pZCI6" + - "IkZhYnJpa2FtIiwicmVkaXJlY3RfdXJpIjoiaHR0cDovL3d3dy5mYWJyaWthbS5jb20vcGF0aCJ9.", - ResponseType = OpenIddictConstants.ResponseTypes.Code, - Scope = OpenIddictConstants.Scopes.OpenId - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.RequestNotSupported, response.Error); - Assert.Equal("The 'request' parameter is not supported.", response.ErrorDescription); - } - - [Fact] - public async Task ExtractAuthorizationRequest_UnsupportedRequestUriParameterIsRejected() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - RequestUri = "http://www.fabrikam.com/request/GkurKxf5T0Y-mnPFCHqWOMiZi4VS138cQO_V7PZHAdM", - ResponseType = OpenIddictConstants.ResponseTypes.Code, - Scope = OpenIddictConstants.Scopes.OpenId - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.RequestUriNotSupported, response.Error); - Assert.Equal("The 'request_uri' parameter is not supported.", response.ErrorDescription); - } - - [Fact] - public async Task ExtractAuthorizationRequest_RequestIdParameterIsRejectedWhenRequestCachingIsDisabled() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The 'request_id' parameter is not supported.", response.ErrorDescription); - } - - [Fact] - public async Task ExtractAuthorizationRequest_InvalidRequestIdParameterIsRejected() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddDistributedMemoryCache(); - - builder.EnableRequestCaching(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The specified 'request_id' parameter is invalid.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateAuthorizationRequest_NoneFlowIsRejected() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.None - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnsupportedResponseType, response.Error); - Assert.Equal("The specified 'response_type' parameter is not supported.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateAuthorizationRequest_UnknownResponseTypeParameterIsRejected() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = "unknown_response_type" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnsupportedResponseType, response.Error); - Assert.Equal("The specified 'response_type' parameter is not supported.", response.ErrorDescription); - } - - [Theory] - [InlineData(OpenIddictConstants.GrantTypes.AuthorizationCode, "code")] - [InlineData(OpenIddictConstants.GrantTypes.AuthorizationCode, "code id_token")] - [InlineData(OpenIddictConstants.GrantTypes.AuthorizationCode, "code id_token token")] - [InlineData(OpenIddictConstants.GrantTypes.AuthorizationCode, "code token")] - [InlineData(OpenIddictConstants.GrantTypes.Implicit, "code id_token")] - [InlineData(OpenIddictConstants.GrantTypes.Implicit, "code id_token token")] - [InlineData(OpenIddictConstants.GrantTypes.Implicit, "code token")] - [InlineData(OpenIddictConstants.GrantTypes.Implicit, "id_token")] - [InlineData(OpenIddictConstants.GrantTypes.Implicit, "id_token token")] - [InlineData(OpenIddictConstants.GrantTypes.Implicit, "token")] - public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenCorrespondingFlowIsDisabled(string flow, string type) - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.GrantTypes.Remove(flow)); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Nonce = "n-0S6_WzA2Mj", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = type, - Scope = OpenIddictConstants.Scopes.OpenId - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnsupportedResponseType, response.Error); - Assert.Equal("The specified 'response_type' parameter is not allowed.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenUnregisteredScopeIsSpecified() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateScopeManager(instance => - { - instance.Setup(mock => mock.FindByNamesAsync( - It.Is>(scopes => scopes.Length == 1 && scopes[0] == "unregistered_scope"), - It.IsAny())) - .ReturnsAsync(ImmutableArray.Create()); - })); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code, - Scope = "unregistered_scope" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidScope, response.Error); - Assert.Equal("The specified 'scope' parameter is not valid.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateAuthorizationRequest_RequestIsValidatedWhenScopeRegisteredInOptionsIsSpecified() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.RegisterScopes("registered_scope"); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Nonce = "n-0S6_WzA2Mj", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Token, - Scope = "registered_scope" - }); - - // Assert - Assert.Null(response.Error); - Assert.Null(response.ErrorDescription); - Assert.Null(response.ErrorUri); - Assert.NotNull(response.AccessToken); - } - - [Fact] - public async Task ValidateAuthorizationRequest_RequestIsValidatedWhenRegisteredScopeIsSpecified() - { - // Arrange - var scope = new OpenIddictScope(); - - var manager = CreateScopeManager(instance => - { - instance.Setup(mock => mock.FindByNamesAsync( - It.Is>(scopes => scopes.Length == 1 && scopes[0] == "scope_registered_in_database"), - It.IsAny())) - .ReturnsAsync(ImmutableArray.Create(scope)); - - instance.Setup(mock => mock.GetNameAsync(scope, It.IsAny())) - .Returns(new ValueTask("scope_registered_in_database")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.RegisterScopes("scope_registered_in_options"); - - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Nonce = "n-0S6_WzA2Mj", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Token, - Scope = "scope_registered_in_database scope_registered_in_options" - }); - - // Assert - Assert.Null(response.Error); - Assert.Null(response.ErrorDescription); - Assert.Null(response.ErrorUri); - Assert.NotNull(response.AccessToken); - } - - [Fact] - public async Task ValidateAuthorizationRequest_RequestWithOfflineAccessScopeIsRejectedWhenRefreshTokenFlowIsDisabled() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.GrantTypes.Remove(OpenIddictConstants.GrantTypes.RefreshToken)); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code, - Scope = OpenIddictConstants.Scopes.OfflineAccess - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The 'offline_access' scope is not allowed.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateAuthorizationRequest_UnknownResponseModeParameterIsRejected() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseMode = "unknown_response_mode", - ResponseType = OpenIddictConstants.ResponseTypes.Code - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The specified 'response_mode' parameter is not supported.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenRedirectUriIsMissing() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = null, - ResponseType = OpenIddictConstants.ResponseTypes.Code - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The mandatory 'redirect_uri' parameter is missing.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenPkceIsRequiredAndCodeChallengeIsMissing() - { - // Arrange - var server = CreateAuthorizationServer(builder => builder.RequireProofKeyForCodeExchange()); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - CodeChallenge = null, - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The mandatory 'code_challenge' parameter is missing.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenCodeChallengeMethodIsMissing() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - CodeChallenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", - CodeChallengeMethod = null, - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The 'code_challenge_method' parameter must be specified.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenCodeChallengeMethodIsPlain() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - CodeChallenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", - CodeChallengeMethod = OpenIdConnectConstants.CodeChallengeMethods.Plain, - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The specified 'code_challenge_method' parameter is not allowed.", response.ErrorDescription); - } - - [Theory] - [InlineData("code id_token token")] - [InlineData("code token")] - public async Task ValidateAuthorizationRequest_CodeChallengeRequestWithForbiddenResponseTypeIsRejected(string type) - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - CodeChallenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", - CodeChallengeMethod = OpenIdConnectConstants.CodeChallengeMethods.Sha256, - Nonce = "n-0S6_WzA2Mj", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = type, - Scope = OpenIddictConstants.Scopes.OpenId - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The specified 'response_type' parameter is not allowed when using PKCE.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenClientCannotBeFound() - { - // Arrange - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(value: null); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The specified 'client_id' parameter is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - } - - [Theory] - [InlineData("code id_token token")] - [InlineData("code token")] - [InlineData("id_token token")] - [InlineData("token")] - public async Task ValidateAuthorizationRequest_AnAccessTokenCannotBeReturnedWhenClientIsConfidential(string type) - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Nonce = "n-0S6_WzA2Mj", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = type, - Scope = OpenIddictConstants.Scopes.OpenId - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnauthorizedClient, response.Error); - Assert.Equal("The specified 'response_type' parameter is not valid for this client application.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.IgnoreEndpointPermissions = false); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnauthorizedClient, response.Error); - Assert.Equal("This client application is not allowed to use the authorization endpoint.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny()), Times.Once()); - } - - [Theory] - [InlineData( - "code", - new[] { OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode }, - "The client application is not allowed to use the authorization code flow.")] - [InlineData( - "code id_token", - new[] { OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, OpenIddictConstants.Permissions.GrantTypes.Implicit }, - "The client application is not allowed to use the hybrid flow.")] - [InlineData( - "code id_token token", - new[] { OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, OpenIddictConstants.Permissions.GrantTypes.Implicit }, - "The client application is not allowed to use the hybrid flow.")] - [InlineData( - "code token", - new[] { OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, OpenIddictConstants.Permissions.GrantTypes.Implicit }, - "The client application is not allowed to use the hybrid flow.")] - [InlineData( - "id_token", - new[] { OpenIddictConstants.Permissions.GrantTypes.Implicit }, - "The client application is not allowed to use the implicit flow.")] - [InlineData( - "id_token token", - new[] { OpenIddictConstants.Permissions.GrantTypes.Implicit }, - "The client application is not allowed to use the implicit flow.")] - [InlineData( - "token", - new[] { OpenIddictConstants.Permissions.GrantTypes.Implicit }, - "The client application is not allowed to use the implicit flow.")] - public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenGrantTypePermissionIsNotGranted( - string type, string[] permissions, string description) - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - foreach (var permission in permissions) - { - instance.Setup(mock => mock.HasPermissionAsync(application, permission, It.IsAny())) - .ReturnsAsync(false); - } - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.IgnoreGrantTypePermissions = false); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Nonce = "n-0S6_WzA2Mj", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = type, - Scope = OpenIddictConstants.Scopes.OpenId - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnauthorizedClient, response.Error); - Assert.Equal(description, response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, permissions[0], It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateAuthorizationRequest_RequestWithOfflineAccessScopeIsRejectedWhenRefreshTokenPermissionIsNotGranted() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.IgnoreGrantTypePermissions = false); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code, - Scope = OpenIddictConstants.Scopes.OfflineAccess - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The client application is not allowed to use the 'offline_access' scope.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenRedirectUriIsInvalid() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The specified 'redirect_uri' parameter is not valid for this client application.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenScopePermissionIsNotGranted() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + - OpenIddictConstants.Scopes.Profile, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + - OpenIddictConstants.Scopes.Email, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - builder.RegisterScopes(OpenIddictConstants.Scopes.Email, OpenIddictConstants.Scopes.Profile); - builder.Configure(options => options.IgnoreScopePermissions = false); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code, - Scope = "openid offline_access profile email" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("This client application is not allowed to use the specified scope.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + - OpenIddictConstants.Scopes.OpenId, It.IsAny()), Times.Never()); - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + - OpenIddictConstants.Scopes.OfflineAccess, It.IsAny()), Times.Never()); - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + - OpenIddictConstants.Scopes.Profile, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + - OpenIddictConstants.Scopes.Email, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleAuthorizationRequest_RequestIsPersistedInDistributedCache() - { - // Arrange - var cache = new Mock(); - var generator = new Mock(); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(cache.Object); - - builder.EnableRequestCaching(); - - builder.SetRequestCachingPolicy(new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(42), - SlidingExpiration = TimeSpan.FromSeconds(42) - }); - - builder.Configure(options => options.RandomNumberGenerator = generator.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Token - }); - - var identifier = (string) response[OpenIddictConstants.Parameters.RequestId]; - - // Assert - Assert.Single(response.GetParameters()); - Assert.NotNull(identifier); - - cache.Verify(mock => mock.SetAsync( - OpenIddictConstants.Environment.AuthorizationRequest + identifier, - It.IsAny(), - It.Is(options => - options.AbsoluteExpirationRelativeToNow == TimeSpan.FromDays(42) && - options.SlidingExpiration == TimeSpan.FromSeconds(42)), - It.IsAny()), Times.Once()); - - generator.Verify(mock => mock.GetBytes(It.Is(bytes => bytes.Length == 256 / 8)), Times.Once()); - } - - [Theory] - [InlineData("code")] - [InlineData("code id_token")] - [InlineData("code id_token token")] - [InlineData("code token")] - [InlineData("id_token")] - [InlineData("id_token token")] - [InlineData("token")] - public async Task HandleAuthorizationRequest_RequestsAreNotHandledLocally(string type) - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - - instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - var token = new OpenIddictToken(); - - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - })); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Nonce = "n-0S6_WzA2Mj", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = type, - Scope = OpenIddictConstants.Scopes.OpenId - }); - - // Assert - Assert.True(!string.IsNullOrEmpty(response.AccessToken) || - !string.IsNullOrEmpty(response.Code) || - !string.IsNullOrEmpty(response.IdToken)); - } - - [Fact] - public async Task ApplyAuthorizationResponse_RequestIsRemovedFromDistributedCache() - { - // Arrange - var request = new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Token - }; - - var stream = new MemoryStream(); - using (var writer = new BsonDataWriter(stream)) - { - writer.CloseOutput = false; - - var serializer = JsonSerializer.CreateDefault(); - serializer.Serialize(writer, request); - } - - var cache = new Mock(); - - cache.Setup(mock => mock.GetAsync( - OpenIddictConstants.Environment.AuthorizationRequest + "b2ee7815-5579-4ff7-86b0-ba671b939d96", - It.IsAny())) - .ReturnsAsync(stream.ToArray()); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(cache.Object); - - builder.EnableRequestCaching(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - RequestId = "b2ee7815-5579-4ff7-86b0-ba671b939d96" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - cache.Verify(mock => mock.RemoveAsync( - OpenIddictConstants.Environment.AuthorizationRequest + "b2ee7815-5579-4ff7-86b0-ba671b939d96", - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ApplyAuthorizationResponse_SupportsNullRequests() - { - // Note: when an invalid HTTP verb is used, the OpenID Connect server handler refuses to extract the request - // and immediately returns an error. In this specific case, ApplyAuthorizationResponseContext.Request is null - // and this test ensures ApplyAuthorizationResponse can safely handle cases where the request is unavailable. - - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.EnableRequestCaching(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.SendAsync(HttpMethods.Put, AuthorizationEndpoint, new OpenIdConnectRequest()); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The specified HTTP method is not valid.", response.ErrorDescription); - } - - [Fact] - public async Task ApplyAuthorizationResponse_ErroredRequestIsNotHandledLocallyWhenStatusCodeMiddlewareIsEnabled() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.EnableAuthorizationEndpoint("/authorize-status-code-middleware"); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync("/authorize-status-code-middleware", new OpenIdConnectRequest - { - ClientId = null, - RedirectUri = null, - ResponseType = null - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, (string) response["error_custom"]); - } - } -} diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Discovery.cs b/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Discovery.cs deleted file mode 100644 index d25ebc4a0..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Discovery.cs +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Linq; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Client; -using AspNet.Security.OpenIdConnect.Primitives; -using Newtonsoft.Json.Linq; -using OpenIddict.Abstractions; -using Xunit; - -namespace OpenIddict.Server.Tests -{ - public partial class OpenIddictServerProviderTests - { - [Fact] - public async Task HandleConfigurationRequest_PlainCodeChallengeMethodIsNotReturned() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.GetAsync(ConfigurationEndpoint); - - // Assert - Assert.DoesNotContain( - OpenIdConnectConstants.CodeChallengeMethods.Plain, - ((JArray) response[OpenIdConnectConstants.Metadata.CodeChallengeMethodsSupported]).Values()); - } - - [Theory] - [InlineData(OpenIddictConstants.GrantTypes.AuthorizationCode)] - [InlineData(OpenIddictConstants.GrantTypes.ClientCredentials)] - [InlineData(OpenIddictConstants.GrantTypes.Implicit)] - [InlineData(OpenIddictConstants.GrantTypes.Password)] - [InlineData(OpenIddictConstants.GrantTypes.RefreshToken)] - public async Task HandleConfigurationRequest_EnabledFlowsAreReturned(string flow) - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => - { - options.GrantTypes.Clear(); - options.GrantTypes.Add(flow); - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.GetAsync(ConfigurationEndpoint); - var types = ((JArray) response[OpenIdConnectConstants.Metadata.GrantTypesSupported]).Values(); - - // Assert - Assert.Single(types); - Assert.Contains(flow, types); - } - - [Fact] - public async Task HandleConfigurationRequest_NoSupportedScopesPropertyIsReturnedWhenNoScopeIsConfigured() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => - { - options.GrantTypes.Remove(OpenIddictConstants.GrantTypes.RefreshToken); - options.Scopes.Clear(); - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.GetAsync(ConfigurationEndpoint); - - // Assert - Assert.False(response.HasParameter(OpenIdConnectConstants.Metadata.ScopesSupported)); - } - - [Theory] - [InlineData(OpenIddictConstants.Scopes.OpenId)] - public async Task HandleConfigurationRequest_DefaultScopesAreReturned(string scope) - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.GetAsync(ConfigurationEndpoint); - - // Assert - Assert.Contains(scope, ((JArray) response[OpenIdConnectConstants.Metadata.ScopesSupported]).Values()); - } - - [Fact] - public async Task HandleConfigurationRequest_CustomScopeIsReturned() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => - { - options.Scopes.Clear(); - options.Scopes.Add("custom_scope"); - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.GetAsync(ConfigurationEndpoint); - - // Assert - Assert.Contains("custom_scope", ((JArray) response[OpenIdConnectConstants.Metadata.ScopesSupported]).Values()); - } - - [Fact] - public async Task HandleConfigurationRequest_OfflineAccessScopeIsReturnedWhenRefreshTokenFlowIsEnabled() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.GetAsync(ConfigurationEndpoint); - - // Assert - Assert.Contains(OpenIddictConstants.Scopes.OfflineAccess, - ((JArray) response[OpenIdConnectConstants.Metadata.ScopesSupported]).Values()); - } - - [Fact] - public async Task HandleConfigurationRequest_OfflineAccessScopeIsReturnedWhenRefreshTokenFlowIsDisabled() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => - { - // Note: at least one flow must be enabled. - options.GrantTypes.Clear(); - options.GrantTypes.Add(OpenIddictConstants.GrantTypes.AuthorizationCode); - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.GetAsync(ConfigurationEndpoint); - - // Assert - Assert.DoesNotContain(OpenIddictConstants.Scopes.OfflineAccess, - ((JArray) response[OpenIdConnectConstants.Metadata.ScopesSupported]).Values()); - } - - [Fact] - public async Task HandleConfigurationRequest_NoSupportedClaimsPropertyIsReturnedWhenNoClaimIsConfigured() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.Claims.Clear()); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.GetAsync(ConfigurationEndpoint); - - // Assert - Assert.False(response.HasParameter(OpenIdConnectConstants.Metadata.ClaimsSupported)); - } - - [Fact] - public async Task HandleConfigurationRequest_DefaultClaimsAreReturned() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.GetAsync(ConfigurationEndpoint); - var claims = ((JArray) response[OpenIdConnectConstants.Metadata.ClaimsSupported]).Values().ToArray(); - - // Assert - Assert.Equal(6, claims.Length); - Assert.Contains(OpenIddictConstants.Claims.Audience, claims); - Assert.Contains(OpenIddictConstants.Claims.ExpiresAt, claims); - Assert.Contains(OpenIddictConstants.Claims.IssuedAt, claims); - Assert.Contains(OpenIddictConstants.Claims.Issuer, claims); - Assert.Contains(OpenIddictConstants.Claims.JwtId, claims); - Assert.Contains(OpenIddictConstants.Claims.Subject, claims); - } - - [Fact] - public async Task HandleConfigurationRequest_ConfiguredClaimsAreReturned() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.Claims.Add("custom_claim")); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.GetAsync(ConfigurationEndpoint); - - // Assert - Assert.Contains("custom_claim", ((JArray) response[OpenIdConnectConstants.Metadata.ClaimsSupported]).Values()); - } - - [Fact] - public async Task HandleConfigurationRequest_DefaultParametersAreReturned() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.GetAsync(ConfigurationEndpoint); - - // Assert - Assert.False((bool) response[OpenIdConnectConstants.Metadata.ClaimsParameterSupported]); - Assert.False((bool) response[OpenIdConnectConstants.Metadata.RequestParameterSupported]); - Assert.False((bool) response[OpenIdConnectConstants.Metadata.RequestUriParameterSupported]); - } - } -} diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Exchange.cs b/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Exchange.cs deleted file mode 100644 index 18a742d31..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Exchange.cs +++ /dev/null @@ -1,2174 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Collections.Immutable; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Client; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Primitives; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using OpenIddict.Abstractions; -using Xunit; - -namespace OpenIddict.Server.Tests -{ - public partial class OpenIddictServerProviderTests - { - [Theory] - [InlineData(OpenIddictConstants.GrantTypes.AuthorizationCode)] - [InlineData(OpenIddictConstants.GrantTypes.ClientCredentials)] - [InlineData(OpenIddictConstants.GrantTypes.Password)] - [InlineData(OpenIddictConstants.GrantTypes.RefreshToken)] - public async Task ValidateTokenRequest_RequestIsRejectedWhenFlowIsNotEnabled(string flow) - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.GrantTypes.Remove(flow)); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = flow, - Username = "johndoe", - Password = "A3ddj3w", - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnsupportedGrantType, response.Error); - Assert.Equal("The specified 'grant_type' parameter is not supported.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateTokenRequest_RequestWithOfflineAccessScopeIsRejectedWhenRefreshTokenFlowIsDisabled() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.GrantTypes.Remove(OpenIddictConstants.GrantTypes.RefreshToken)); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The 'offline_access' scope is not allowed.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateTokenRequest_AuthorizationCodeRequestIsRejectedWhenRedirectUriIsMissing() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = null - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The mandatory 'redirect_uri' parameter is missing.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateTokenRequest_AuthorizationCodeRequestIsRejectedWhenCodeVerifierIsMissing() - { - // Arrange - var server = CreateAuthorizationServer(builder => builder.RequireProofKeyForCodeExchange()); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - CodeVerifier = null, - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The mandatory 'code_verifier' parameter is missing.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateTokenRequest_RequestIsRejectedWhenUnregisteredScopeIsSpecified() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateScopeManager(instance => - { - instance.Setup(mock => mock.FindByNamesAsync( - It.Is>(scopes => scopes.Length == 1 && scopes[0] == "unregistered_scope"), - It.IsAny())) - .ReturnsAsync(ImmutableArray.Create()); - })); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = "unregistered_scope" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidScope, response.Error); - Assert.Equal("The specified 'scope' parameter is not valid.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateTokenRequest_RequestIsValidatedWhenScopeRegisteredInOptionsIsSpecified() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.RegisterScopes("registered_scope"); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = "registered_scope" - }); - - // Assert - Assert.Null(response.Error); - Assert.Null(response.ErrorDescription); - Assert.Null(response.ErrorUri); - Assert.NotNull(response.AccessToken); - } - - [Fact] - public async Task ValidateTokenRequest_RequestIsValidatedWhenRegisteredScopeIsSpecified() - { - // Arrange - var scope = new OpenIddictScope(); - - var manager = CreateScopeManager(instance => - { - instance.Setup(mock => mock.FindByNamesAsync( - It.Is>(scopes => scopes.Length == 1 && scopes[0] == "scope_registered_in_database"), - It.IsAny())) - .ReturnsAsync(ImmutableArray.Create(scope)); - - instance.Setup(mock => mock.GetNameAsync(scope, It.IsAny())) - .Returns(new ValueTask("scope_registered_in_database")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.RegisterScopes("scope_registered_in_options"); - - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = "scope_registered_in_database scope_registered_in_options" - }); - - // Assert - Assert.Null(response.Error); - Assert.Null(response.ErrorDescription); - Assert.Null(response.ErrorUri); - Assert.NotNull(response.AccessToken); - } - - [Fact] - public async Task ValidateTokenRequest_ClientCredentialsRequestWithOfflineAccessScopeIsRejected() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.ClientCredentials, - Scope = OpenIddictConstants.Scopes.OfflineAccess - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The 'offline_access' scope is not valid for the specified 'grant_type' parameter.", response.ErrorDescription); - } - - [Theory] - [InlineData("client_id", "")] - [InlineData("", "client_secret")] - public async Task ValidateTokenRequest_ClientCredentialsRequestIsRejectedWhenCredentialsAreMissing(string identifier, string secret) - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = identifier, - ClientSecret = secret, - GrantType = OpenIddictConstants.GrantTypes.ClientCredentials - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The 'client_id' and 'client_secret' parameters are " + - "required when using the client credentials grant.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateTokenRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.AcceptAnonymousClients = false); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = null, - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateTokenRequest_RequestIsRejectedWhenClientCannotBeFound() - { - // Arrange - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(value: null); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidClient, response.Error); - Assert.Equal("The specified 'client_id' parameter is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateTokenRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.IgnoreEndpointPermissions = false); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnauthorizedClient, response.Error); - Assert.Equal("This client application is not allowed to use the token endpoint.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateTokenRequest_RequestIsRejectedWhenGrantTypePermissionIsNotGranted() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.IgnoreGrantTypePermissions = false); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnauthorizedClient, response.Error); - Assert.Equal("This client application is not allowed to use the specified grant type.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateTokenRequest_RequestWithOfflineAccessScopeIsRejectedWhenRefreshTokenPermissionIsNotGranted() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.IgnoreGrantTypePermissions = false); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The client application is not allowed to use the 'offline_access' scope.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateTokenRequest_ClientCredentialsRequestFromPublicClientIsRejected() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - GrantType = OpenIddictConstants.GrantTypes.ClientCredentials - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnauthorizedClient, response.Error); - Assert.Equal("The specified 'grant_type' parameter is not valid for this client application.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateTokenRequest_RequestIsRejectedWhenScopePermissionIsNotGranted() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + - OpenIddictConstants.Scopes.Profile, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + - OpenIddictConstants.Scopes.Email, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - builder.RegisterScopes(OpenIddictConstants.Scopes.Email, OpenIddictConstants.Scopes.Profile); - builder.Configure(options => options.IgnoreScopePermissions = false); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = "openid offline_access profile email" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("This client application is not allowed to use the specified scope.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + - OpenIddictConstants.Scopes.OpenId, It.IsAny()), Times.Never()); - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + - OpenIddictConstants.Scopes.OfflineAccess, It.IsAny()), Times.Never()); - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + - OpenIddictConstants.Scopes.Profile, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + - OpenIddictConstants.Scopes.Email, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateTokenRequest_ClientSecretCannotBeUsedByPublicClients() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The 'client_secret' parameter is not valid for this client application.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateTokenRequest_ClientSecretIsRequiredForConfidentialClients() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = null, - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidClient, response.Error); - Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateTokenRequest_ClientSecretIsRequiredForHybridClients() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Hybrid)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = null, - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidClient, response.Error); - Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateTokenRequest_RequestIsRejectedWhenClientCredentialsAreInvalid() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidClient, response.Error); - Assert.Equal("The specified client credentials are invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_AuthorizationCodeRevocationIsIgnoredWhenTokenStorageIsDisabled() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); - - builder.DisableTokenStorage(); - builder.DisableSlidingExpiration(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.NotNull(response.AccessToken); - } - - [Fact] - public async Task HandleTokenRequest_RefreshTokenRevocationIsIgnoredWhenTokenStorageIsDisabled() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); - - builder.DisableTokenStorage(); - builder.DisableSlidingExpiration(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.NotNull(response.AccessToken); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsUnknown() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(value: null); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsUnknown() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(value: null); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsAlreadyRedeemed() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsAlreadyRedeemed() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103951EFBA23A56")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RevokesAuthorizationWhenAuthorizationCodeIsAlreadyRedeemed() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - ticket.SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - var token = new OpenIddictToken(); - - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(ImmutableArray.Create()); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(authorization, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RevokesAuthorizationWhenRefreshTokenIsAlreadyRedeemed() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - var token = new OpenIddictToken(); - - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(ImmutableArray.Create()); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(authorization, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RevokesTokensWhenAuthorizationCodeIsAlreadyRedeemed() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - ticket.SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var tokens = ImmutableArray.Create( - new OpenIddictToken(), - new OpenIddictToken(), - new OpenIddictToken()); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(tokens[0]); - - instance.Setup(mock => mock.GetIdAsync(tokens[0], It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetIdAsync(tokens[1], It.IsAny())) - .Returns(new ValueTask("481FCAC6-06BC-43EE-92DB-37A78AA09B595073CC313103")); - - instance.Setup(mock => mock.GetIdAsync(tokens[2], It.IsAny())) - .Returns(new ValueTask("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8")); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(tokens[0], It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - - instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(tokens); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(CreateAuthorizationManager(instance => - { - var authorization = new OpenIddictAuthorization(); - - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RevokesTokensWhenRefreshTokenIsAlreadyRedeemed() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var tokens = ImmutableArray.Create( - new OpenIddictToken(), - new OpenIddictToken(), - new OpenIddictToken()); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(tokens[0]); - - instance.Setup(mock => mock.GetIdAsync(tokens[0], It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetIdAsync(tokens[1], It.IsAny())) - .Returns(new ValueTask("481FCAC6-06BC-43EE-92DB-37A78AA09B595073CC313103")); - - instance.Setup(mock => mock.GetIdAsync(tokens[2], It.IsAny())) - .Returns(new ValueTask("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8")); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(tokens[0], It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - - instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(tokens); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(CreateAuthorizationManager(instance => - { - var authorization = new OpenIddictAuthorization(); - - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsInvalid() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsInvalid() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103951EFBA23A56")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_AuthorizationAssociatedWithCodeIsIgnoredWhenAuthorizationStorageIsDisabled() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - var token = new OpenIddictToken(); - - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - })); - - builder.Services.AddSingleton(manager); - - builder.DisableAuthorizationStorage(); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task HandleTokenRequest_AuthorizationAssociatedWithRefreshTokenIsIgnoredWhenAuthorizationStorageIsDisabled() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - var token = new OpenIddictToken(); - - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - })); - - builder.Services.AddSingleton(manager); - - builder.DisableAuthorizationStorage(); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithAuthorizationCodeCannotBeFound() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(value: null); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - var token = new OpenIddictToken(); - - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The authorization associated with the authorization code is no longer valid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithAuthorizationCodeIsInvalid() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - var token = new OpenIddictToken(); - - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The authorization associated with the authorization code is no longer valid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithRefreshTokenCannotBeFound() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(value: null); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - var token = new OpenIddictToken(); - - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103951EFBA23A56")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The authorization associated with the refresh token is no longer valid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithRefreshTokenIsInvalid() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - var token = new OpenIddictToken(); - - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103951EFBA23A56")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The authorization associated with the refresh token is no longer valid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny()), Times.Once()); - } - - [Theory] - [InlineData(OpenIddictConstants.GrantTypes.AuthorizationCode)] - [InlineData(OpenIddictConstants.GrantTypes.ClientCredentials)] - [InlineData(OpenIddictConstants.GrantTypes.Password)] - [InlineData(OpenIddictConstants.GrantTypes.RefreshToken)] - [InlineData("urn:ietf:params:oauth:grant-type:custom_grant")] - public async Task HandleTokenRequest_RequestsAreNotHandledLocally(string flow) - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - - switch (flow) - { - case OpenIddictConstants.GrantTypes.AuthorizationCode: - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - ticket.SetPresenters("Fabrikam"); - break; - - case OpenIddictConstants.GrantTypes.RefreshToken: - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - break; - } - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103951EFBA23A56")); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(CreateAuthorizationManager(instance => - { - var authorization = new OpenIddictAuthorization(); - - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) - .ReturnsAsync(true); - })); - - builder.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant"); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Code = "8xLOxBtZp8", - GrantType = flow, - RedirectUri = "http://www.fabrikam.com/path", - RefreshToken = "8xLOxBtZp8", - Username = "johndoe", - Password = "A3ddj3w" - }); - - // Assert - Assert.NotNull(response.AccessToken); - } - } -} diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Introspection.cs b/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Introspection.cs deleted file mode 100644 index 10936a9b3..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Introspection.cs +++ /dev/null @@ -1,789 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Client; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Primitives; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using OpenIddict.Abstractions; -using Xunit; - -namespace OpenIddict.Server.Tests -{ - public partial class OpenIddictServerProviderTests - { - [Theory] - [InlineData("client_id", "")] - [InlineData("", "client_secret")] - public async Task ValidateIntrospectionRequest_ClientCredentialsRequestIsRejectedWhenCredentialsAreMissing(string identifier, string secret) - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = identifier, - ClientSecret = secret, - Token = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The mandatory 'client_id' and/or 'client_secret' parameters are missing.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCannotBeFound() - { - // Arrange - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(value: null); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidClient, response.Error); - Assert.Equal("The specified 'client_id' parameter is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.IgnoreEndpointPermissions = false); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnauthorizedClient, response.Error); - Assert.Equal("This client application is not allowed to use the introspection endpoint.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateIntrospectionRequest_RequestsSentByPublicClientsAreRejected() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidClient, response.Error); - Assert.Equal("This client application is not allowed to use the introspection endpoint.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCredentialsAreInvalid() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidClient, response.Error); - Assert.Equal("The specified client credentials are invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); - } - - [Theory] - [InlineData(OpenIdConnectConstants.TokenUsages.AuthorizationCode)] - [InlineData(OpenIdConnectConstants.TokenUsages.IdToken)] - [InlineData(OpenIdConnectConstants.TokenUsages.RefreshToken)] - public async Task HandleIntrospectionRequest_RequestIsRejectedWhenTokenIsNotAnAccessToken(string type) - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(type); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Configure(options => options.AccessTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - } - - [Fact] - public async Task HandleIntrospectionRequest_RequestIsRejectedWhenAudienceIsMissing() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Configure(options => options.AccessTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - } - - [Fact] - public async Task HandleIntrospectionRequest_RequestIsRejectedWhenClientIsNotAValidAudience() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetAudiences("Contoso"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Configure(options => options.AccessTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - } - - [Fact] - public async Task HandleIntrospectionRequest_RequestIsRejectedWhenReferenceTokenIsUnknown() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny())) - .ReturnsAsync(value: null); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI" - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny()), Times.AtLeastOnce()); - } - - [Fact] - public async Task HandleIntrospectionRequest_AuthorizationIsIgnoredWhenAuthorizationStorageIsDisabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetAudiences("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - var token = new OpenIddictToken(); - - instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("2YotnFZFEjr1zCsicMWpAA")); - - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - })); - - builder.Services.AddSingleton(manager); - - builder.DisableAuthorizationStorage(); - builder.UseReferenceTokens(); - - builder.Configure(options => options.AccessTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI" - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task HandleIntrospectionRequest_RequestIsRejectedWhenAuthorizationCannotBeFound() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetAudiences("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(value: null); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - var token = new OpenIddictToken(); - - instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("2YotnFZFEjr1zCsicMWpAA")); - - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AccessTokenFormat = format.Object); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI" - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleIntrospectionRequest_RequestIsRejectedWhenAuthorizationIsInvalid() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetAudiences("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - var token = new OpenIddictToken(); - - instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("2YotnFZFEjr1zCsicMWpAA")); - - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AccessTokenFormat = format.Object); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI" - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleIntrospectionRequest_RequestIsRejectedWhenReferenceTokenIsInvalid() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetAudiences("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("2YotnFZFEjr1zCsicMWpAA")); - - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AccessTokenFormat = format.Object); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI" - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); - } - } -} diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Revocation.cs b/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Revocation.cs deleted file mode 100644 index 541604eb1..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Revocation.cs +++ /dev/null @@ -1,529 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Client; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Primitives; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.IdentityModel.Tokens; -using Moq; -using OpenIddict.Abstractions; -using Xunit; - -namespace OpenIddict.Server.Tests -{ - public partial class OpenIddictServerProviderTests - { - [Theory] - [InlineData(OpenIdConnectConstants.TokenTypeHints.AccessToken)] - [InlineData(OpenIdConnectConstants.TokenTypeHints.IdToken)] - public async Task ValidateRevocationRequest_UnsupportedTokenTypeHintIsRejected(string type) - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest - { - Token = "SlAV32hkKG", - TokenTypeHint = type - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnsupportedTokenType, response.Error); - Assert.Equal("The specified 'token_type_hint' parameter is not supported.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateRevocationRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.AcceptAnonymousClients = false); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest - { - Token = "SlAV32hkKG", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription); - } - - [Fact] - public async Task ValidateRevocationRequest_RequestIsRejectedWhenClientCannotBeFound() - { - // Arrange - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(value: null); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Token = "SlAV32hkKG", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidClient, response.Error); - Assert.Equal("The specified 'client_id' parameter is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateRevocationRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Endpoints.Revocation, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.IgnoreEndpointPermissions = false); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "SlAV32hkKG", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnauthorizedClient, response.Error); - Assert.Equal("This client application is not allowed to use the revocation endpoint.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Endpoints.Revocation, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateRevocationRequest_ClientSecretCannotBeUsedByPublicClients() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "SlAV32hkKG", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The 'client_secret' parameter is not valid for this client application.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateRevocationRequest_ClientSecretIsRequiredForConfidentialClients() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = null, - Token = "SlAV32hkKG", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidClient, response.Error); - Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateRevocationRequest_ClientSecretIsRequiredForHybridClients() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Hybrid)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = null, - Token = "SlAV32hkKG", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidClient, response.Error); - Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateRevocationRequest_RequestIsRejectedWhenClientCredentialsAreInvalid() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "SlAV32hkKG", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidClient, response.Error); - Assert.Equal("The specified client credentials are invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleRevocationRequest_RequestIsRejectedWhenTokenIsAnAccessTokenIfReferenceTokensAreDisabled() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SlAV32hkKG")) - .Returns(ticket); - - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.AccessTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest - { - Token = "SlAV32hkKG" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnsupportedTokenType, response.Error); - Assert.Equal("The specified token cannot be revoked.", response.ErrorDescription); - - format.Verify(mock => mock.Unprotect("SlAV32hkKG"), Times.Once()); - } - - [Fact] - public async Task HandleRevocationRequest_RequestIsRejectedWhenTokenIsAnIdentityToken() - { - // Arrange - var token = Mock.Of(mock => - mock.ValidFrom == DateTime.UtcNow.AddDays(-1) && - mock.ValidTo == DateTime.UtcNow.AddDays(1)); - - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.TokenUsage, OpenIdConnectConstants.TokenUsages.IdToken); - - var handler = new Mock(); - - handler.Setup(mock => mock.CanReadToken("SlAV32hkKG")) - .Returns(true); - - handler.As() - .Setup(mock => mock.ValidateToken("SlAV32hkKG", It.IsAny(), out token)) - .Returns(new ClaimsPrincipal(identity)); - - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.IdentityTokenHandler = handler.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest - { - Token = "SlAV32hkKG" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.UnsupportedTokenType, response.Error); - Assert.Equal("The specified token cannot be revoked.", response.ErrorDescription); - - handler.As() - .Verify(mock => mock.CanReadToken("SlAV32hkKG"), Times.Once()); - - handler.As() - .Verify(mock => mock.ValidateToken("SlAV32hkKG", It.IsAny(), out token), Times.Once()); - } - - [Fact] - public async Task HandleRevocationRequest_TokenIsNotRevokedWhenItIsUnknown() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SlAV32hkKG")) - .Returns(ticket); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(value: null); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest - { - Token = "SlAV32hkKG" - }); - - // Assert - Assert.Empty(response.GetParameters()); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(It.IsAny(), It.IsAny()), Times.Never()); - } - - [Fact] - public async Task HandleRevocationRequest_TokenIsNotRevokedWhenItIsAlreadyRevoked() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SlAV32hkKG")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRevokedAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest - { - Token = "SlAV32hkKG" - }); - - // Assert - Assert.Empty(response.GetParameters()); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(It.IsAny(), It.IsAny()), Times.Never()); - } - - [Fact] - public async Task HandleRevocationRequest_TokenIsSuccessfullyRevoked() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SlAV32hkKG")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest - { - Token = "SlAV32hkKG" - }); - - // Assert - Assert.Empty(response.GetParameters()); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(token, It.IsAny()), Times.Once()); - } - } -} diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Serialization.cs b/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Serialization.cs deleted file mode 100644 index 3b3735e84..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Serialization.cs +++ /dev/null @@ -1,2610 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Client; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Primitives; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using OpenIddict.Abstractions; -using Xunit; - -namespace OpenIddict.Server.Tests -{ - public partial class OpenIddictServerProviderTests - { - [Fact] - public async Task DeserializeAccessToken_ReturnsNullForMalformedReferenceToken() - { - // Arrange - var format = new Mock>(); - var manager = CreateTokenManager(); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - - builder.Configure(options => options.AccessTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "2YotnFZFEjr1zCsicMWpAA", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Never()); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task DeserializeAccessToken_AccessTokenIsNotRetrievedFromDatabaseWhenReferenceTokensAreDisabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetAudiences("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AccessTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "2YotnFZFEjr1zCsicMWpAA", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken - }); - - // Assert - Assert.True((bool) response[OpenIddictConstants.Claims.Active]); - - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync(It.IsAny(), It.IsAny()), Times.Never()); - } - - [Fact] - public async Task DeserializeAccessToken_ReturnsNullForMissingTokenType() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(result: null)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.AtLeastOnce()); - } - - [Fact] - public async Task DeserializeAccessToken_ReturnsNullForIncompatibleTokenType() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.RefreshToken)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.AtLeastOnce()); - } - - [Fact] - public async Task DeserializeAccessToken_ReturnsNullForMissingReferenceTokenIdentifier() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask(result: null)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); - } - - [Fact] - public async Task DeserializeAccessToken_ReturnsNullForMissingReferenceTokenCiphertext() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask(result: null)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); - } - - [Fact] - public async Task DeserializeAccessToken_ReturnsNullForInvalidReferenceTokenCiphertext() - { - // Arrange - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(value: null); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("2YotnFZFEjr1zCsicMWpAA")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - - builder.Configure(options => options.AccessTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken - }); - - // Assert - Assert.Single(response.GetParameters()); - Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); - } - - [Fact] - public async Task DeserializeAccessToken_ReturnsExpectedReferenceToken() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetAudiences("Fabrikam"); - ticket.SetTokenId("070AAEDE-38BF-41BE-870C-4E5A73E54566"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("2YotnFZFEjr1zCsicMWpAA")); - - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero))); - - instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero))); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - - options.AccessTokenFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", - TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken - }); - - // Assert - Assert.True((bool) response[OpenIddictConstants.Claims.Active]); - Assert.Equal("070AAEDE-38BF-41BE-870C-4E5A73E54566", response[OpenIddictConstants.Claims.JwtId]); - Assert.Equal(1483228800, (long) response[OpenIddictConstants.Claims.IssuedAt]); - Assert.Equal(1484006400, (long) response[OpenIddictConstants.Claims.ExpiresAt]); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); - } - - [Fact] - public async Task DeserializeAuthorizationCode_ReturnsNullForMalformedReferenceToken() - { - // Arrange - var format = new Mock>(); - var manager = CreateTokenManager(); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Code = "2YotnFZFEjr1zCsicMWpAA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is invalid.", response.ErrorDescription); - - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Never()); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task DeserializeAuthorizationCode_AuthorizationCodeIsNotRetrievedUsingHashWhenReferenceTokensAreDisabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Code = "2YotnFZFEjr1zCsicMWpAA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync(It.IsAny(), It.IsAny()), Times.Never()); - } - - [Fact] - public async Task DeserializeAuthorizationCode_ReturnsNullForMissingTokenType() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(result: null)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Code = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.AtLeastOnce()); - } - - [Fact] - public async Task DeserializeAuthorizationCode_ReturnsNullForIncompatibleTokenType() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Code = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.AtLeastOnce()); - } - - [Fact] - public async Task DeserializeAuthorizationCode_ReturnsNullForMissingReferenceTokenIdentifier() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AuthorizationCode)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask(result: null)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Code = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); - } - - [Fact] - public async Task DeserializeAuthorizationCode_ReturnsNullForMissingReferenceTokenCiphertext() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AuthorizationCode)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask(result: null)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Code = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); - } - - [Fact] - public async Task DeserializeAuthorizationCode_ReturnsNullForInvalidReferenceTokenCiphertext() - { - // Arrange - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(value: null); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AuthorizationCode)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("2YotnFZFEjr1zCsicMWpAA")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Code = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); - } - - [Fact] - public async Task DeserializeAuthorizationCode_ReturnsExpectedReferenceToken() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AuthorizationCode)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("2YotnFZFEjr1zCsicMWpAA")); - - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero))); - - instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero))); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - - options.AuthorizationCodeFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Code = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); - } - - [Fact] - public async Task DeserializeAuthorizationCode_ReturnsNullForMissingTokenIdentifier() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Code = "2YotnFZFEjr1zCsicMWpAA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is invalid.", response.ErrorDescription); - } - - [Fact] - public async Task DeserializeAuthorizationCode_ReturnsNullForInvalidTokenCiphertext() - { - // Arrange - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(value: null); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Code = "2YotnFZFEjr1zCsicMWpAA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is invalid.", response.ErrorDescription); - - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); - } - - [Fact] - public async Task DeserializeAuthorizationCode_ReturnsExpectedToken() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero))); - - instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero))); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - - options.AuthorizationCodeFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Code = "2YotnFZFEjr1zCsicMWpAA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); - } - - [Fact] - public async Task DeserializeRefreshToken_ReturnsNullForMalformedReferenceToken() - { - // Arrange - var format = new Mock>(); - var manager = CreateTokenManager(); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); - - instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is invalid.", response.ErrorDescription); - - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Never()); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task DeserializeRefreshToken_RefreshTokenIsNotRetrievedUsingHashWhenReferenceTokensAreDisabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync(It.IsAny(), It.IsAny()), Times.Never()); - } - - [Fact] - public async Task DeserializeRefreshToken_ReturnsNullForMissingTokenType() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(result: null)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.AtLeastOnce()); - } - - [Fact] - public async Task DeserializeRefreshToken_ReturnsNullForIncompatibleTokenType() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AuthorizationCode)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.AtLeastOnce()); - } - - [Fact] - public async Task DeserializeRefreshToken_ReturnsNullForMissingReferenceTokenIdentifier() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.RefreshToken)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask(result: null)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); - } - - [Fact] - public async Task DeserializeRefreshToken_ReturnsNullForMissingReferenceTokenCiphertext() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.RefreshToken)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask(result: null)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); - } - - [Fact] - public async Task DeserializeRefreshToken_ReturnsNullForInvalidReferenceTokenCiphertext() - { - // Arrange - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(value: null); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.RefreshToken)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("2YotnFZFEjr1zCsicMWpAA")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is invalid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); - } - - [Fact] - public async Task DeserializeRefreshToken_ReturnsExpectedReferenceToken() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.RefreshToken)); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("2YotnFZFEjr1zCsicMWpAA")); - - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero))); - - instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero))); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - - options.RefreshTokenFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); - } - - [Fact] - public async Task DeserializeRefreshToken_ReturnsNullForMissingTokenIdentifier() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is invalid.", response.ErrorDescription); - } - - [Fact] - public async Task DeserializeRefreshToken_ReturnsNullForInvalidTokenCiphertext() - { - // Arrange - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(value: null); - - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is invalid.", response.ErrorDescription); - - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); - } - - [Fact] - public async Task DeserializeRefreshToken_ReturnsExpectedToken() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero))); - - instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero))); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - - options.RefreshTokenFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); - format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); - } - - [Fact] - public async Task SerializeAccessToken_AccessTokenIsNotPersistedWhenReferenceTokensAreDisabled() - { - // Arrange - var manager = CreateTokenManager(); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AccessToken), - It.IsAny()), Times.Never()); - } - - [Fact] - public async Task SerializeAccessToken_ReferenceAccessTokenIsCorrectlyPersisted() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero)); - options.AccessTokenLifetime = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) - - new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero); - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) && - descriptor.CreationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && - descriptor.Payload != null && - descriptor.ReferenceId != null && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AccessToken), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task SerializeAccessToken_ClientApplicationIsAutomaticallyAttached() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - - instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AccessToken), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task SerializeAccessToken_AuthorizationIsAutomaticallyAttached() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess, - ["attach-authorization"] = true - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.AuthorizationId == "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70" && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AccessToken), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task SerializeAuthorizationCode_AuthorizationCodeIsNotPersistedWhenRevocationIsDisabled() - { - // Arrange - var manager = CreateTokenManager(); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); - - builder.DisableTokenStorage(); - builder.DisableSlidingExpiration(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code - }); - - // Assert - Assert.NotNull(response.Code); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.IsAny(), - It.IsAny()), Times.Never()); - } - - [Fact] - public async Task SerializeAuthorizationCode_AuthorizationCodeIsCorrectlyPersisted() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - - instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)); - options.AuthorizationCodeLifetime = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) - - new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero); - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code - }); - - // Assert - Assert.NotNull(response.Code); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && - descriptor.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) && - descriptor.Payload == null && - descriptor.ReferenceId == null && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AuthorizationCode), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task SerializeAuthorizationCode_ReferenceAuthorizationCodeIsCorrectlyPersisted() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - - instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - })); - - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)); - options.AuthorizationCodeLifetime = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) - - new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero); - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code - }); - - // Assert - Assert.NotNull(response.Code); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && - descriptor.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) && - descriptor.Payload != null && - descriptor.ReferenceId != null && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AuthorizationCode), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task SerializeAuthorizationCode_ClientApplicationIsAutomaticallyAttached() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - - instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - })); - - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code - }); - - // Assert - Assert.NotNull(response.Code); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AuthorizationCode), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task SerializeAuthorizationCode_AuthorizationIsAutomaticallyAttached() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - - instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - })); - - builder.Services.AddSingleton(CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - })); - - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code, - ["attach-authorization"] = true - }); - - // Assert - Assert.NotNull(response.Code); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && - descriptor.AuthorizationId == "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70" && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIdConnectConstants.TokenTypeHints.AuthorizationCode), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task SerializeRefreshToken_ExpirationDateIsFixedWhenSlidingExpirationIsDisabled() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103951EFBA23A56")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.DisableSlidingExpiration(); - builder.UseRollingTokens(); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - options.RefreshTokenLifetime = TimeSpan.FromDays(10); - options.RefreshTokenFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.NotNull(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, - new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero), - It.IsAny()), Times.Never()); - } - - [Fact] - public async Task SerializeRefreshToken_RefreshTokenIsNotPersistedWhenRevocationIsDisabled() - { - // Arrange - var manager = CreateTokenManager(); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); - - builder.DisableTokenStorage(); - builder.DisableSlidingExpiration(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess - }); - - // Assert - Assert.NotNull(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.IsAny(), - It.IsAny()), Times.Never()); - } - - [Fact] - public async Task SerializeRefreshToken_RefreshTokenIsCorrectlyPersisted() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)); - options.RefreshTokenLifetime = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) - - new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero); - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess - }); - - // Assert - Assert.NotNull(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && - descriptor.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) && - descriptor.Payload == null && - descriptor.ReferenceId == null && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIdConnectConstants.TokenTypeHints.RefreshToken), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task SerializeRefreshToken_ReferenceRefreshTokenIsCorrectlyPersisted() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseReferenceTokens(); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)); - options.RefreshTokenLifetime = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) - - new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero); - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess - }); - - // Assert - Assert.NotNull(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && - descriptor.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) && - descriptor.Payload != null && - descriptor.ReferenceId != null && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIdConnectConstants.TokenTypeHints.RefreshToken), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task SerializeRefreshToken_ClientApplicationIsAutomaticallyAttached() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - - instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - })); - - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess - }); - - // Assert - Assert.NotNull(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIdConnectConstants.TokenTypeHints.RefreshToken), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task SerializeRefreshToken_AuthorizationIsAutomaticallyAttached() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - })); - - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess, - ["attach-authorization"] = true - }); - - // Assert - Assert.NotNull(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.AuthorizationId == "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70" && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIdConnectConstants.TokenTypeHints.RefreshToken), - It.IsAny()), Times.Once()); - } - } -} diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Session.cs b/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Session.cs deleted file mode 100644 index fd23e4bf8..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Session.cs +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Immutable; -using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Client; -using AspNet.Security.OpenIdConnect.Primitives; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using OpenIddict.Abstractions; -using Xunit; - -namespace OpenIddict.Server.Tests -{ - public partial class OpenIddictServerProviderTests - { - [Fact] - public async Task ExtractLogoutRequest_RequestIdParameterIsRejectedWhenRequestCachingIsDisabled() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest - { - RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The 'request_id' parameter is not supported.", response.ErrorDescription); - } - - [Fact] - public async Task ExtractLogoutRequest_InvalidRequestIdParameterIsRejected() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddDistributedMemoryCache(); - - builder.EnableRequestCaching(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest - { - RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The specified 'request_id' parameter is invalid.", response.ErrorDescription); - } - - [Theory] - [InlineData("/path", "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.")] - [InlineData("/tmp/file.xml", "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.")] - [InlineData("C:\\tmp\\file.xml", "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.")] - [InlineData("http://www.fabrikam.com/path#param=value", "The 'post_logout_redirect_uri' parameter must not include a fragment.")] - public async Task ValidateLogoutRequest_RequestIsRejectedWhenRedirectUriIsInvalid(string address, string message) - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest - { - PostLogoutRedirectUri = address - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal(message, response.ErrorDescription); - } - - [Fact] - public async Task ValidateLogoutRequest_RequestIsRejectedWhenRedirectUriIsUnknown() - { - // Arrange - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(ImmutableArray.Create()); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest - { - PostLogoutRedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The specified 'post_logout_redirect_uri' parameter is not valid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateLogoutRequest_RequestIsRejectedWhenApplicationHasNoLogoutPermission() - { - // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(ImmutableArray.Create(application)); - - instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Endpoints.Logout, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.IgnoreEndpointPermissions = false); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest - { - PostLogoutRedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The specified 'post_logout_redirect_uri' parameter is not valid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Endpoints.Logout, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleLogoutRequest_RequestIsPersistedInDistributedCache() - { - // Arrange - var cache = new Mock(); - var generator = new Mock(); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(ImmutableArray.Create(new OpenIddictApplication())); - })); - - builder.Services.AddSingleton(cache.Object); - - builder.EnableRequestCaching(); - - builder.SetRequestCachingPolicy(new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(42), - SlidingExpiration = TimeSpan.FromSeconds(42) - }); - - builder.Configure(options => options.RandomNumberGenerator = generator.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest - { - PostLogoutRedirectUri = "http://www.fabrikam.com/path" - }); - - var identifier = (string) response[OpenIddictConstants.Parameters.RequestId]; - - // Assert - Assert.Single(response.GetParameters()); - Assert.NotNull(identifier); - - cache.Verify(mock => mock.SetAsync( - OpenIddictConstants.Environment.LogoutRequest + identifier, - It.IsAny(), - It.Is(options => - options.AbsoluteExpirationRelativeToNow == TimeSpan.FromDays(42) && - options.SlidingExpiration == TimeSpan.FromSeconds(42)), - It.IsAny()), Times.Once()); - - generator.Verify(mock => mock.GetBytes(It.Is(bytes => bytes.Length == 256 / 8)), Times.Once()); - } - - [Fact] - public async Task HandleLogoutRequest_RequestsAreNotHandledLocally() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(ImmutableArray.Create(new OpenIddictApplication())); - })); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest - { - PostLogoutRedirectUri = "http://www.fabrikam.com/path", - State = "af0ifjsldkj" - }); - - // Assert - Assert.Equal("af0ifjsldkj", response.State); - } - - [Fact] - public async Task ApplyLogoutResponse_SupportsNullRequests() - { - // Note: when an invalid HTTP verb is used, the OpenID Connect server handler refuses to extract the request - // and immediately returns an error. In this specific case, ApplyLogoutResponseContext.Request is null - // and this test ensures ApplyLogoutResponse can safely handle cases where the request is unavailable. - - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.EnableRequestCaching(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.SendAsync(HttpMethods.Put, LogoutEndpoint, new OpenIdConnectRequest()); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The specified HTTP method is not valid.", response.ErrorDescription); - } - - [Fact] - public async Task ApplyLogoutResponse_ErroredRequestIsNotHandledLocallyWhenStatusCodeMiddlewareIsEnabled() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(ImmutableArray.Create(new OpenIddictApplication())); - })); - - builder.EnableAuthorizationEndpoint("/logout-status-code-middleware"); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync("/logout-status-code-middleware", new OpenIdConnectRequest - { - PostLogoutRedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidRequest, (string) response["error_custom"]); - } - } -} diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Userinfo.cs b/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Userinfo.cs deleted file mode 100644 index b4fad0862..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.Userinfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Client; -using AspNet.Security.OpenIdConnect.Primitives; -using OpenIddict.Abstractions; -using Xunit; - -namespace OpenIddict.Server.Tests -{ - public partial class OpenIddictServerProviderTests - { - [Fact] - public async Task ExtractUserinfoRequest_RequestIsHandledByUserCode() - { - // Arrange - var server = CreateAuthorizationServer(); - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(UserinfoEndpoint, new OpenIdConnectRequest - { - AccessToken = "SlAV32hkKG" - }); - - // Assert - Assert.Equal("SlAV32hkKG", (string) response[OpenIddictConstants.Parameters.AccessToken]); - Assert.Equal("Bob le Bricoleur", (string) response[OpenIddictConstants.Claims.Subject]); - } - } -} diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.cs deleted file mode 100644 index 7c4e7a265..000000000 --- a/test/OpenIddict.Server.Tests/OpenIddictServerProviderTests.cs +++ /dev/null @@ -1,1767 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Immutable; -using System.Linq; -using System.Reflection; -using System.Security.Claims; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Client; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Primitives; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Diagnostics; -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 Microsoft.Net.Http.Headers; -using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OpenIddict.Abstractions; -using OpenIddict.Core; -using Xunit; - -namespace OpenIddict.Server.Tests -{ - public partial class OpenIddictServerProviderTests - { - public const string AuthorizationEndpoint = "/connect/authorize"; - public const string ConfigurationEndpoint = "/.well-known/openid-configuration"; - public const string IntrospectionEndpoint = "/connect/introspect"; - public const string LogoutEndpoint = "/connect/logout"; - public const string RevocationEndpoint = "/connect/revoke"; - public const string TokenEndpoint = "/connect/token"; - public const string UserinfoEndpoint = "/connect/userinfo"; - - [Fact] - public async Task ProcessChallengeResponse_CustomPublicParametersAreAddedToAuthorizationResponse() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code, - ["attach-public-parameters"] = true, - ["deny-authorization"] = true - }); - - // Assert - Assert.NotEmpty(response.Error); - Assert.NotEmpty(response.ErrorDescription); - Assert.True((bool) response["custom_boolean_parameter"]); - Assert.Equal(42, (long) response["custom_integer_parameter"]); - Assert.Equal("value", (string) response["custom_string_parameter"]); - } - - [Fact] - public async Task ProcessChallengeResponse_CustomPublicParametersAreAddedToTokenResponse() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess, - ["attach-public-parameters"] = true, - ["deny-authorization"] = true - }); - - // Assert - Assert.NotEmpty(response.Error); - Assert.NotEmpty(response.ErrorDescription); - Assert.True((bool) response["custom_boolean_parameter"]); - Assert.Equal(42, (long) response["custom_integer_parameter"]); - Assert.Equal(new JArray(1, 2, 3), (JArray) response["custom_json_array_parameter"]); - Assert.Equal(JObject.FromObject(new { Property = "value" }), (JObject) response["custom_json_object_parameter"]); - Assert.Equal("value", (string) response["custom_string_parameter"]); - } - - [Fact] - public async Task ProcessSigninResponse_ThrowsAnExceptionForInvalidIdentity() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - ["use-null-authentication-type"] = true - }); - }); - - Assert.Equal(new StringBuilder() - .AppendLine("The specified principal doesn't contain a valid or authenticated identity.") - .Append("Make sure that both 'ClaimsPrincipal.Identity' and 'ClaimsPrincipal.Identity.AuthenticationType' ") - .Append("are not null and that 'ClaimsPrincipal.Identity.IsAuthenticated' returns 'true'.") - .ToString(), exception.Message); - } - - [Fact] - public async Task ProcessSigninResponse_AuthenticationPropertiesAreAutomaticallyRestored() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - ticket.SetProperty("custom_property_in_original_ticket", "original_value"); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseRollingTokens(); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8", - ["do-not-flow-original-properties"] = true - }); - - // Assert - Assert.NotNull(response.IdToken); - Assert.NotNull(response.RefreshToken); - - format.Verify(mock => mock.Protect( - It.Is(value => - value.Properties.Items["custom_property_in_original_ticket"] == "original_value" && - value.Properties.Items["custom_property_in_new_ticket"] == "new_value"))); - } - - [Fact] - public async Task ProcessSigninResponse_RefreshTokenIsIssuedForAuthorizationCodeRequestsWhenRollingTokensAreEnabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(manager); - - builder.UseRollingTokens(); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.NotNull(response.RefreshToken); - } - - [Fact] - public async Task ProcessSigninResponse_RefreshTokenIsAlwaysIssuedWhenRollingTokensAreEnabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseRollingTokens(); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.NotNull(response.RefreshToken); - } - - [Fact] - public async Task ProcessSigninResponse_RefreshTokenIsNotIssuedWhenRollingTokensAreDisabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Null(response.RefreshToken); - } - - [Fact] - public async Task ProcessSigninResponse_AuthorizationCodeIsAutomaticallyRedeemed() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_ReturnsErrorResponseWhenRedeemingAuthorizationCodeFails() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.RedeemAsync(token, It.IsAny())) - .ThrowsAsync(new Exception()); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_RefreshTokenIsAutomaticallyRedeemedWhenRollingTokensAreEnabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseRollingTokens(); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.NotNull(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_ReturnsErrorResponseWhenRedeemingRefreshTokenFails() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.RedeemAsync(token, It.IsAny())) - .ThrowsAsync(new Exception()); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseRollingTokens(); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_RefreshTokenIsNotRedeemedWhenRollingTokensAreDisabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Null(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Never()); - } - - [Fact] - public async Task ProcessSigninResponse_PreviousTokensAreAutomaticallyRevokedWhenRollingTokensAreEnabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var tokens = ImmutableArray.Create( - new OpenIddictToken(), - new OpenIddictToken(), - new OpenIddictToken()); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(tokens[0]); - - instance.Setup(mock => mock.GetIdAsync(tokens[0], It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103")); - - instance.Setup(mock => mock.GetIdAsync(tokens[1], It.IsAny())) - .Returns(new ValueTask("481FCAC6-06BC-43EE-92DB-37A78AA09B595073CC313103")); - - instance.Setup(mock => mock.GetIdAsync(tokens[2], It.IsAny())) - .Returns(new ValueTask("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8")); - - instance.Setup(mock => mock.GetAuthorizationIdAsync(tokens[0], It.IsAny())) - .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); - - instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(tokens[0], It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(tokens); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateAuthorizationManager(instance => - { - var authorization = new OpenIddictAuthorization(); - - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.UseRollingTokens(); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.NotNull(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny()), Times.Never()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_PreviousTokensAreNotRevokedWhenRollingTokensAreDisabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - ticket.SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var tokens = ImmutableArray.Create( - new OpenIddictToken(), - new OpenIddictToken(), - new OpenIddictToken()); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(tokens[0]); - - instance.Setup(mock => mock.GetIdAsync(tokens[0], It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103")); - - instance.Setup(mock => mock.GetIdAsync(tokens[1], It.IsAny())) - .Returns(new ValueTask("481FCAC6-06BC-43EE-92DB-37A78AA09B595073CC313103")); - - instance.Setup(mock => mock.GetIdAsync(tokens[2], It.IsAny())) - .Returns(new ValueTask("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8")); - - instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(tokens[0], It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(tokens); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateAuthorizationManager(instance => - { - var authorization = new OpenIddictAuthorization(); - - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) - .ReturnsAsync(true); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.NotNull(response.AccessToken); - Assert.Null(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny()), Times.Never()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Never()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), Times.Never()); - } - - [Fact] - public async Task ProcessSigninResponse_ExtendsLifetimeWhenRollingTokensAreDisabledAndSlidingExpirationEnabled() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - options.RefreshTokenLifetime = TimeSpan.FromDays(10); - options.RefreshTokenFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Null(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, - new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_DoesNotExtendLifetimeWhenSlidingExpirationIsDisabled() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.DisableSlidingExpiration(); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - options.RefreshTokenLifetime = TimeSpan.FromDays(10); - options.RefreshTokenFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Null(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, - new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), - It.IsAny()), Times.Never()); - } - - [Fact] - public async Task ProcessSigninResponse_DoesNotUpdateExpirationDateWhenAlreadyNull() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(result: null)); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - options.RefreshTokenLifetime = null; - options.RefreshTokenFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Null(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, null, It.IsAny()), Times.Never()); - } - - [Fact] - public async Task ProcessSigninResponse_SetsExpirationDateToNullWhenLifetimeIsNull() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(DateTimeOffset.Now + TimeSpan.FromDays(1))); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - options.RefreshTokenLifetime = null; - options.RefreshTokenFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Null(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, null, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_IgnoresErrorWhenExtendingLifetimeOfExistingTokenFailed() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103")); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.ExtendAsync(token, It.IsAny(), It.IsAny())) - .ThrowsAsync(new Exception()); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - options.RefreshTokenLifetime = TimeSpan.FromDays(10); - options.RefreshTokenFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, - new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_AdHocAuthorizationIsAutomaticallyCreated() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - - instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - })); - - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code, - }); - - // Assert - Assert.NotNull(response.Code); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIddictConstants.AuthorizationTypes.AdHoc), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_AdHocAuthorizationIsNotCreatedWhenAuthorizationStorageIsDisabled() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - - instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - })); - - builder.Services.AddSingleton(manager); - - builder.DisableAuthorizationStorage(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code, - }); - - // Assert - Assert.NotNull(response.Code); - - Mock.Get(manager).Verify(mock => mock.CreateAsync(It.IsAny(), It.IsAny()), Times.Never()); - } - - [Fact] - public async Task ProcessSigninResponse_CustomPublicParametersAreAddedToAuthorizationResponse() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); - })); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIddictConstants.ResponseTypes.Code, - ["attach-public-parameters"] = true - }); - - // Assert - Assert.True((bool) response["custom_boolean_parameter"]); - Assert.Equal(42, (long) response["custom_integer_parameter"]); - Assert.False(response.HasParameter("custom_json_array_parameter")); - Assert.False(response.HasParameter("custom_json_object_parameter")); - Assert.Equal("value", (string) response["custom_string_parameter"]); - } - - [Fact] - public async Task ProcessSigninResponse_CustomPublicParametersAreAddedToTokenResponse() - { - // Arrange - var server = CreateAuthorizationServer(); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess, - ["attach-public-parameters"] = true - }); - - // Assert - Assert.True((bool) response["custom_boolean_parameter"]); - Assert.Equal(42, (long) response["custom_integer_parameter"]); - Assert.Equal(new JArray(1, 2, 3), (JArray) response["custom_json_array_parameter"]); - Assert.Equal(JObject.FromObject(new { Property = "value" }), (JObject) response["custom_json_object_parameter"]); - Assert.Equal("value", (string) response["custom_string_parameter"]); - } - - [Fact] - public async Task ProcessSigninResponse_CustomPublicParametersAreRemovedFromTicket() - { - // Arrange - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.AccessTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIddictConstants.GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - Scope = OpenIddictConstants.Scopes.OfflineAccess, - ["attach-public-parameters"] = true - }); - - // Assert - Assert.NotNull(response.AccessToken); - - format.Verify(mock => mock.Protect( - It.Is(ticket => - !ticket.Properties.Items.Any(property => property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Boolean)) && - !ticket.Properties.Items.Any(property => property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Integer)) && - !ticket.Properties.Items.Any(property => property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Json)) && - !ticket.Properties.Items.Any(property => property.Key.EndsWith(OpenIddictConstants.PropertyTypes.String))))); - } - - [Fact] - public async Task ProcessSignoutResponse_CustomPublicParametersAreAddedToLogoutResponse() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - instance.Setup(mock => mock.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(ImmutableArray.Create(new OpenIddictApplication())); - })); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest - { - PostLogoutRedirectUri = "http://www.fabrikam.com/path", - State = "af0ifjsldkj", - ["attach-public-parameters"] = true - }); - - // Assert - Assert.True((bool) response["custom_boolean_parameter"]); - Assert.Equal(42, (long) response["custom_integer_parameter"]); - Assert.False(response.HasParameter("custom_json_array_parameter")); - Assert.False(response.HasParameter("custom_json_object_parameter")); - Assert.Equal("value", (string) response["custom_string_parameter"]); - } - - private static TestServer CreateAuthorizationServer(Action configuration = null) - { - var builder = new WebHostBuilder(); - - builder.UseEnvironment("Testing"); - - builder.ConfigureLogging(options => options.AddDebug()); - - builder.ConfigureServices(services => - { - services.AddOptions(); - services.AddDistributedMemoryCache(); - - services.AddOpenIddict() - .AddCore(options => - { - options.SetDefaultApplicationEntity() - .SetDefaultAuthorizationEntity() - .SetDefaultScopeEntity() - .SetDefaultTokenEntity(); - - options.Services.AddSingleton(CreateApplicationManager()) - .AddSingleton(CreateAuthorizationManager()) - .AddSingleton(CreateScopeManager()) - .AddSingleton(CreateTokenManager()); - }) - - .AddServer(options => - { - // Accept anonymous clients by default. - options.AcceptAnonymousClients(); - - // Disable permission enforcement by default. - options.IgnoreEndpointPermissions() - .IgnoreGrantTypePermissions() - .IgnoreScopePermissions(); - - // Disable the transport security requirement during testing. - options.DisableHttpsRequirement(); - - // Enable the tested endpoints. - options.EnableAuthorizationEndpoint(AuthorizationEndpoint) - .EnableIntrospectionEndpoint(IntrospectionEndpoint) - .EnableLogoutEndpoint(LogoutEndpoint) - .EnableRevocationEndpoint(RevocationEndpoint) - .EnableTokenEndpoint(TokenEndpoint) - .EnableUserinfoEndpoint(UserinfoEndpoint); - - // Enable the tested flows. - options.AllowAuthorizationCodeFlow() - .AllowClientCredentialsFlow() - .AllowImplicitFlow() - .AllowPasswordFlow() - .AllowRefreshTokenFlow(); - - // Register the X.509 certificate used to sign the identity tokens. - options.AddSigningCertificate( - assembly: typeof(OpenIddictServerProviderTests).GetTypeInfo().Assembly, - resource: "OpenIddict.Server.Tests.Certificate.pfx", - password: "OpenIddict"); - - // Note: overriding the default data protection provider is not necessary for the tests to pass, - // but is useful to ensure unnecessary keys are not persisted in testing environments, which also - // helps make the unit tests run faster, as no registry or disk access is required in this case. - options.UseDataProtectionProvider(new EphemeralDataProtectionProvider()); - - // Run the configuration delegate - // registered by the unit tests. - configuration?.Invoke(options); - }); - }); - - builder.Configure(app => - { - app.UseStatusCodePages(context => - { - context.HttpContext.Response.Headers[HeaderNames.ContentType] = "application/json"; - - return context.HttpContext.Response.WriteAsync(JsonConvert.SerializeObject(new - { - error_custom = OpenIddictConstants.Errors.InvalidRequest - })); - }); - - app.Use(next => context => - { - if (context.Request.Path != "/authorize-status-code-middleware" && - context.Request.Path != "/logout-status-code-middleware") - { - var feature = context.Features.Get(); - feature.Enabled = false; - } - - return next(context); - }); - - app.UseAuthentication(); - - app.Run(context => - { - var request = context.GetOpenIdConnectRequest(); - if (request == null) - { - return Task.CompletedTask; - } - - var identity = !request.HasParameter("use-null-authentication-type") ? - new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme) : - new ClaimsIdentity(); - - identity.AddClaim(OpenIddictConstants.Claims.Subject, "Bob le Magnifique"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIddictServerDefaults.AuthenticationScheme); - - ticket.SetScopes(request.GetScopes()); - - if (request.HasParameter("attach-authorization")) - { - ticket.SetInternalAuthorizationId("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70"); - } - - if (request.HasParameter("attach-public-parameters")) - { - ticket.SetProperty("custom_boolean_parameter" + OpenIddictConstants.PropertyTypes.Boolean, "true"); - ticket.SetProperty("custom_integer_parameter" + OpenIddictConstants.PropertyTypes.Integer, "42"); - - ticket.SetProperty("custom_json_array_parameter" + OpenIddictConstants.PropertyTypes.Json, - new JArray(1, 2, 3).ToString()); - ticket.SetProperty("custom_json_object_parameter" + OpenIddictConstants.PropertyTypes.Json, - JObject.FromObject(new { Property = "value" }).ToString()); - - ticket.SetProperty("custom_string_parameter" + OpenIddictConstants.PropertyTypes.String, "value"); - } - - if (request.IsAuthorizationRequest() || request.IsTokenRequest()) - { - if (request.HasParameter("deny-authorization")) - { - return context.ForbidAsync(OpenIddictServerDefaults.AuthenticationScheme, ticket.Properties); - } - - if (request.HasParameter("do-not-flow-original-properties")) - { - var properties = new AuthenticationProperties(); - properties.SetProperty("custom_property_in_new_ticket", "new_value"); - - return context.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, properties); - } - - return context.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties); - } - - else if (request.IsLogoutRequest()) - { - return context.SignOutAsync(OpenIddictServerDefaults.AuthenticationScheme, ticket.Properties); - } - - else if (request.IsUserinfoRequest()) - { - context.Response.Headers[HeaderNames.ContentType] = "application/json"; - - return context.Response.WriteAsync(JsonConvert.SerializeObject(new - { - access_token = request.AccessToken, - sub = "Bob le Bricoleur" - })); - } - - return Task.CompletedTask; - }); - }); - - return new TestServer(builder); - } - - private static OpenIddictApplicationManager CreateApplicationManager( - Action>> configuration = null) - { - var manager = new Mock>( - Mock.Of>(), - Mock.Of(), - Mock.Of>>(), - Mock.Of>()); - - configuration?.Invoke(manager); - - return manager.Object; - } - - private static OpenIddictAuthorizationManager CreateAuthorizationManager( - Action>> configuration = null) - { - var manager = new Mock>( - Mock.Of>(), - Mock.Of(), - Mock.Of>>(), - Mock.Of>()); - - configuration?.Invoke(manager); - - return manager.Object; - } - - private static OpenIddictScopeManager CreateScopeManager( - Action>> configuration = null) - { - var manager = new Mock>( - Mock.Of>(), - Mock.Of(), - Mock.Of>>(), - Mock.Of>()); - - configuration?.Invoke(manager); - - return manager.Object; - } - - private static OpenIddictTokenManager CreateTokenManager( - Action>> configuration = null) - { - var manager = new Mock>( - Mock.Of>(), - Mock.Of(), - Mock.Of>>(), - Mock.Of>()); - - configuration?.Invoke(manager); - - return manager.Object; - } - - public class OpenIddictApplication { } - public class OpenIddictAuthorization { } - public class OpenIddictScope { } - public class OpenIddictToken { } - } -} diff --git a/test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj b/test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj deleted file mode 100755 index 8b6621216..000000000 --- a/test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - netcoreapp2.0;net461 - netcoreapp2.0 - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs deleted file mode 100644 index 35d4926cf..000000000 --- a/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Xunit; -using static OpenIddict.Validation.OpenIddictValidationEvents; - -namespace OpenIddict.Validation.Tests -{ - public class OpenIddictValidationBuilderTests - { - [Fact] - public void Constructor_ThrowsAnExceptionForNullServices() - { - // Arrange - var services = (IServiceCollection) null; - - // Act and assert - var exception = Assert.Throws(() => new OpenIddictValidationBuilder(services)); - - Assert.Equal("services", exception.ParamName); - } - - [Fact] - public void AddEventHandler_HandlerIsAttached() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AddEventHandler(notification => Task.FromResult(OpenIddictValidationEventState.Handled)); - - // Assert - Assert.Contains(services, service => - service.ServiceType == typeof(IOpenIddictValidationEventHandler) && - service.ImplementationInstance.GetType() == typeof(OpenIddictValidationEventHandler)); - } - - [Fact] - public void AddEventHandler_ThrowsAnExceptionForUnsupportedLifetime() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act and assert - var exception = Assert.Throws(delegate - { - return builder.AddEventHandler(ServiceLifetime.Transient); - }); - - Assert.Equal("lifetime", exception.ParamName); - Assert.StartsWith("Handlers cannot be registered as transient services.", exception.Message); - } - - [Fact] - public void AddEventHandler_ThrowsAnExceptionForOpenGenericHandlerType() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act and assert - var exception = Assert.Throws(delegate - { - return builder.AddEventHandler(typeof(OpenIddictValidationEventHandler<>)); - }); - - Assert.Equal("type", exception.ParamName); - Assert.StartsWith("The specified type is invalid.", exception.Message); - } - - [Fact] - public void AddEventHandler_ThrowsAnExceptionForNonHandlerType() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act and assert - var exception = Assert.Throws(delegate - { - return builder.AddEventHandler(typeof(object)); - }); - - Assert.Equal("type", exception.ParamName); - Assert.StartsWith("The specified type is invalid.", exception.Message); - } - - [Fact] - public void AddEventHandler_HandlerIsRegistered() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AddEventHandler(ServiceLifetime.Singleton); - - // Assert - Assert.Contains(services, service => - service.ServiceType == typeof(IOpenIddictValidationEventHandler) && - service.ImplementationType == typeof(CustomHandler) && - service.Lifetime == ServiceLifetime.Singleton); - Assert.Contains(services, service => - service.ServiceType == typeof(IOpenIddictValidationEventHandler) && - service.ImplementationType == typeof(CustomHandler) && - service.Lifetime == ServiceLifetime.Singleton); - } - - [Fact] - public void Configure_OptionsAreCorrectlyAmended() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.Configure(configuration => configuration.ClaimsIssuer = "custom_issuer"); - - var options = GetOptions(services); - - // Assert - Assert.Equal("custom_issuer", options.ClaimsIssuer); - } - - [Fact] - public void AddAudiences_AudiencesAreAdded() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.AddAudiences("Fabrikam", "Contoso"); - - var options = GetOptions(services); - - // Assert - Assert.Equal(new[] { "Fabrikam", "Contoso" }, options.Audiences); - } - - [Fact] - public void EnableAuthorizationValidation_ValidationIsEnforced() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.EnableAuthorizationValidation(); - - var options = GetOptions(services); - - // Assert - Assert.True(options.EnableAuthorizationValidation); - } - - [Fact] - public void RemoveErrorDetails_IncludeErrorDetailsIsSetToFalse() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.RemoveErrorDetails(); - - var options = GetOptions(services); - - // Assert - Assert.False(options.IncludeErrorDetails); - } - - [Fact] - public void SetRealm_RealmIsReplaced() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.SetRealm("custom_realm"); - - var options = GetOptions(services); - - // Assert - Assert.Equal("custom_realm", options.Realm); - } - - [Fact] - public void UseDataProtectionProvider_DefaultProviderIsReplaced() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.UseDataProtectionProvider(new EphemeralDataProtectionProvider()); - - var options = GetOptions(services); - - // Assert - Assert.IsType(options.DataProtectionProvider); - } - - [Fact] - public void UseReferenceTokens_ReferenceTokensAreEnabled() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.UseReferenceTokens(); - - var options = GetOptions(services); - - // Assert - Assert.True(options.UseReferenceTokens); - } - - private static IServiceCollection CreateServices() - => new ServiceCollection().AddOptions(); - - private static OpenIddictValidationBuilder CreateBuilder(IServiceCollection services) - => new OpenIddictValidationBuilder(services); - - private static OpenIddictValidationOptions GetOptions(IServiceCollection services) - { - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); - return options.Get(OpenIddictValidationDefaults.AuthenticationScheme); - } - - public class CustomHandler : IOpenIddictValidationEventHandler, - IOpenIddictValidationEventHandler - { - public Task HandleAsync(ApplyChallenge notification) - { - throw new NotImplementedException(); - } - - public Task HandleAsync(CreateTicket notification) - { - throw new NotImplementedException(); - } - } - } -} diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs deleted file mode 100644 index a45c691a1..000000000 --- a/test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Text; -using System.Threading.Tasks; -using AspNet.Security.OAuth.Validation; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using Xunit; - -namespace OpenIddict.Validation.Tests -{ - public class OpenIddictValidationConfigurationTests - { - [Fact] - public void Configure_ThrowsAnExceptionForNullOptions() - { - // Arrange - var configuration = new OpenIddictValidationConfiguration(Mock.Of()); - - // Act and assert - var exception = Assert.Throws(() => configuration.Configure(null)); - - Assert.Equal("options", exception.ParamName); - } - - [Theory] - [InlineData(new object[] { new string[] { OpenIddictValidationDefaults.AuthenticationScheme, null } })] - [InlineData(new object[] { new string[] { null, OpenIddictValidationDefaults.AuthenticationScheme } })] - public void PostConfigure_ThrowsAnExceptionWhenDefaultSchemesPointToValidationHandler(string[] schemes) - { - // Arrange - var options = new AuthenticationOptions - { - DefaultSignInScheme = schemes[0], - DefaultSignOutScheme = schemes[1] - }; - - options.AddScheme(OpenIddictValidationDefaults.AuthenticationScheme, displayName: null); - - var configuration = new OpenIddictValidationConfiguration(Mock.Of()); - - // Act and assert - var exception = Assert.Throws(() => configuration.PostConfigure(Options.DefaultName, options)); - - // Assert - Assert.Equal(new StringBuilder() - .AppendLine("The OpenIddict validation handler cannot be used as the default sign-in/out scheme handler.") - .Append("Make sure that neither DefaultSignInScheme nor DefaultSignOutScheme ") - .Append("point to an instance of the OpenIddict validation handler.") - .ToString(), exception.Message); - } - - [Fact] - public void Configure_ThrowsAnExceptionWhenSchemeIsAlreadyRegisteredWithDifferentHandlerType() - { - // Arrange - var options = new AuthenticationOptions(); - options.AddScheme(OpenIddictValidationDefaults.AuthenticationScheme, builder => - { - builder.HandlerType = typeof(OAuthValidationHandler); - }); - - var configuration = new OpenIddictValidationConfiguration(Mock.Of()); - - // Act and assert - var exception = Assert.Throws(() => configuration.Configure(options)); - - Assert.Equal(new StringBuilder() - .AppendLine("The OpenIddict validation handler cannot be registered as an authentication scheme.") - .AppendLine("This may indicate that an instance of the OAuth validation or JWT bearer handler was registered.") - .Append("Make sure that neither 'services.AddAuthentication().AddOAuthValidation()' nor ") - .Append("'services.AddAuthentication().AddJwtBearer()' are called from 'ConfigureServices'.") - .ToString(), exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenEventsTypeIsNull() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Configure(options => options.EventsType = null); - }); - - var client = server.CreateClient(); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - // Assert - Assert.Equal(new StringBuilder() - .AppendLine("OpenIddict can only be used with its built-in validation provider.") - .AppendLine("This error may indicate that 'OpenIddictValidationOptions.EventsType' was manually set.") - .Append("To execute custom request handling logic, consider registering an event handler using ") - .Append("the generic 'services.AddOpenIddict().AddValidation().AddEventHandler()' method.") - .ToString(), exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenEventsTypeIsIncompatible() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Configure(options => options.EventsType = typeof(OAuthValidationEvents)); - }); - - var client = server.CreateClient(); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - // Assert - Assert.Equal(new StringBuilder() - .AppendLine("OpenIddict can only be used with its built-in validation provider.") - .AppendLine("This error may indicate that 'OpenIddictValidationOptions.EventsType' was manually set.") - .Append("To execute custom request handling logic, consider registering an event handler using ") - .Append("the generic 'services.AddOpenIddict().AddValidation().AddEventHandler()' method.") - .ToString(), exception.Message); - } - - private static TestServer CreateResourceServer(Action configuration = null) - { - var builder = new WebHostBuilder(); - - builder.UseEnvironment("Testing"); - - builder.ConfigureLogging(options => options.AddDebug()); - - builder.ConfigureServices(services => - { - services.AddAuthentication(); - services.AddOptions(); - services.AddDistributedMemoryCache(); - - services.AddOpenIddict() - .AddValidation(options => configuration?.Invoke(options)); - }); - - builder.Configure(app => - { - app.UseAuthentication(); - - app.Run(context => context.ChallengeAsync(OpenIddictValidationDefaults.AuthenticationScheme)); - }); - - return new TestServer(builder); - } - } -} diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationEventDispatcherTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationEventDispatcherTests.cs deleted file mode 100644 index c1e77f9a5..000000000 --- a/test/OpenIddict.Validation.Tests/OpenIddictValidationEventDispatcherTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Moq; -using Xunit; - -namespace OpenIddict.Validation.Tests -{ - public class OpenIddictValidationEventDispatcherTests - { - [Fact] - public async Task DispatchAsync_ThrowsAnExceptionForNullNotification() - { - // Arrange - var provider = Mock.Of(); - var dispatcher = new OpenIddictValidationEventDispatcher(provider); - - // Act and assert - var exception = await Assert.ThrowsAsync(() - => dispatcher.DispatchAsync(notification: null)); - - Assert.Equal("notification", exception.ParamName); - } - - [Fact] - public async Task DispatchAsync_InvokesHandlers() - { - // Arrange - var handlers = new List> - { - Mock.Of>(), - Mock.Of>() - }; - - var provider = new Mock(); - provider.Setup(mock => mock.GetService(typeof(IEnumerable>))) - .Returns(handlers); - - var dispatcher = new OpenIddictValidationEventDispatcher(provider.Object); - - var notification = new Event(); - - // Act - await dispatcher.DispatchAsync(notification); - - // Assert - Mock.Get(handlers[0]).Verify(mock => mock.HandleAsync(notification), Times.Once()); - Mock.Get(handlers[1]).Verify(mock => mock.HandleAsync(notification), Times.Once()); - } - - [Fact] - public async Task DispatchAsync_StopsInvokingHandlersWhenHandledIsReturned() - { - // Arrange - var handlers = new List> - { - Mock.Of>( - mock => mock.HandleAsync(It.IsAny()) == Task.FromResult(OpenIddictValidationEventState.Unhandled)), - Mock.Of>( - mock => mock.HandleAsync(It.IsAny()) == Task.FromResult(OpenIddictValidationEventState.Unhandled)), - Mock.Of>( - mock => mock.HandleAsync(It.IsAny()) == Task.FromResult(OpenIddictValidationEventState.Handled)), - Mock.Of>() - }; - - var provider = new Mock(); - provider.Setup(mock => mock.GetService(typeof(IEnumerable>))) - .Returns(handlers); - - var dispatcher = new OpenIddictValidationEventDispatcher(provider.Object); - - var notification = new Event(); - - // Act - await dispatcher.DispatchAsync(notification); - - // Assert - Mock.Get(handlers[0]).Verify(mock => mock.HandleAsync(notification), Times.Once()); - Mock.Get(handlers[1]).Verify(mock => mock.HandleAsync(notification), Times.Once()); - Mock.Get(handlers[2]).Verify(mock => mock.HandleAsync(notification), Times.Once()); - Mock.Get(handlers[3]).Verify(mock => mock.HandleAsync(notification), Times.Never()); - } - - public class Event : IOpenIddictValidationEvent { } - } -} diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationEventHandlerTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationEventHandlerTests.cs deleted file mode 100644 index 279352dc4..000000000 --- a/test/OpenIddict.Validation.Tests/OpenIddictValidationEventHandlerTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Threading.Tasks; -using Xunit; - -namespace OpenIddict.Validation.Tests -{ - public class OpenIddictValidationEventHandlerTests - { - [Fact] - public void Constructor_ThrowsAnExceptionForNullHandler() - { - // Arrange, act and assert - var exception = Assert.Throws(() - => new OpenIddictValidationEventHandler(handler: null)); - - Assert.Equal("handler", exception.ParamName); - } - - [Fact] - public async Task HandleAsync_ThrowsAnExceptionForNullNotification() - { - // Arrange - var handler = new OpenIddictValidationEventHandler( - notification => Task.FromResult(OpenIddictValidationEventState.Handled)); - - // Act and assert - var exception = await Assert.ThrowsAsync(() - => handler.HandleAsync(notification: null)); - - Assert.Equal("notification", exception.ParamName); - } - - [Fact] - public async Task HandleAsync_InvokesInlineHandler() - { - // Arrange - var marker = false; - var handler = new OpenIddictValidationEventHandler( - notification => - { - marker = true; - return Task.FromResult(OpenIddictValidationEventState.Handled); - }); - - // Act - await handler.HandleAsync(new Event()); - - // Assert - Assert.True(marker); - } - - public class Event : IOpenIddictValidationEvent { } - } -} diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationExtensionsTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationExtensionsTests.cs deleted file mode 100644 index c6d7f2504..000000000 --- a/test/OpenIddict.Validation.Tests/OpenIddictValidationExtensionsTests.cs +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Text; -using AspNet.Security.OAuth.Validation; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Xunit; - -namespace OpenIddict.Validation.Tests -{ - public class OpenIddictValidationExtensionsTests - { - [Fact] - public void AddValidation_ThrowsAnExceptionForNullBuilder() - { - // Arrange - var builder = (OpenIddictBuilder) null; - - // Act and assert - var exception = Assert.Throws(() => builder.AddValidation()); - - Assert.Equal("builder", exception.ParamName); - } - - [Fact] - public void AddValidation_ThrowsAnExceptionForNullConfiguration() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act and assert - var exception = Assert.Throws(() => builder.AddValidation(configuration: null)); - - Assert.Equal("configuration", exception.ParamName); - } - - [Fact] - public void AddValidation_RegistersAuthenticationServices() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddValidation(); - - // Assert - Assert.Contains(services, service => service.ServiceType == typeof(IAuthenticationService)); - } - - [Fact] - public void AddValidation_RegistersLoggingServices() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddValidation(); - - // Assert - Assert.Contains(services, service => service.ServiceType == typeof(ILogger<>)); - } - - [Fact] - public void AddValidation_RegistersOptionsServices() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddValidation(); - - // Assert - Assert.Contains(services, service => service.ServiceType == typeof(IOptions<>)); - } - - [Fact] - public void AddValidation_RegistersEventService() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddValidation(); - - // Assert - Assert.Contains(services, service => service.Lifetime == ServiceLifetime.Scoped && - service.ServiceType == typeof(IOpenIddictValidationEventDispatcher) && - service.ImplementationType == typeof(OpenIddictValidationEventDispatcher)); - } - - [Fact] - public void AddValidation_RegistersHandler() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddValidation(); - - // Assert - Assert.Contains(services, service => service.Lifetime == ServiceLifetime.Scoped && - service.ServiceType == typeof(OpenIddictValidationHandler) && - service.ImplementationType == typeof(OpenIddictValidationHandler)); - } - - [Fact] - public void AddValidation_RegistersProvider() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddValidation(); - - // Assert - Assert.Contains(services, service => service.Lifetime == ServiceLifetime.Scoped && - service.ServiceType == typeof(OpenIddictValidationProvider) && - service.ImplementationType == typeof(OpenIddictValidationProvider)); - } - - [Theory] - [InlineData(typeof(IPostConfigureOptions), typeof(OpenIddictValidationConfiguration))] - [InlineData(typeof(IPostConfigureOptions), typeof(OAuthValidationInitializer))] - public void AddValidation_RegistersConfiguration(Type serviceType, Type implementationType) - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddValidation(); - - // Assert - Assert.Contains(services, service => service.ServiceType == serviceType && - service.ImplementationType == implementationType); - } - - [Fact] - public void AddValidation_RegistersAuthenticationScheme() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddValidation(); - - // Assert - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>().Value; - - Assert.Contains(options.Schemes, scheme => scheme.Name == OpenIddictValidationDefaults.AuthenticationScheme && - scheme.HandlerType == typeof(OpenIddictValidationHandler)); - } - - [Fact] - public void AddValidation_ThrowsAnExceptionWhenSchemeIsAlreadyRegisteredWithDifferentHandlerType() - { - // Arrange - var services = new ServiceCollection(); - services.AddAuthentication() - .AddOAuthValidation(); - - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddValidation(); - - // Assert - var provider = services.BuildServiceProvider(); - var exception = Assert.Throws(delegate - { - return provider.GetRequiredService>().Value; - }); - - Assert.Equal(new StringBuilder() - .AppendLine("The OpenIddict validation handler cannot be registered as an authentication scheme.") - .AppendLine("This may indicate that an instance of the OAuth validation or JWT bearer handler was registered.") - .Append("Make sure that neither 'services.AddAuthentication().AddOAuthValidation()' nor ") - .Append("'services.AddAuthentication().AddJwtBearer()' are called from 'ConfigureServices'.") - .ToString(), exception.Message); - } - - [Fact] - public void AddValidation_CanBeSafelyInvokedMultipleTimes() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act and assert - builder.AddValidation(); - builder.AddValidation(); - builder.AddValidation(); - } - } -} diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationProviderTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationProviderTests.cs deleted file mode 100644 index 613fa6254..000000000 --- a/test/OpenIddict.Validation.Tests/OpenIddictValidationProviderTests.cs +++ /dev/null @@ -1,738 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Security.Claims; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using AspNet.Security.OAuth.Validation; -using AspNet.Security.OpenIdConnect.Extensions; -using Microsoft.AspNetCore.Authentication; -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.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OpenIddict.Abstractions; -using OpenIddict.Core; -using Xunit; - -namespace OpenIddict.Validation.Tests -{ - public class OpenIddictValidationProviderTests - { - [Fact] - public async Task DecryptToken_ThrowsAnExceptionWhenTokenManagerIsNotRegistered() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Services.RemoveAll(typeof(IOpenIddictTokenManager)); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-reference-token-id"); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.SendAsync(request); - }); - - Assert.Equal(new StringBuilder() - .AppendLine("The core services must be registered when enabling reference tokens support.") - .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") - .Append("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") - .ToString(), exception.Message); - } - - [Fact] - public async Task DecryptToken_ReturnsFailedResultForUnknownReferenceToken() - { - // Arrange - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("invalid-reference-token-id", It.IsAny())) - .ReturnsAsync(value: null); - }); - - var server = CreateResourceServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "invalid-reference-token-id"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("invalid-reference-token-id", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task DecryptToken_ReturnsFailedResultForMissingTokenType() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(result: null)); - }); - - var server = CreateResourceServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-reference-token-id"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task DecryptToken_ReturnsFailedResultForIncompatibleTokenType() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.TokenTypes.RefreshToken)); - }); - - var server = CreateResourceServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-reference-token-id"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task DecryptToken_ReturnsFailedResultForNonReferenceToken() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.TokenTypes.AccessToken)); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask(result: null)); - }); - - var server = CreateResourceServer(builder => - { - builder.Services.AddSingleton(manager); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-reference-token-id"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task DecryptToken_ReturnsFailedResultForInvalidReferenceTokenPayload() - { - // Arrange - var token = new OpenIddictToken(); - - var format = new Mock>(); - format.Setup(mock => mock.Unprotect("invalid-reference-token-payload")) - .Returns(value: null); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.TokenTypes.AccessToken)); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("invalid-reference-token-payload")); - }); - - var server = CreateResourceServer(builder => - { - builder.Services.AddSingleton(manager); - builder.Configure(options => options.AccessTokenFormat = format.Object); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-reference-token-id"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny()), Times.Once()); - format.Verify(mock => mock.Unprotect("invalid-reference-token-payload"), Times.Once()); - } - - [Fact] - public async Task ValidateToken_ReturnsFailedResultForInvalidReferenceToken() - { - // Arrange - var token = new OpenIddictToken(); - - var format = new Mock>(); - format.Setup(mock => mock.Unprotect("valid-reference-token-payload")) - .Returns(delegate - { - var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); - - return new AuthenticationTicket( - new ClaimsPrincipal(identity), - OpenIddictValidationDefaults.AuthenticationScheme); - }); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.TokenTypes.AccessToken)); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("valid-reference-token-payload")); - - instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2018, 01, 01, 00, 00, 00, TimeSpan.Zero))); - - instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2918, 01, 01, 00, 00, 00, TimeSpan.Zero))); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("4392E01A-1BC4-4776-8450-EC267C2B708A")); - - instance.Setup(mock => mock.FindByIdAsync("4392E01A-1BC4-4776-8450-EC267C2B708A", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateResourceServer(builder => - { - builder.Services.AddSingleton(manager); - builder.Configure(options => options.AccessTokenFormat = format.Object); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/ticket"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-reference-token-id"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetCreationDateAsync(token, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetExpirationDateAsync(token, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); - format.Verify(mock => mock.Unprotect("valid-reference-token-payload"), Times.Once()); - } - - [Fact] - public async Task ValidateToken_ReturnsValidResultForValidReferenceToken() - { - // Arrange - var token = new OpenIddictToken(); - - var format = new Mock>(); - format.Setup(mock => mock.Unprotect("valid-reference-token-payload")) - .Returns(delegate - { - var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); - - return new AuthenticationTicket( - new ClaimsPrincipal(identity), - OpenIddictValidationDefaults.AuthenticationScheme); - }); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) - .Returns(new ValueTask(OpenIddictConstants.TokenTypes.AccessToken)); - - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("valid-reference-token-payload")); - - instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2018, 01, 01, 00, 00, 00, TimeSpan.Zero))); - - instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) - .Returns(new ValueTask(new DateTimeOffset(2918, 01, 01, 00, 00, 00, TimeSpan.Zero))); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .Returns(new ValueTask("4392E01A-1BC4-4776-8450-EC267C2B708A")); - - instance.Setup(mock => mock.FindByIdAsync("4392E01A-1BC4-4776-8450-EC267C2B708A", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateResourceServer(builder => - { - builder.Services.AddSingleton(manager); - builder.Configure(options => options.AccessTokenFormat = format.Object); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/ticket"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-reference-token-id"); - - // Act - var response = await client.SendAsync(request); - - var ticket = JObject.Parse(await response.Content.ReadAsStringAsync()); - var properties = (from property in ticket.Value("Properties") - select new - { - Name = property.Value("Name"), - Value = property.Value("Value") - }).ToDictionary(property => property.Name, property => property.Value); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - Assert.Equal( - new DateTimeOffset(2018, 01, 01, 00, 00, 00, TimeSpan.Zero), - DateTimeOffset.Parse(properties[".issued"], CultureInfo.InvariantCulture)); - Assert.Equal( - new DateTimeOffset(2918, 01, 01, 00, 00, 00, TimeSpan.Zero), - DateTimeOffset.Parse(properties[".expires"], CultureInfo.InvariantCulture)); - - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetCreationDateAsync(token, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetExpirationDateAsync(token, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); - format.Verify(mock => mock.Unprotect("valid-reference-token-payload"), Times.Once()); - } - - [Fact] - public async Task ValidateToken_ThrowsAnExceptionWhenAuthorizationManagerIsNotRegistered() - { - // Arrange - var format = new Mock>(); - format.Setup(mock => mock.Unprotect("valid-token")) - .Returns(delegate - { - var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); - - return new AuthenticationTicket( - new ClaimsPrincipal(identity), - OpenIddictValidationDefaults.AuthenticationScheme); - }); - - var server = CreateResourceServer(builder => - { - builder.Services.RemoveAll(typeof(IOpenIddictAuthorizationManager)); - - builder.EnableAuthorizationValidation(); - - builder.Configure(options => - { - options.AccessTokenFormat = format.Object; - options.UseReferenceTokens = false; - }); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.SendAsync(request); - }); - - Assert.Equal(new StringBuilder() - .AppendLine("The core services must be registered when enabling authorization validation.") - .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") - .Append("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") - .ToString(), exception.Message); - } - - [Fact] - public async Task ValidateToken_ReturnsFailedResultForUnknownAuthorization() - { - // Arrange - var format = new Mock>(); - format.Setup(mock => mock.Unprotect("valid-token")) - .Returns(delegate - { - var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - OpenIddictValidationDefaults.AuthenticationScheme); - - ticket.SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, "5230CBAD-89F9-4C3F-B48C-9253B6FB8620"); - - return ticket; - }); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny())) - .ReturnsAsync(value: null); - }); - - var server = CreateResourceServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.EnableAuthorizationValidation(); - - builder.Configure(options => - { - options.AccessTokenFormat = format.Object; - options.UseReferenceTokens = false; - }); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateToken_ReturnsFailedResultForInvalidAuthorization() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var format = new Mock>(); - format.Setup(mock => mock.Unprotect("valid-token")) - .Returns(delegate - { - var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - OpenIddictValidationDefaults.AuthenticationScheme); - - ticket.SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, "5230CBAD-89F9-4C3F-B48C-9253B6FB8620"); - - return ticket; - }); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny())) - .ReturnsAsync(authorization); - - instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) - .ReturnsAsync(false); - }); - - var server = CreateResourceServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.EnableAuthorizationValidation(); - - builder.Configure(options => - { - options.AccessTokenFormat = format.Object; - options.UseReferenceTokens = false; - }); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ValidateToken_ReturnsValidResultForValidAuthorization() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var format = new Mock>(); - format.Setup(mock => mock.Unprotect("valid-token")) - .Returns(delegate - { - var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - OpenIddictValidationDefaults.AuthenticationScheme); - - ticket.SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, "5230CBAD-89F9-4C3F-B48C-9253B6FB8620"); - - return ticket; - }); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny())) - .ReturnsAsync(authorization); - - instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateResourceServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.EnableAuthorizationValidation(); - - builder.Configure(options => - { - options.AccessTokenFormat = format.Object; - options.UseReferenceTokens = false; - }); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny()), Times.Once()); - } - - private static TestServer CreateResourceServer(Action configuration = null) - { - var builder = new WebHostBuilder(); - builder.UseEnvironment("Testing"); - - builder.ConfigureLogging(options => options.AddDebug()); - - builder.ConfigureServices(services => - { - services.AddOpenIddict() - .AddCore(options => - { - options.SetDefaultAuthorizationEntity(); - options.SetDefaultTokenEntity(); - options.Services.AddSingleton(CreateTokenManager()); - }) - - .AddValidation(options => - { - options.UseReferenceTokens(); - - // Note: overriding the default data protection provider is not necessary for the tests to pass, - // but is useful to ensure unnecessary keys are not persisted in testing environments, which also - // helps make the unit tests run faster, as no registry or disk access is required in this case. - options.UseDataProtectionProvider(new EphemeralDataProtectionProvider()); - - // Run the configuration delegate - // registered by the unit tests. - configuration?.Invoke(options); - }); - }); - - builder.Configure(app => - { - app.Map("/ticket", map => map.Run(async context => - { - var result = await context.AuthenticateAsync(OpenIddictValidationDefaults.AuthenticationScheme); - if (result.Principal == null) - { - await context.ChallengeAsync(OpenIddictValidationDefaults.AuthenticationScheme); - - return; - } - - context.Response.ContentType = "application/json"; - - // Return the authentication ticket as a JSON object. - await context.Response.WriteAsync(JsonConvert.SerializeObject(new - { - Claims = from claim in result.Principal.Claims - select new { claim.Type, claim.Value }, - - Properties = from property in result.Properties.Items - select new { Name = property.Key, property.Value } - })); - })); - - app.Run(async context => - { - var result = await context.AuthenticateAsync(OpenIddictValidationDefaults.AuthenticationScheme); - if (result.Principal == null) - { - await context.ChallengeAsync(OpenIddictValidationDefaults.AuthenticationScheme); - - return; - } - - var subject = result.Principal.FindFirst(OAuthValidationConstants.Claims.Subject)?.Value; - if (string.IsNullOrEmpty(subject)) - { - await context.ChallengeAsync(OpenIddictValidationDefaults.AuthenticationScheme); - - return; - } - - await context.Response.WriteAsync(subject); - }); - }); - - return new TestServer(builder); - } - - private static OpenIddictAuthorizationManager CreateAuthorizationManager( - Action>> configuration = null) - { - var manager = new Mock>( - Mock.Of>(), - Mock.Of(), - Mock.Of>>(), - Mock.Of>(mock => - mock.CurrentValue == new OpenIddictCoreOptions())); - - configuration?.Invoke(manager); - - return manager.Object; - } - - private static OpenIddictTokenManager CreateTokenManager( - Action>> configuration = null) - { - var manager = new Mock>( - Mock.Of>(), - Mock.Of(), - Mock.Of>>(), - Mock.Of>(mock => - mock.CurrentValue == new OpenIddictCoreOptions())); - - configuration?.Invoke(manager); - - return manager.Object; - } - - public class OpenIddictAuthorization { } - - public class OpenIddictToken { } - } -}