From 3bf51e1600080c9d7b180ff728896af9c9d414c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sat, 28 Sep 2019 19:26:11 +0200 Subject: [PATCH] Introduce the first bits of the OpenIddict client --- OpenIddict.sln | 25 +- Packages.props | 5 +- .../Controllers/AuthenticationController.cs | 26 - sandbox/Mvc.Client/Startup.cs | 79 - sandbox/Mvc.Server/Views/_ViewImports.cshtml | 8 - .../Controllers/AuthenticationController.cs | 139 + .../Controllers/ErrorController.cs | 32 + .../Controllers/HomeController.cs | 12 +- ...enIddict.Sandbox.AspNetCore.Client.csproj} | 4 + .../Program.cs | 2 +- .../Properties/launchSettings.json | 2 +- .../Startup.cs | 80 + .../ViewModels/Shared/ErrorViewModel.cs | 2 +- .../Views/Shared/Error.cshtml | 0 .../Views/Shared/Home.cshtml | 0 .../Views/Shared/_Layout.cshtml | 4 +- .../Views/_ViewImports.cshtml | 3 + .../Views/_ViewStart.cshtml | 0 .../appsettings.Development.json | 0 .../appsettings.json | 0 .../web.config | 24 + .../fonts/glyphicons-halflings-regular.eot | Bin .../fonts/glyphicons-halflings-regular.svg | 0 .../fonts/glyphicons-halflings-regular.ttf | Bin .../fonts/glyphicons-halflings-regular.woff | Bin .../wwwroot/scripts/bootstrap.js | 0 .../wwwroot/scripts/bootstrap.min.js | 0 .../scripts/jquery-1.9.0.intellisense.js | 0 .../wwwroot/scripts/jquery-1.9.0.js | 0 .../wwwroot/scripts/jquery-1.9.0.min.js | 0 .../wwwroot/scripts/jquery-1.9.0.min.map | 0 .../wwwroot/stylesheets/bootstrap-theme.css | 0 .../stylesheets/bootstrap-theme.css.map | 0 .../stylesheets/bootstrap-theme.min.css | 0 .../wwwroot/stylesheets/bootstrap.css | 0 .../wwwroot/stylesheets/bootstrap.css.map | 0 .../wwwroot/stylesheets/bootstrap.min.css | 0 .../wwwroot/stylesheets/jumbotron-narrow.css | 0 .../.bowerrc | 0 .../Controllers/AccountController.cs | 8 +- .../Controllers/AuthorizationController.cs | 8 +- .../Controllers/ErrorController.cs | 4 +- .../Controllers/HomeController.cs | 2 +- .../Controllers/ManageController.cs | 8 +- .../Controllers/ResourceController.cs | 4 +- .../Controllers/UserinfoController.cs | 4 +- .../Helpers/AsyncEnumerableExtensions.cs | 2 +- .../Helpers/FormValueRequiredAttribute.cs | 2 +- .../Models/ApplicationDbContext.cs | 2 +- .../Models/ApplicationUser.cs | 2 +- ...enIddict.Sandbox.AspNetCore.Server.csproj} | 0 .../Program.cs | 2 +- .../Properties/launchSettings.json | 2 +- .../Services/IEmailSender.cs | 2 +- .../Services/ISmsSender.cs | 2 +- .../Services/MessageServices.cs | 2 +- .../Startup.cs | 6 +- .../ExternalLoginConfirmationViewModel.cs | 2 +- .../Account/ForgotPasswordViewModel.cs | 2 +- .../ViewModels/Account/LoginViewModel.cs | 2 +- .../ViewModels/Account/RegisterViewModel.cs | 2 +- .../Account/ResetPasswordViewModel.cs | 2 +- .../ViewModels/Account/SendCodeViewModel.cs | 2 +- .../ViewModels/Account/VerifyCodeViewModel.cs | 2 +- .../Authorization/AuthorizeViewModel.cs | 2 +- .../Authorization/VerifyViewModel.cs | 2 +- .../Manage/AddPhoneNumberViewModel.cs | 2 +- .../Manage/ChangePasswordViewModel.cs | 2 +- .../Manage/ConfigureTwoFactorViewModel.cs | 2 +- .../ViewModels/Manage/FactorViewModel.cs | 2 +- .../ViewModels/Manage/IndexViewModel.cs | 2 +- .../Manage/ManageLoginsViewModel.cs | 2 +- .../ViewModels/Manage/RemoveLoginViewModel.cs | 2 +- .../ViewModels/Manage/SetPasswordViewModel.cs | 2 +- .../Manage/VerifyPhoneNumberViewModel.cs | 2 +- .../ViewModels/Shared/ErrorViewModel.cs | 12 + .../Views/Account/ConfirmEmail.cshtml | 0 .../Account/ExternalLoginConfirmation.cshtml | 0 .../Views/Account/ExternalLoginFailure.cshtml | 0 .../Views/Account/ForgotPassword.cshtml | 0 .../Account/ForgotPasswordConfirmation.cshtml | 0 .../Views/Account/Lockout.cshtml | 0 .../Views/Account/Login.cshtml | 0 .../Views/Account/Register.cshtml | 0 .../Views/Account/ResetPassword.cshtml | 0 .../Account/ResetPasswordConfirmation.cshtml | 0 .../Views/Account/SendCode.cshtml | 0 .../Views/Account/VerifyCode.cshtml | 0 .../Views/Authorization/Authorize.cshtml | 0 .../Views/Authorization/Logout.cshtml | 0 .../Views/Authorization/Verify.cshtml | 0 .../Views/Home/About.cshtml | 0 .../Views/Home/Contact.cshtml | 0 .../Views/Home/Index.cshtml | 0 .../Views/Manage/AddPhoneNumber.cshtml | 0 .../Views/Manage/ChangePassword.cshtml | 0 .../Views/Manage/Index.cshtml | 0 .../Views/Manage/ManageLogins.cshtml | 0 .../Views/Manage/RemoveLogin.cshtml | 0 .../Views/Manage/SetPassword.cshtml | 0 .../Views/Manage/VerifyPhoneNumber.cshtml | 0 .../Views/Shared/Error.cshtml | 14 + .../Views/Shared/_Layout.cshtml | 6 +- .../Views/Shared/_LoginPartial.cshtml | 2 +- .../Shared/_ValidationScriptsPartial.cshtml | 0 .../Views/_ViewImports.cshtml | 8 + .../Views/_ViewStart.cshtml | 0 .../Worker.cs | 4 +- .../appsettings.Development.json | 0 .../appsettings.json | 0 .../bower.json | 0 .../gulpfile.js | 0 .../package.json | 0 .../web.config | 24 + .../wwwroot/css/site.css | 0 .../wwwroot/favicon.ico | Bin .../wwwroot/images/ASP-NET-Banners-01.png | Bin .../wwwroot/images/ASP-NET-Banners-02.png | Bin .../wwwroot/images/Banner-01-Azure.png | Bin .../wwwroot/images/Banner-02-VS.png | Bin .../wwwroot/js/site.js | 0 .../lib/bootstrap-touch-carousel/.bower.json | 0 .../lib/bootstrap-touch-carousel/CHANGELOG.md | 0 .../lib/bootstrap-touch-carousel/Gruntfile.js | 0 .../lib/bootstrap-touch-carousel/LICENSE-MIT | 0 .../lib/bootstrap-touch-carousel/README.md | 0 .../lib/bootstrap-touch-carousel/bower.json | 0 .../dist/css/bootstrap-touch-carousel.css | 0 .../dist/js/bootstrap-touch-carousel.js | 0 .../lib/bootstrap-touch-carousel/package.json | 0 .../src/js/touch-carousel.js | 0 .../src/js/transition.js | 0 .../src/less/carousel.less | 0 .../src/less/elements.less | 0 .../wwwroot/lib/bootstrap/.bower.json | 0 .../wwwroot/lib/bootstrap/CNAME | 0 .../wwwroot/lib/bootstrap/CONTRIBUTING.md | 0 .../wwwroot/lib/bootstrap/Gruntfile.js | 0 .../wwwroot/lib/bootstrap/LICENSE | 0 .../wwwroot/lib/bootstrap/README.md | 0 .../wwwroot/lib/bootstrap/_config.yml | 0 .../wwwroot/lib/bootstrap/_includes/ads.html | 0 .../lib/bootstrap/_includes/footer.html | 0 .../lib/bootstrap/_includes/header.html | 0 .../bootstrap/_includes/nav-components.html | 0 .../lib/bootstrap/_includes/nav-css.html | 0 .../bootstrap/_includes/nav-customize.html | 0 .../_includes/nav-getting-started.html | 0 .../bootstrap/_includes/nav-javascript.html | 0 .../lib/bootstrap/_includes/nav-main.html | 0 .../lib/bootstrap/_includes/old-bs-docs.html | 0 .../bootstrap/_includes/social-buttons.html | 0 .../lib/bootstrap/_layouts/default.html | 0 .../wwwroot/lib/bootstrap/_layouts/home.html | 0 .../wwwroot/lib/bootstrap/assets/css/docs.css | 0 .../bootstrap/assets/css/pygments-manni.css | 0 .../ico/apple-touch-icon-114-precomposed.png | Bin .../ico/apple-touch-icon-144-precomposed.png | Bin .../ico/apple-touch-icon-57-precomposed.png | Bin .../ico/apple-touch-icon-72-precomposed.png | Bin .../lib/bootstrap/assets/ico/favicon.png | Bin .../lib/bootstrap/assets/js/application.js | 0 .../lib/bootstrap/assets/js/customizer.js | 0 .../lib/bootstrap/assets/js/filesaver.js | 0 .../wwwroot/lib/bootstrap/assets/js/holder.js | 0 .../lib/bootstrap/assets/js/html5shiv.js | 0 .../wwwroot/lib/bootstrap/assets/js/jquery.js | 0 .../wwwroot/lib/bootstrap/assets/js/jszip.js | 0 .../wwwroot/lib/bootstrap/assets/js/less.js | 0 .../lib/bootstrap/assets/js/raw-files.js | 0 .../lib/bootstrap/assets/js/respond.min.js | 0 .../wwwroot/lib/bootstrap/assets/js/uglify.js | 0 .../wwwroot/lib/bootstrap/bower.json | 0 .../wwwroot/lib/bootstrap/browserstack.json | 0 .../wwwroot/lib/bootstrap/components.html | 0 .../wwwroot/lib/bootstrap/composer.json | 0 .../wwwroot/lib/bootstrap/css.html | 0 .../wwwroot/lib/bootstrap/customize.html | 0 .../bootstrap/dist/css/bootstrap-theme.css | 0 .../dist/css/bootstrap-theme.min.css | 0 .../lib/bootstrap/dist/css/bootstrap.css | 0 .../lib/bootstrap/dist/css/bootstrap.min.css | 0 .../fonts/glyphicons-halflings-regular.eot | Bin .../fonts/glyphicons-halflings-regular.svg | 0 .../fonts/glyphicons-halflings-regular.ttf | Bin .../fonts/glyphicons-halflings-regular.woff | Bin .../lib/bootstrap/dist/js/bootstrap.js | 0 .../lib/bootstrap/dist/js/bootstrap.min.js | 0 .../bootstrap/examples/carousel/carousel.css | 0 .../bootstrap/examples/carousel/index.html | 0 .../lib/bootstrap/examples/grid/grid.css | 0 .../lib/bootstrap/examples/grid/index.html | 0 .../examples/jumbotron-narrow/index.html | 0 .../jumbotron-narrow/jumbotron-narrow.css | 0 .../bootstrap/examples/jumbotron/index.html | 0 .../examples/jumbotron/jumbotron.css | 0 .../examples/justified-nav/index.html | 0 .../examples/justified-nav/justified-nav.css | 0 .../examples/navbar-fixed-top/index.html | 0 .../navbar-fixed-top/navbar-fixed-top.css | 0 .../examples/navbar-static-top/index.html | 0 .../navbar-static-top/navbar-static-top.css | 0 .../lib/bootstrap/examples/navbar/index.html | 0 .../lib/bootstrap/examples/navbar/navbar.css | 0 .../examples/non-responsive/index.html | 0 .../non-responsive/non-responsive.css | 0 .../bootstrap/examples/offcanvas/index.html | 0 .../examples/offcanvas/offcanvas.css | 0 .../bootstrap/examples/offcanvas/offcanvas.js | 0 .../examples/screenshots/carousel.jpg | Bin .../bootstrap/examples/screenshots/grid.jpg | Bin .../examples/screenshots/jumbotron-narrow.jpg | Bin .../examples/screenshots/jumbotron.jpg | Bin .../examples/screenshots/justified-nav.jpg | Bin .../examples/screenshots/navbar-fixed.jpg | Bin .../examples/screenshots/navbar-static.jpg | Bin .../bootstrap/examples/screenshots/navbar.jpg | Bin .../examples/screenshots/non-responsive.jpg | Bin .../examples/screenshots/offcanvas.jpg | Bin .../examples/screenshots/sign-in.jpg | Bin .../examples/screenshots/starter-template.jpg | Bin .../screenshots/sticky-footer-navbar.jpg | Bin .../examples/screenshots/sticky-footer.jpg | Bin .../bootstrap/examples/screenshots/theme.jpg | Bin .../lib/bootstrap/examples/signin/index.html | 0 .../lib/bootstrap/examples/signin/signin.css | 0 .../examples/starter-template/index.html | 0 .../starter-template/starter-template.css | 0 .../examples/sticky-footer-navbar/index.html | 0 .../sticky-footer-navbar.css | 0 .../examples/sticky-footer/index.html | 0 .../examples/sticky-footer/sticky-footer.css | 0 .../lib/bootstrap/examples/theme/index.html | 0 .../lib/bootstrap/examples/theme/theme.css | 0 .../fonts/glyphicons-halflings-regular.eot | Bin .../fonts/glyphicons-halflings-regular.svg | 0 .../fonts/glyphicons-halflings-regular.ttf | Bin .../fonts/glyphicons-halflings-regular.woff | Bin .../lib/bootstrap/getting-started.html | 0 .../wwwroot/lib/bootstrap/index.html | 0 .../wwwroot/lib/bootstrap/javascript.html | 0 .../wwwroot/lib/bootstrap/js/affix.js | 0 .../wwwroot/lib/bootstrap/js/alert.js | 0 .../wwwroot/lib/bootstrap/js/button.js | 0 .../wwwroot/lib/bootstrap/js/carousel.js | 0 .../wwwroot/lib/bootstrap/js/collapse.js | 0 .../wwwroot/lib/bootstrap/js/dropdown.js | 0 .../wwwroot/lib/bootstrap/js/modal.js | 0 .../wwwroot/lib/bootstrap/js/popover.js | 0 .../wwwroot/lib/bootstrap/js/scrollspy.js | 0 .../wwwroot/lib/bootstrap/js/tab.js | 0 .../wwwroot/lib/bootstrap/js/tests/index.html | 0 .../wwwroot/lib/bootstrap/js/tests/phantom.js | 0 .../wwwroot/lib/bootstrap/js/tests/server.js | 0 .../lib/bootstrap/js/tests/unit/affix.js | 0 .../lib/bootstrap/js/tests/unit/alert.js | 0 .../lib/bootstrap/js/tests/unit/button.js | 0 .../lib/bootstrap/js/tests/unit/carousel.js | 0 .../lib/bootstrap/js/tests/unit/collapse.js | 0 .../lib/bootstrap/js/tests/unit/dropdown.js | 0 .../lib/bootstrap/js/tests/unit/modal.js | 0 .../lib/bootstrap/js/tests/unit/phantom.js | 0 .../lib/bootstrap/js/tests/unit/popover.js | 0 .../lib/bootstrap/js/tests/unit/scrollspy.js | 0 .../lib/bootstrap/js/tests/unit/tab.js | 0 .../lib/bootstrap/js/tests/unit/tooltip.js | 0 .../lib/bootstrap/js/tests/unit/transition.js | 0 .../lib/bootstrap/js/tests/vendor/jquery.js | 0 .../lib/bootstrap/js/tests/vendor/qunit.css | 0 .../lib/bootstrap/js/tests/vendor/qunit.js | 0 .../wwwroot/lib/bootstrap/js/tooltip.js | 0 .../wwwroot/lib/bootstrap/js/transition.js | 0 .../wwwroot/lib/bootstrap/less/alerts.less | 0 .../wwwroot/lib/bootstrap/less/badges.less | 0 .../wwwroot/lib/bootstrap/less/bootstrap.less | 0 .../lib/bootstrap/less/breadcrumbs.less | 0 .../lib/bootstrap/less/button-groups.less | 0 .../wwwroot/lib/bootstrap/less/buttons.less | 0 .../wwwroot/lib/bootstrap/less/carousel.less | 0 .../wwwroot/lib/bootstrap/less/close.less | 0 .../wwwroot/lib/bootstrap/less/code.less | 0 .../bootstrap/less/component-animations.less | 0 .../wwwroot/lib/bootstrap/less/dropdowns.less | 0 .../wwwroot/lib/bootstrap/less/forms.less | 0 .../lib/bootstrap/less/glyphicons.less | 0 .../wwwroot/lib/bootstrap/less/grid.less | 0 .../lib/bootstrap/less/input-groups.less | 0 .../wwwroot/lib/bootstrap/less/jumbotron.less | 0 .../wwwroot/lib/bootstrap/less/labels.less | 0 .../lib/bootstrap/less/list-group.less | 0 .../wwwroot/lib/bootstrap/less/media.less | 0 .../wwwroot/lib/bootstrap/less/mixins.less | 0 .../wwwroot/lib/bootstrap/less/modals.less | 0 .../wwwroot/lib/bootstrap/less/navbar.less | 0 .../wwwroot/lib/bootstrap/less/navs.less | 0 .../wwwroot/lib/bootstrap/less/normalize.less | 0 .../wwwroot/lib/bootstrap/less/pager.less | 0 .../lib/bootstrap/less/pagination.less | 0 .../wwwroot/lib/bootstrap/less/panels.less | 0 .../wwwroot/lib/bootstrap/less/popovers.less | 0 .../wwwroot/lib/bootstrap/less/print.less | 0 .../lib/bootstrap/less/progress-bars.less | 0 .../bootstrap/less/responsive-utilities.less | 0 .../lib/bootstrap/less/scaffolding.less | 0 .../wwwroot/lib/bootstrap/less/tables.less | 0 .../wwwroot/lib/bootstrap/less/theme.less | 0 .../lib/bootstrap/less/thumbnails.less | 0 .../wwwroot/lib/bootstrap/less/tooltip.less | 0 .../wwwroot/lib/bootstrap/less/type.less | 0 .../wwwroot/lib/bootstrap/less/utilities.less | 0 .../wwwroot/lib/bootstrap/less/variables.less | 0 .../wwwroot/lib/bootstrap/less/wells.less | 0 .../wwwroot/lib/bootstrap/package.json | 0 .../wwwroot/lib/hammer.js/.bower.json | 0 .../wwwroot/lib/hammer.js/.bowerrc | 0 .../wwwroot/lib/hammer.js/.gitignore | 0 .../wwwroot/lib/hammer.js/.jscsrc | 0 .../wwwroot/lib/hammer.js/.jshintrc | 0 .../wwwroot/lib/hammer.js/.travis.yml | 0 .../wwwroot/lib/hammer.js/CHANGELOG.md | 0 .../wwwroot/lib/hammer.js/CONTRIBUTING.md | 0 .../wwwroot/lib/hammer.js/Gruntfile.coffee | 0 .../wwwroot/lib/hammer.js/LICENSE.md | 0 .../wwwroot/lib/hammer.js/README.md | 0 .../wwwroot/lib/hammer.js/bower.json | 0 .../wwwroot/lib/hammer.js/component.json | 0 .../wwwroot/lib/hammer.js/hammer.js | 0 .../wwwroot/lib/hammer.js/hammer.min.js | 0 .../wwwroot/lib/hammer.js/hammer.min.map | 0 .../wwwroot/lib/hammer.js/package.json | 0 .../jquery-validation-unobtrusive/.bower.json | 0 .../jquery-validation-unobtrusive/.gitignore | 0 .../CONTRIBUTING.md | 0 .../jquery-validation-unobtrusive/LICENSE.txt | 0 .../jquery-validation-unobtrusive/README.md | 0 .../jquery-validation-unobtrusive/bower.json | 0 .../jquery.validate.unobtrusive.js | 0 .../jquery.validate.unobtrusive.min.js | 0 .../wwwroot/lib/jquery-validation/.bower.json | 0 .../lib/jquery-validation/.gitattributes | 0 .../wwwroot/lib/jquery-validation/.gitignore | 0 .../wwwroot/lib/jquery-validation/.travis.yml | 0 .../lib/jquery-validation/CONTRIBUTING.md | 0 .../lib/jquery-validation/Gruntfile.js | 0 .../wwwroot/lib/jquery-validation/README.md | 0 .../jquery-validation/additional-methods.js | 0 .../lib/jquery-validation/changelog.txt | 0 .../demo/ajaxSubmit-integration-demo.html | 0 .../jquery-validation/demo/captcha/captcha.js | 0 .../demo/captcha/fonts/Anorexia.ttf | Bin .../demo/captcha/image_req.php | 0 .../demo/captcha/images/.htaccess | 0 .../demo/captcha/images/button.png | Bin .../demo/captcha/images/image.php | 0 .../jquery-validation/demo/captcha/index.php | 0 .../demo/captcha/newsession.php | 0 .../demo/captcha/process.php | 0 .../jquery-validation/demo/captcha/rand.php | 0 .../jquery-validation/demo/captcha/style.css | 0 .../jquery-validation/demo/css/cmxform.css | 0 .../demo/css/cmxformTemplate.css | 0 .../lib/jquery-validation/demo/css/core.css | 0 .../lib/jquery-validation/demo/css/reset.css | 0 .../lib/jquery-validation/demo/css/screen.css | 0 .../demo/custom-messages-data-demo.html | 0 .../demo/custom-methods-demo.html | 0 .../demo/dynamic-totals.html | 0 .../demo/errorcontainer-demo.html | 0 .../jquery-validation/demo/file_input.html | 0 .../lib/jquery-validation/demo/images/bg.gif | Bin .../jquery-validation/demo/images/checked.gif | Bin .../demo/images/cmxform-divider.gif | Bin .../demo/images/cmxform-fieldset.gif | Bin .../jquery-validation/demo/images/loading.gif | Bin .../demo/images/unchecked.gif | Bin .../lib/jquery-validation/demo/index.html | 0 .../jquery-validation/demo/jquerymobile.html | 0 .../demo/login/images/bg.gif | Bin .../demo/login/images/header1.jpg | Bin .../demo/login/images/page.gif | Bin .../demo/login/images/required_star.gif | Bin .../jquery-validation/demo/login/index.html | 0 .../jquery-validation/demo/login/screen.css | 0 .../demo/marketo/images/backRequiredGray.gif | Bin .../demo/marketo/images/back_green-fade.gif | Bin .../demo/marketo/images/back_nav_blue.gif | Bin .../demo/marketo/images/blank.gif | Bin .../demo/marketo/images/button-submit.gif | Bin .../demo/marketo/images/favicon.ico | Bin .../demo/marketo/images/help.png | Bin .../marketo/images/left-nav-callout-long.png | Bin .../demo/marketo/images/login-sprite.gif | Bin .../demo/marketo/images/logo_marketo.gif | Bin .../demo/marketo/images/sf.png | Bin .../demo/marketo/images/step1-24.gif | Bin .../demo/marketo/images/step2-24.gif | Bin .../demo/marketo/images/step3-24.gif | Bin .../demo/marketo/images/tab-sprite.gif | Bin .../demo/marketo/images/tab_green.gif | Bin .../demo/marketo/images/time.png | Bin .../demo/marketo/images/toggle.gif | Bin .../demo/marketo/images/warning.gif | Bin .../jquery-validation/demo/marketo/index.html | 0 .../demo/marketo/jquery.maskedinput.js | 0 .../demo/marketo/mktSignup.js | 0 .../jquery-validation/demo/marketo/step2.htm | 0 .../demo/marketo/stylesheet.css | 0 .../lib/jquery-validation/demo/milk/bg.gif | Bin .../jquery-validation/demo/milk/index.html | 0 .../demo/milk/left_white.png | Bin .../lib/jquery-validation/demo/milk/milk.css | 0 .../lib/jquery-validation/demo/milk/milk.png | Bin .../demo/milk/right_white.png | Bin .../demo/multipart/index.html | 0 .../multipart/js/jquery.maskedinput-1.0.js | 0 .../demo/multipart/js/ui.accordion.js | 0 .../demo/multipart/js/ui.core.js | 0 .../demo/multipart/style.css | 0 .../demo/radio-checkbox-select-demo.html | 0 .../jquery-validation/demo/tabs/index.html | 0 .../jquery-validation/demo/themerollered.html | 0 .../jquery-validation/demo/tinymce/index.html | 0 .../tinymce/themes/simple/editor_template.js | 0 .../demo/tinymce/themes/simple/img/icons.gif | Bin .../demo/tinymce/themes/simple/langs/en.js | 0 .../themes/simple/skins/default/ui.css | 0 .../demo/tinymce/tiny_mce.js | 0 .../lib/jquery-validation/jquery.validate.js | 0 .../lib/jquery-validation/lib/jquery-1.6.4.js | 0 .../lib/jquery-validation/lib/jquery-1.7.2.js | 0 .../lib/jquery-validation/lib/jquery-1.8.3.js | 0 .../lib/jquery-validation/lib/jquery-1.9.0.js | 0 .../lib/jquery-validation/lib/jquery.form.js | 0 .../lib/jquery-validation/lib/jquery.js | 0 .../jquery-validation/lib/jquery.mockjax.js | 0 .../localization/messages_ar.js | 0 .../localization/messages_bg.js | 0 .../localization/messages_ca.js | 0 .../localization/messages_cs.js | 0 .../localization/messages_da.js | 0 .../localization/messages_de.js | 0 .../localization/messages_el.js | 0 .../localization/messages_es.js | 0 .../localization/messages_et.js | 0 .../localization/messages_eu.js | 0 .../localization/messages_fa.js | 0 .../localization/messages_fi.js | 0 .../localization/messages_fr.js | 0 .../localization/messages_he.js | 0 .../localization/messages_hr.js | 0 .../localization/messages_hu.js | 0 .../localization/messages_it.js | 0 .../localization/messages_ja.js | 0 .../localization/messages_ka.js | 0 .../localization/messages_kk.js | 0 .../localization/messages_ko.js | 0 .../localization/messages_lt.js | 0 .../localization/messages_lv.js | 0 .../localization/messages_my.js | 0 .../localization/messages_nl.js | 0 .../localization/messages_no.js | 0 .../localization/messages_pl.js | 0 .../localization/messages_pt_BR.js | 0 .../localization/messages_pt_PT.js | 0 .../localization/messages_ro.js | 0 .../localization/messages_ru.js | 0 .../localization/messages_si.js | 0 .../localization/messages_sk.js | 0 .../localization/messages_sl.js | 0 .../localization/messages_sr.js | 0 .../localization/messages_sv.js | 0 .../localization/messages_th.js | 0 .../localization/messages_tr.js | 0 .../localization/messages_uk.js | 0 .../localization/messages_vi.js | 0 .../localization/messages_zh.js | 0 .../localization/messages_zh_TW.js | 0 .../localization/methods_de.js | 0 .../localization/methods_nl.js | 0 .../localization/methods_pt.js | 0 .../lib/jquery-validation/package.json | 0 .../lib/jquery-validation/test/events.html | 0 .../test/firebug/errorIcon.png | Bin .../test/firebug/firebug.css | 0 .../test/firebug/firebug.html | 0 .../jquery-validation/test/firebug/firebug.js | 0 .../test/firebug/firebugx.js | 0 .../test/firebug/infoIcon.png | Bin .../test/firebug/warningIcon.png | Bin .../lib/jquery-validation/test/index.html | 0 .../lib/jquery-validation/test/jquery.js | 0 .../lib/jquery-validation/test/large.html | 0 .../lib/jquery-validation/test/messages.js | 0 .../lib/jquery-validation/test/methods.js | 0 .../jquery-validation/test/qunit/qunit.css | 0 .../lib/jquery-validation/test/qunit/qunit.js | 0 .../lib/jquery-validation/test/rules.js | 0 .../jquery-validation/test/selects/index.html | 0 .../lib/jquery-validation/test/tabs.html | 0 .../lib/jquery-validation/test/test.js | 0 .../wwwroot/lib/jquery-validation/todo | 0 .../jquery-validation/validation.jquery.json | 0 .../wwwroot/lib/jquery/.bower.json | 0 .../wwwroot/lib/jquery/MIT-LICENSE.txt | 0 .../wwwroot/lib/jquery/bower.json | 0 .../wwwroot/lib/jquery/dist/jquery.js | 0 .../wwwroot/lib/jquery/dist/jquery.min.js | 0 .../wwwroot/lib/jquery/dist/jquery.min.map | 0 .../wwwroot/lib/jquery/src/ajax.js | 0 .../wwwroot/lib/jquery/src/ajax/jsonp.js | 0 .../wwwroot/lib/jquery/src/ajax/load.js | 0 .../wwwroot/lib/jquery/src/ajax/parseJSON.js | 0 .../wwwroot/lib/jquery/src/ajax/parseXML.js | 0 .../wwwroot/lib/jquery/src/ajax/script.js | 0 .../wwwroot/lib/jquery/src/ajax/var/nonce.js | 0 .../wwwroot/lib/jquery/src/ajax/var/rquery.js | 0 .../wwwroot/lib/jquery/src/ajax/xhr.js | 0 .../wwwroot/lib/jquery/src/attributes.js | 0 .../wwwroot/lib/jquery/src/attributes/attr.js | 0 .../lib/jquery/src/attributes/classes.js | 0 .../wwwroot/lib/jquery/src/attributes/prop.js | 0 .../lib/jquery/src/attributes/support.js | 0 .../wwwroot/lib/jquery/src/attributes/val.js | 0 .../wwwroot/lib/jquery/src/callbacks.js | 0 .../wwwroot/lib/jquery/src/core.js | 0 .../wwwroot/lib/jquery/src/core/access.js | 0 .../wwwroot/lib/jquery/src/core/init.js | 0 .../wwwroot/lib/jquery/src/core/parseHTML.js | 0 .../wwwroot/lib/jquery/src/core/ready.js | 0 .../lib/jquery/src/core/var/rsingleTag.js | 0 .../wwwroot/lib/jquery/src/css.js | 0 .../lib/jquery/src/css/addGetHookIf.js | 0 .../wwwroot/lib/jquery/src/css/curCSS.js | 0 .../lib/jquery/src/css/defaultDisplay.js | 0 .../jquery/src/css/hiddenVisibleSelectors.js | 0 .../wwwroot/lib/jquery/src/css/support.js | 0 .../wwwroot/lib/jquery/src/css/swap.js | 0 .../lib/jquery/src/css/var/cssExpand.js | 0 .../lib/jquery/src/css/var/getStyles.js | 0 .../lib/jquery/src/css/var/isHidden.js | 0 .../wwwroot/lib/jquery/src/css/var/rmargin.js | 0 .../lib/jquery/src/css/var/rnumnonpx.js | 0 .../wwwroot/lib/jquery/src/data.js | 0 .../wwwroot/lib/jquery/src/data/Data.js | 0 .../wwwroot/lib/jquery/src/data/accepts.js | 0 .../lib/jquery/src/data/var/data_priv.js | 0 .../lib/jquery/src/data/var/data_user.js | 0 .../wwwroot/lib/jquery/src/deferred.js | 0 .../wwwroot/lib/jquery/src/deprecated.js | 0 .../wwwroot/lib/jquery/src/dimensions.js | 0 .../wwwroot/lib/jquery/src/effects.js | 0 .../wwwroot/lib/jquery/src/effects/Tween.js | 0 .../jquery/src/effects/animatedSelector.js | 0 .../wwwroot/lib/jquery/src/event.js | 0 .../wwwroot/lib/jquery/src/event/ajax.js | 0 .../wwwroot/lib/jquery/src/event/alias.js | 0 .../wwwroot/lib/jquery/src/event/support.js | 0 .../wwwroot/lib/jquery/src/exports/amd.js | 0 .../wwwroot/lib/jquery/src/exports/global.js | 0 .../wwwroot/lib/jquery/src/intro.js | 0 .../wwwroot/lib/jquery/src/jquery.js | 0 .../wwwroot/lib/jquery/src/manipulation.js | 0 .../lib/jquery/src/manipulation/_evalUrl.js | 0 .../lib/jquery/src/manipulation/support.js | 0 .../src/manipulation/var/rcheckableType.js | 0 .../wwwroot/lib/jquery/src/offset.js | 0 .../wwwroot/lib/jquery/src/outro.js | 0 .../wwwroot/lib/jquery/src/queue.js | 0 .../wwwroot/lib/jquery/src/queue/delay.js | 0 .../wwwroot/lib/jquery/src/selector-native.js | 0 .../wwwroot/lib/jquery/src/selector-sizzle.js | 0 .../wwwroot/lib/jquery/src/selector.js | 0 .../wwwroot/lib/jquery/src/serialize.js | 0 .../lib/jquery/src/sizzle/dist/sizzle.js | 0 .../lib/jquery/src/sizzle/dist/sizzle.min.js | 0 .../lib/jquery/src/sizzle/dist/sizzle.min.map | 0 .../wwwroot/lib/jquery/src/traversing.js | 0 .../lib/jquery/src/traversing/findFilter.js | 0 .../src/traversing/var/rneedsContext.js | 0 .../wwwroot/lib/jquery/src/var/arr.js | 0 .../wwwroot/lib/jquery/src/var/class2type.js | 0 .../wwwroot/lib/jquery/src/var/concat.js | 0 .../wwwroot/lib/jquery/src/var/hasOwn.js | 0 .../wwwroot/lib/jquery/src/var/indexOf.js | 0 .../wwwroot/lib/jquery/src/var/pnum.js | 0 .../wwwroot/lib/jquery/src/var/push.js | 0 .../wwwroot/lib/jquery/src/var/rnotwhite.js | 0 .../wwwroot/lib/jquery/src/var/slice.js | 0 .../lib/jquery/src/var/strundefined.js | 0 .../wwwroot/lib/jquery/src/var/support.js | 0 .../wwwroot/lib/jquery/src/var/toString.js | 0 .../wwwroot/lib/jquery/src/wrap.js | 0 .../OpenIddict.Abstractions.csproj | 1 + .../OpenIddictConstants.cs | 13 + .../OpenIddictResources.resx | 122 +- .../Primitives/OpenIddictConfiguration.cs | 93 + .../Primitives/OpenIddictExtensions.cs | 17 + .../Primitives/OpenIddictRequest.cs | 9 + .../OpenIddict.AspNetCore.csproj | 3 +- .../OpenIddict.Client.AspNetCore.csproj | 44 + .../OpenIddictClientAspNetCoreBuilder.cs | 103 + ...OpenIddictClientAspNetCoreConfiguration.cs | 82 + .../OpenIddictClientAspNetCoreConstants.cs | 39 + .../OpenIddictClientAspNetCoreDefaults.cs | 18 + .../OpenIddictClientAspNetCoreExtensions.cs | 83 + .../OpenIddictClientAspNetCoreFeature.cs | 19 + .../OpenIddictClientAspNetCoreHandler.cs | 317 ++ ...penIddictClientAspNetCoreHandlerFilters.cs | 98 + ...ClientAspNetCoreHandlers.Authentication.cs | 108 + .../OpenIddictClientAspNetCoreHandlers.cs | 919 +++++ .../OpenIddictClientAspNetCoreHelpers.cs | 86 + .../OpenIddictClientAspNetCoreOptions.cs | 54 + .../OpenIddict.Client.SystemNetHttp.csproj | 33 + .../OpenIddictClientSystemNetHttpBuilder.cs | 78 + ...nIddictClientSystemNetHttpConfiguration.cs | 77 + ...OpenIddictClientSystemNetHttpExtensions.cs | 76 + ...IddictClientSystemNetHttpHandlerFilters.cs | 31 + ...ctClientSystemNetHttpHandlers.Discovery.cs | 44 + ...ictClientSystemNetHttpHandlers.Exchange.cs | 115 + .../OpenIddictClientSystemNetHttpHandlers.cs | 401 ++ .../OpenIddictClientSystemNetHttpHelpers.cs | 31 + .../OpenIddictClientSystemNetHttpOptions.cs | 25 + .../IOpenIddictClientDispatcher.cs | 12 + .../IOpenIddictClientFactory.cs | 12 + .../IOpenIddictClientHandler.cs | 23 + .../IOpenIddictClientHandlerFilter.cs | 12 + .../OpenIddict.Client.csproj | 41 + .../OpenIddictClientBuilder.cs | 1067 +++++ .../OpenIddictClientConfiguration.cs | 197 + .../OpenIddictClientDispatcher.cs | 132 + .../OpenIddictClientEndpointType.cs | 23 + .../OpenIddictClientEvents.Authentication.cs | 190 + .../OpenIddictClientEvents.Discovery.cs | 248 ++ .../OpenIddictClientEvents.Exchange.cs | 155 + .../OpenIddictClientEvents.Protection.cs | 130 + .../OpenIddictClientEvents.cs | 668 ++++ .../OpenIddictClientExtensions.cs | 87 + .../OpenIddictClientFactory.cs | 34 + .../OpenIddictClientHandler.cs | 33 + .../OpenIddictClientHandlerDescriptor.cs | 281 ++ .../OpenIddictClientHandlerFilters.cs | 222 ++ .../OpenIddictClientHandlerType.cs | 28 + ...OpenIddictClientHandlers.Authentication.cs | 509 +++ .../OpenIddictClientHandlers.Discovery.cs | 651 ++++ .../OpenIddictClientHandlers.Exchange.cs | 78 + .../OpenIddictClientHandlers.Protection.cs | 507 +++ .../OpenIddictClientHandlers.cs | 3467 +++++++++++++++++ .../OpenIddictClientHelpers.cs | 76 + .../OpenIddictClientOptions.cs | 101 + .../OpenIddictClientRegistration.cs | 120 + .../OpenIddictClientRetriever.cs | 62 + .../OpenIddictClientService.cs | 464 +++ .../OpenIddictClientTransaction.cs | 55 + .../OpenIddictServerAspNetCoreConstants.cs | 12 +- .../OpenIddictServerAspNetCoreHandler.cs | 25 +- ...ServerAspNetCoreHandlers.Authentication.cs | 2 +- ...nIddictServerAspNetCoreHandlers.Session.cs | 2 +- .../OpenIddictServerOwinHandler.cs | 12 +- ...IddictServerOwinHandlers.Authentication.cs | 2 +- .../OpenIddictServerOwinHandlers.Session.cs | 2 +- .../OpenIddictServerEvents.cs | 98 +- .../OpenIddictServerHandlers.Protection.cs | 12 +- .../OpenIddictServerHandlers.cs | 254 +- ...OpenIddictValidationAspNetCoreConstants.cs | 5 + .../OpenIddictValidationAspNetCoreHandler.cs | 5 +- .../OpenIddictValidationAspNetCoreHandlers.cs | 6 +- .../OpenIddictValidationOwinHandler.cs | 2 +- .../OpenIddictValidationOwinHandlers.cs | 6 +- ...alidationServerIntegrationConfiguration.cs | 5 +- ...ctValidationSystemNetHttpHandlerFilters.cs | 8 +- ...tionSystemNetHttpHandlers.Introspection.cs | 6 + ...enIddictValidationSystemNetHttpHandlers.cs | 2 +- .../OpenIddictValidationBuilder.cs | 3 +- .../OpenIddictValidationConfiguration.cs | 10 +- .../OpenIddictValidationEvents.Discovery.cs | 3 +- .../OpenIddictValidationEvents.cs | 14 +- .../OpenIddictValidationExtensions.cs | 1 + .../OpenIddictValidationHandlerFilters.cs | 16 + .../OpenIddictValidationHandlers.Discovery.cs | 80 +- ...OpenIddictValidationHandlers.Protection.cs | 39 +- .../OpenIddictValidationHandlers.cs | 63 +- .../OpenIddictValidationOptions.cs | 5 +- .../OpenIddictValidationRetriever.cs | 9 +- .../OpenIddictValidationService.cs | 13 +- src/OpenIddict/OpenIddict.csproj | 4 +- .../Primitives/OpenIddictRequestTests.cs | 7 + .../OpenIddictServerBuilderTests.cs | 4 +- 687 files changed, 13678 insertions(+), 498 deletions(-) delete mode 100644 sandbox/Mvc.Client/Controllers/AuthenticationController.cs delete mode 100644 sandbox/Mvc.Client/Startup.cs delete mode 100644 sandbox/Mvc.Server/Views/_ViewImports.cshtml create mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs create mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/ErrorController.cs rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/Controllers/HomeController.cs (71%) rename sandbox/{Mvc.Client/Mvc.Client.csproj => OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj} (71%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Client}/Program.cs (86%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/Properties/launchSettings.json (92%) create mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Client}/ViewModels/Shared/ErrorViewModel.cs (78%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Client}/Views/Shared/Error.cshtml (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/Views/Shared/Home.cshtml (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/Views/Shared/_Layout.cshtml (80%) create mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/_ViewImports.cshtml rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/Views/_ViewStart.cshtml (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/appsettings.Development.json (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/appsettings.json (100%) create mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Client/web.config rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/fonts/glyphicons-halflings-regular.eot (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/fonts/glyphicons-halflings-regular.svg (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/fonts/glyphicons-halflings-regular.ttf (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/fonts/glyphicons-halflings-regular.woff (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/scripts/bootstrap.js (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/scripts/bootstrap.min.js (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/scripts/jquery-1.9.0.intellisense.js (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/scripts/jquery-1.9.0.js (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/scripts/jquery-1.9.0.min.js (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/scripts/jquery-1.9.0.min.map (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/stylesheets/bootstrap-theme.css (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/stylesheets/bootstrap-theme.css.map (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/stylesheets/bootstrap-theme.min.css (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/stylesheets/bootstrap.css (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/stylesheets/bootstrap.css.map (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/stylesheets/bootstrap.min.css (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Client}/wwwroot/stylesheets/jumbotron-narrow.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/.bowerrc (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Controllers/AccountController.cs (98%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Controllers/AuthorizationController.cs (99%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Controllers/ErrorController.cs (88%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Controllers/HomeController.cs (89%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Controllers/ManageController.cs (98%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Controllers/ResourceController.cs (95%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Controllers/UserinfoController.cs (95%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Helpers/AsyncEnumerableExtensions.cs (89%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Helpers/FormValueRequiredAttribute.cs (95%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Models/ApplicationDbContext.cs (91%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Models/ApplicationUser.cs (76%) rename sandbox/{Mvc.Server/Mvc.Server.csproj => OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj} (100%) rename sandbox/{Mvc.Client => OpenIddict.Sandbox.AspNetCore.Server}/Program.cs (86%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Properties/launchSettings.json (92%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Services/IEmailSender.cs (63%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Services/ISmsSender.cs (59%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Services/MessageServices.cs (91%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Startup.cs (98%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Account/ExternalLoginConfirmationViewModel.cs (72%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Account/ForgotPasswordViewModel.cs (70%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Account/LoginViewModel.cs (82%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Account/RegisterViewModel.cs (90%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Account/ResetPasswordViewModel.cs (90%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Account/SendCodeViewModel.cs (80%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Account/VerifyCodeViewModel.cs (86%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Authorization/AuthorizeViewModel.cs (76%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Authorization/VerifyViewModel.cs (90%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Manage/AddPhoneNumberViewModel.cs (75%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Manage/ChangePasswordViewModel.cs (91%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Manage/ConfigureTwoFactorViewModel.cs (75%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Manage/FactorViewModel.cs (51%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Manage/IndexViewModel.cs (82%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Manage/ManageLoginsViewModel.cs (79%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Manage/RemoveLoginViewModel.cs (65%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Manage/SetPasswordViewModel.cs (89%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/ViewModels/Manage/VerifyPhoneNumberViewModel.cs (79%) create mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Shared/ErrorViewModel.cs rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Account/ConfirmEmail.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Account/ExternalLoginConfirmation.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Account/ExternalLoginFailure.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Account/ForgotPassword.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Account/ForgotPasswordConfirmation.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Account/Lockout.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Account/Login.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Account/Register.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Account/ResetPassword.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Account/ResetPasswordConfirmation.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Account/SendCode.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Account/VerifyCode.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Authorization/Authorize.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Authorization/Logout.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Authorization/Verify.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Home/About.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Home/Contact.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Home/Index.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Manage/AddPhoneNumber.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Manage/ChangePassword.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Manage/Index.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Manage/ManageLogins.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Manage/RemoveLogin.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Manage/SetPassword.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Manage/VerifyPhoneNumber.cshtml (100%) create mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Shared/Error.cshtml rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Shared/_Layout.cshtml (95%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Shared/_LoginPartial.cshtml (94%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/Shared/_ValidationScriptsPartial.cshtml (100%) create mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/_ViewImports.cshtml rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Views/_ViewStart.cshtml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/Worker.cs (98%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/appsettings.Development.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/appsettings.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/bower.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/gulpfile.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/package.json (100%) create mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Server/web.config rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/css/site.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/favicon.ico (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/images/ASP-NET-Banners-01.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/images/ASP-NET-Banners-02.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/images/Banner-01-Azure.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/images/Banner-02-VS.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/js/site.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap-touch-carousel/.bower.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap-touch-carousel/CHANGELOG.md (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap-touch-carousel/Gruntfile.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap-touch-carousel/LICENSE-MIT (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap-touch-carousel/README.md (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap-touch-carousel/bower.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap-touch-carousel/dist/css/bootstrap-touch-carousel.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap-touch-carousel/dist/js/bootstrap-touch-carousel.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap-touch-carousel/package.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap-touch-carousel/src/js/touch-carousel.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap-touch-carousel/src/js/transition.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap-touch-carousel/src/less/carousel.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap-touch-carousel/src/less/elements.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/.bower.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/CNAME (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/CONTRIBUTING.md (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/Gruntfile.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/LICENSE (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/README.md (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_config.yml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_includes/ads.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_includes/footer.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_includes/header.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_includes/nav-components.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_includes/nav-css.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_includes/nav-customize.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_includes/nav-getting-started.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_includes/nav-javascript.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_includes/nav-main.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_includes/old-bs-docs.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_includes/social-buttons.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_layouts/default.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/_layouts/home.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/css/docs.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/css/pygments-manni.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-114-precomposed.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-144-precomposed.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-57-precomposed.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-72-precomposed.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/ico/favicon.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/js/application.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/js/customizer.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/js/filesaver.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/js/holder.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/js/html5shiv.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/js/jquery.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/js/jszip.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/js/less.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/js/raw-files.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/js/respond.min.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/assets/js/uglify.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/bower.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/browserstack.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/components.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/composer.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/css.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/customize.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/dist/css/bootstrap.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/dist/js/bootstrap.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/carousel/carousel.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/carousel/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/grid/grid.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/grid/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/jumbotron-narrow/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/jumbotron-narrow/jumbotron-narrow.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/jumbotron/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/jumbotron/jumbotron.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/justified-nav/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/justified-nav/justified-nav.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/navbar-fixed-top/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/navbar-fixed-top/navbar-fixed-top.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/navbar-static-top/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/navbar-static-top/navbar-static-top.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/navbar/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/navbar/navbar.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/non-responsive/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/non-responsive/non-responsive.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/offcanvas/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/offcanvas/offcanvas.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/offcanvas/offcanvas.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/carousel.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/grid.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/jumbotron-narrow.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/jumbotron.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/justified-nav.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/navbar-fixed.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/navbar-static.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/navbar.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/non-responsive.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/offcanvas.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/sign-in.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/starter-template.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/sticky-footer-navbar.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/sticky-footer.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/screenshots/theme.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/signin/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/signin/signin.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/starter-template/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/starter-template/starter-template.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/sticky-footer-navbar/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/sticky-footer-navbar/sticky-footer-navbar.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/sticky-footer/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/sticky-footer/sticky-footer.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/theme/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/examples/theme/theme.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.svg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/getting-started.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/javascript.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/affix.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/alert.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/button.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/carousel.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/collapse.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/dropdown.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/modal.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/popover.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/scrollspy.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tab.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/phantom.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/server.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/unit/affix.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/unit/alert.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/unit/button.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/unit/carousel.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/unit/collapse.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/unit/dropdown.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/unit/modal.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/unit/phantom.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/unit/popover.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/unit/scrollspy.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/unit/tab.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/unit/tooltip.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/unit/transition.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/vendor/jquery.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/vendor/qunit.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tests/vendor/qunit.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/tooltip.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/js/transition.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/alerts.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/badges.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/bootstrap.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/breadcrumbs.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/button-groups.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/buttons.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/carousel.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/close.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/code.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/component-animations.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/dropdowns.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/forms.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/glyphicons.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/grid.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/input-groups.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/jumbotron.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/labels.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/list-group.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/media.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/mixins.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/modals.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/navbar.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/navs.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/normalize.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/pager.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/pagination.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/panels.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/popovers.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/print.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/progress-bars.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/responsive-utilities.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/scaffolding.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/tables.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/theme.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/thumbnails.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/tooltip.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/type.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/utilities.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/variables.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/less/wells.less (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/bootstrap/package.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/.bower.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/.bowerrc (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/.gitignore (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/.jscsrc (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/.jshintrc (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/.travis.yml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/CHANGELOG.md (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/CONTRIBUTING.md (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/Gruntfile.coffee (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/LICENSE.md (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/README.md (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/bower.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/component.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/hammer.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/hammer.min.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/hammer.min.map (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/hammer.js/package.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation-unobtrusive/.bower.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation-unobtrusive/.gitignore (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation-unobtrusive/CONTRIBUTING.md (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation-unobtrusive/README.md (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation-unobtrusive/bower.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/.bower.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/.gitattributes (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/.gitignore (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/.travis.yml (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/CONTRIBUTING.md (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/Gruntfile.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/README.md (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/additional-methods.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/changelog.txt (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/ajaxSubmit-integration-demo.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/captcha/captcha.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/captcha/fonts/Anorexia.ttf (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/captcha/image_req.php (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/captcha/images/.htaccess (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/captcha/images/button.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/captcha/images/image.php (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/captcha/index.php (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/captcha/newsession.php (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/captcha/process.php (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/captcha/rand.php (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/captcha/style.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/css/cmxform.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/css/cmxformTemplate.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/css/core.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/css/reset.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/css/screen.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/custom-messages-data-demo.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/custom-methods-demo.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/dynamic-totals.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/errorcontainer-demo.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/file_input.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/images/bg.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/images/checked.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/images/cmxform-divider.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/images/cmxform-fieldset.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/images/loading.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/images/unchecked.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/jquerymobile.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/login/images/bg.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/login/images/header1.jpg (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/login/images/page.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/login/images/required_star.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/login/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/login/screen.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/backRequiredGray.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/back_green-fade.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/back_nav_blue.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/blank.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/button-submit.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/favicon.ico (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/help.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/left-nav-callout-long.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/login-sprite.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/logo_marketo.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/sf.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/step1-24.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/step2-24.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/step3-24.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/tab-sprite.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/tab_green.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/time.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/toggle.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/images/warning.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/jquery.maskedinput.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/mktSignup.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/step2.htm (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/marketo/stylesheet.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/milk/bg.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/milk/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/milk/left_white.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/milk/milk.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/milk/milk.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/milk/right_white.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/multipart/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/multipart/js/jquery.maskedinput-1.0.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/multipart/js/ui.accordion.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/multipart/js/ui.core.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/multipart/style.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/radio-checkbox-select-demo.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/tabs/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/themerollered.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/tinymce/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/editor_template.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/img/icons.gif (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/langs/en.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/skins/default/ui.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/demo/tinymce/tiny_mce.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/jquery.validate.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/lib/jquery-1.6.4.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/lib/jquery-1.7.2.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/lib/jquery-1.8.3.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/lib/jquery-1.9.0.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/lib/jquery.form.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/lib/jquery.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/lib/jquery.mockjax.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_ar.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_bg.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_ca.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_cs.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_da.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_de.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_el.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_es.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_et.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_eu.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_fa.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_fi.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_fr.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_he.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_hr.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_hu.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_it.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_ja.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_ka.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_kk.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_ko.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_lt.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_lv.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_my.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_nl.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_no.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_pl.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_pt_BR.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_pt_PT.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_ro.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_ru.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_si.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_sk.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_sl.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_sr.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_sv.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_th.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_tr.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_uk.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_vi.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_zh.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/messages_zh_TW.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/methods_de.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/methods_nl.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/localization/methods_pt.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/package.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/events.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/firebug/errorIcon.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/firebug/firebug.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/firebug/firebug.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/firebug/firebug.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/firebug/firebugx.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/firebug/infoIcon.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/firebug/warningIcon.png (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/jquery.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/large.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/messages.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/methods.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/qunit/qunit.css (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/qunit/qunit.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/rules.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/selects/index.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/tabs.html (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/test/test.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/todo (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery-validation/validation.jquery.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/.bower.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/MIT-LICENSE.txt (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/bower.json (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/dist/jquery.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/dist/jquery.min.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/dist/jquery.min.map (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/ajax.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/ajax/jsonp.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/ajax/load.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/ajax/parseJSON.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/ajax/parseXML.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/ajax/script.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/ajax/var/nonce.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/ajax/var/rquery.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/ajax/xhr.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/attributes.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/attributes/attr.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/attributes/classes.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/attributes/prop.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/attributes/support.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/attributes/val.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/callbacks.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/core.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/core/access.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/core/init.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/core/parseHTML.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/core/ready.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/core/var/rsingleTag.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/css.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/css/addGetHookIf.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/css/curCSS.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/css/defaultDisplay.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/css/hiddenVisibleSelectors.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/css/support.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/css/swap.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/css/var/cssExpand.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/css/var/getStyles.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/css/var/isHidden.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/css/var/rmargin.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/css/var/rnumnonpx.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/data.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/data/Data.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/data/accepts.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/data/var/data_priv.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/data/var/data_user.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/deferred.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/deprecated.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/dimensions.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/effects.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/effects/Tween.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/effects/animatedSelector.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/event.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/event/ajax.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/event/alias.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/event/support.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/exports/amd.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/exports/global.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/intro.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/jquery.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/manipulation.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/manipulation/_evalUrl.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/manipulation/support.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/manipulation/var/rcheckableType.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/offset.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/outro.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/queue.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/queue/delay.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/selector-native.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/selector-sizzle.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/selector.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/serialize.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/sizzle/dist/sizzle.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/sizzle/dist/sizzle.min.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/sizzle/dist/sizzle.min.map (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/traversing.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/traversing/findFilter.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/traversing/var/rneedsContext.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/var/arr.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/var/class2type.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/var/concat.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/var/hasOwn.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/var/indexOf.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/var/pnum.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/var/push.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/var/rnotwhite.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/var/slice.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/var/strundefined.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/var/support.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/var/toString.js (100%) rename sandbox/{Mvc.Server => OpenIddict.Sandbox.AspNetCore.Server}/wwwroot/lib/jquery/src/wrap.js (100%) create mode 100644 src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs create mode 100644 src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj create mode 100644 src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs create mode 100644 src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs create mode 100644 src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs create mode 100644 src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreDefaults.cs create mode 100644 src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs create mode 100644 src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreFeature.cs create mode 100644 src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs create mode 100644 src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlerFilters.cs create mode 100644 src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.Authentication.cs create mode 100644 src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs create mode 100644 src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHelpers.cs create mode 100644 src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreOptions.cs create mode 100644 src/OpenIddict.Client.SystemNetHttp/OpenIddict.Client.SystemNetHttp.csproj create mode 100644 src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs create mode 100644 src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs create mode 100644 src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs create mode 100644 src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlerFilters.cs create mode 100644 src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Discovery.cs create mode 100644 src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs create mode 100644 src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs create mode 100644 src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHelpers.cs create mode 100644 src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs create mode 100644 src/OpenIddict.Client/IOpenIddictClientDispatcher.cs create mode 100644 src/OpenIddict.Client/IOpenIddictClientFactory.cs create mode 100644 src/OpenIddict.Client/IOpenIddictClientHandler.cs create mode 100644 src/OpenIddict.Client/IOpenIddictClientHandlerFilter.cs create mode 100644 src/OpenIddict.Client/OpenIddict.Client.csproj create mode 100644 src/OpenIddict.Client/OpenIddictClientBuilder.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientConfiguration.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientDispatcher.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientEndpointType.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientEvents.Discovery.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientEvents.Exchange.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientEvents.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientExtensions.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientFactory.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientHandler.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientHandlerDescriptor.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientHandlerType.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientHandlers.Exchange.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientHandlers.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientHelpers.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientOptions.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientRegistration.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientRetriever.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientService.cs create mode 100644 src/OpenIddict.Client/OpenIddictClientTransaction.cs diff --git a/OpenIddict.sln b/OpenIddict.sln index af4f3f92..bca05fc1 100644 --- a/OpenIddict.sln +++ b/OpenIddict.sln @@ -11,9 +11,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5FC71D6A-A EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict", "src\OpenIddict\OpenIddict.csproj", "{80A8D6CE-C29A-4602-9844-D51FEF9C33C8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mvc.Client", "sandbox\Mvc.Client\Mvc.Client.csproj", "{96B22EB9-771A-4DCA-B828-E6EA2774CF1B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Sandbox.AspNetCore.Client", "sandbox\OpenIddict.Sandbox.AspNetCore.Client\OpenIddict.Sandbox.AspNetCore.Client.csproj", "{96B22EB9-771A-4DCA-B828-E6EA2774CF1B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mvc.Server", "sandbox\Mvc.Server\Mvc.Server.csproj", "{7CBEAFD2-E3D0-4424-9B78-E87AB52327A6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Sandbox.AspNetCore.Server", "sandbox\OpenIddict.Sandbox.AspNetCore.Server\OpenIddict.Sandbox.AspNetCore.Server.csproj", "{7CBEAFD2-E3D0-4424-9B78-E87AB52327A6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFrameworkCore", "src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj", "{D2450929-ED0E-420D-B475-327924F9701C}" EndProject @@ -111,6 +111,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Quartz", "src\Op EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Quartz.Tests", "test\OpenIddict.Quartz.Tests\OpenIddict.Quartz.Tests.csproj", "{01420929-33B8-4B60-A618-8C8B5F139630}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client", "src\OpenIddict.Client\OpenIddict.Client.csproj", "{A5654E58-18D3-4148-B210-F4637AD57D48}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client.AspNetCore", "src\OpenIddict.Client.AspNetCore\OpenIddict.Client.AspNetCore.csproj", "{C92FB132-96A7-47C5-BBC2-0FC3EF4E0205}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client.SystemNetHttp", "src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj", "{00E19194-427A-42BC-BC72-519CF9C2B7D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -257,6 +263,18 @@ Global {01420929-33B8-4B60-A618-8C8B5F139630}.Debug|Any CPU.Build.0 = Debug|Any CPU {01420929-33B8-4B60-A618-8C8B5F139630}.Release|Any CPU.ActiveCfg = Release|Any CPU {01420929-33B8-4B60-A618-8C8B5F139630}.Release|Any CPU.Build.0 = Release|Any CPU + {A5654E58-18D3-4148-B210-F4637AD57D48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5654E58-18D3-4148-B210-F4637AD57D48}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5654E58-18D3-4148-B210-F4637AD57D48}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5654E58-18D3-4148-B210-F4637AD57D48}.Release|Any CPU.Build.0 = Release|Any CPU + {C92FB132-96A7-47C5-BBC2-0FC3EF4E0205}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C92FB132-96A7-47C5-BBC2-0FC3EF4E0205}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C92FB132-96A7-47C5-BBC2-0FC3EF4E0205}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C92FB132-96A7-47C5-BBC2-0FC3EF4E0205}.Release|Any CPU.Build.0 = Release|Any CPU + {00E19194-427A-42BC-BC72-519CF9C2B7D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00E19194-427A-42BC-BC72-519CF9C2B7D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00E19194-427A-42BC-BC72-519CF9C2B7D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00E19194-427A-42BC-BC72-519CF9C2B7D1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -297,6 +315,9 @@ Global {D94B10D3-3DD3-4829-B305-17C48833AB33} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {FD150A90-1997-45C7-9EBE-7C6E62E464E8} = {D544447C-D701-46BB-9A5B-C76C612A596B} {01420929-33B8-4B60-A618-8C8B5F139630} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} + {A5654E58-18D3-4148-B210-F4637AD57D48} = {D544447C-D701-46BB-9A5B-C76C612A596B} + {C92FB132-96A7-47C5-BBC2-0FC3EF4E0205} = {D544447C-D701-46BB-9A5B-C76C612A596B} + {00E19194-427A-42BC-BC72-519CF9C2B7D1} = {D544447C-D701-46BB-9A5B-C76C612A596B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616} diff --git a/Packages.props b/Packages.props index 791771b3..f90d7b4f 100644 --- a/Packages.props +++ b/Packages.props @@ -5,8 +5,9 @@ - - + + + diff --git a/sandbox/Mvc.Client/Controllers/AuthenticationController.cs b/sandbox/Mvc.Client/Controllers/AuthenticationController.cs deleted file mode 100644 index f6f18f48..00000000 --- a/sandbox/Mvc.Client/Controllers/AuthenticationController.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.AspNetCore.Mvc; - -namespace Mvc.Client.Controllers; - -public class AuthenticationController : Controller -{ - [HttpGet("~/login")] - public ActionResult LogIn() - { - // Instruct the OIDC client middleware to redirect the user agent to the identity provider. - // Note: the authenticationType parameter must match the value configured in Startup.cs - return Challenge(new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults.AuthenticationScheme); - } - - [HttpGet("~/logout"), HttpPost("~/logout")] - public ActionResult LogOut() - { - // Instruct the cookies middleware to delete the local cookie created when the user agent - // is redirected from the identity provider after a successful authorization flow and - // to redirect the user agent to the identity provider to sign out. - return SignOut(CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme); - } -} diff --git a/sandbox/Mvc.Client/Startup.cs b/sandbox/Mvc.Client/Startup.cs deleted file mode 100644 index 74eb9a74..00000000 --- a/sandbox/Mvc.Client/Startup.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; - -namespace Mvc.Client; - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddAuthentication(options => - { - options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; - }) - - .AddCookie(options => - { - options.LoginPath = "/login"; - options.ExpireTimeSpan = TimeSpan.FromMinutes(50); - options.SlidingExpiration = false; - }) - - .AddOpenIdConnect(options => - { - // Note: these settings must match the application details - // inserted in the database at the server level. - options.ClientId = "mvc"; - options.ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654"; - - options.RequireHttpsMetadata = false; - options.GetClaimsFromUserInfoEndpoint = true; - options.SaveTokens = true; - - // Use the authorization code flow. - options.ResponseType = OpenIdConnectResponseType.Code; - options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet; - - // Note: setting the Authority allows the OIDC client middleware to automatically - // retrieve the identity provider's configuration and spare you from setting - // the different endpoints URIs or the token validation parameters explicitly. - options.Authority = "https://localhost:44395/"; - - options.Scope.Add("email"); - options.Scope.Add("roles"); - options.Scope.Add("offline_access"); - options.Scope.Add("demo_api"); - - // Disable the built-in JWT claims mapping feature. - options.MapInboundClaims = false; - - options.TokenValidationParameters.NameClaimType = "name"; - options.TokenValidationParameters.RoleClaimType = "role"; - - options.AccessDeniedPath = "/"; - }); - - services.AddHttpClient(); - - services.AddControllersWithViews(); - } - - public void Configure(IApplicationBuilder app) - { - app.UseDeveloperExceptionPage(); - - app.UseStaticFiles(); - - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(options => - { - options.MapControllers(); - options.MapDefaultControllerRoute(); - }); - } -} diff --git a/sandbox/Mvc.Server/Views/_ViewImports.cshtml b/sandbox/Mvc.Server/Views/_ViewImports.cshtml deleted file mode 100644 index e8080468..00000000 --- a/sandbox/Mvc.Server/Views/_ViewImports.cshtml +++ /dev/null @@ -1,8 +0,0 @@ -@using Mvc.Server -@using Mvc.Server.Models -@using Mvc.Server.ViewModels.Account -@using Mvc.Server.ViewModels.Authorization -@using Mvc.Server.ViewModels.Manage -@using Mvc.Server.ViewModels.Shared -@using Microsoft.AspNetCore.Identity -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs new file mode 100644 index 00000000..67ef74a8 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs @@ -0,0 +1,139 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Mvc; +using OpenIddict.Client.AspNetCore; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace OpenIddict.Sandbox.AspNetCore.Client.Controllers; + +public class AuthenticationController : Controller +{ + [HttpGet("~/login")] + public ActionResult LogIn(string returnUrl) + { + var properties = new AuthenticationProperties(new Dictionary + { + // Note: when only one client is registered in the client options, + // setting the issuer property is not required and can be omitted. + [OpenIddictClientAspNetCoreConstants.Properties.Issuer] = "https://localhost:44395/" + }) + { + // Only allow local return URLs to prevent open redirect attacks. + RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/" + }; + + // Ask the OpenIddict client middleware to redirect the user agent to the identity provider. + return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme); + } + + [HttpGet("~/signin-oidc"), HttpPost("~/signin-oidc")] + public async Task Callback() + { + // Retrieve the authorization data validated by OpenIddict as part of the callback handling. + var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme); + + // Multiple strategies exist to handle OAuth 2.0/OpenID Connect callbacks, each with their pros and cons: + // + // * Directly using the tokens to perform the necessary action(s) on behalf of the user, which is suitable + // for applications that don't need a long-term access to the user's resources or don't want to store + // access/refresh tokens in a database or in an authentication cookie (which has security implications). + // It is also suitable for applications that don't need to authenticate users but only need to perform + // action(s) on their behalf by making API calls using the access tokens returned by the remote server. + // + // * Storing the external claims/tokens in a database (and optionally keeping the essentials claims in an + // authentication cookie so that cookie size limits are not hit). For the applications that use ASP.NET + // Core Identity, the UserManager.SetAuthenticationTokenAsync() API can be used to store external tokens. + // + // Note: in this case, it's recommended to use column encryption to protect the tokens in the database. + // + // * Storing the external claims/tokens in an authentication cookie, which doesn't require having + // a user database but may be affected by the cookie size limits enforced by most browser vendors + // (e.g Safari for macOS and Safari for iOS/iPadOS enforce a per-domain 4KB limit for all cookies). + // + // Note: this is the approach used here, but the external claims are first filtered to only persist + // a few claims like the user identifier. The same approach is used to store the access/refresh tokens. + + // Important: if the remote server doesn't support OpenID Connect and doesn't expose a userinfo endpoint, + // result.Principal.Identity will represent an unauthenticated identity and won't contain any claim. + // + // Such identities cannot be used as-is to build an authentication cookie in ASP.NET Core (as the + // antiforgery stack requires at least a name claim to bind CSRF cookies to the user's identity) but + // the access/refresh tokens can be retrieved using result.Properties.GetTokens() to make API calls. + if (result.Principal.Identity is not ClaimsIdentity { IsAuthenticated: true }) + { + throw new InvalidOperationException("The external authorization data cannot be used for authentication."); + } + + // Build an identity based on the external claims and that will be used to create the authentication cookie. + // + // By default, all claims extracted during the authorization dance are available. The claims collection stored + // in the cookie can be filtered out or mapped to different names depending the claim name or its issuer. + var claims = result.Principal.Claims + .Select(claim => claim switch + { + // Applications can map non-standard claims issued by specific issuers to a standard equivalent. + { Type: "non_standard_user_id", Issuer: "https://example.com/" } + => new Claim(Claims.Subject, claim.Value, claim.ValueType, claim.Issuer), + + _ => claim + }) + .Where(claim => claim switch + { + // Preserve the "name" and "sub" claims. + { Type: Claims.Name or Claims.Subject } => true, + + // Applications that use multiple client registrations can filter claims based on the issuer. + { Type: "custom_claim", Issuer: "https://example.com/" } => true, + + // Don't preserve the other claims. + _ => false + }); + + var identity = new ClaimsIdentity(claims, + authenticationType: CookieAuthenticationDefaults.AuthenticationScheme, + nameType: Claims.Name, + roleType: Claims.Role); + + // If needed, the tokens returned by the authorization server can be stored in the authentication cookie. + // To make cookies less heavy, tokens that are not used can be filtered out before creating the cookie. + var tokens = result.Properties.GetTokens().Where(token => token switch + { + // Preserve the access and refresh tokens returned in the token response, if available. + { + Name: OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken or + OpenIddictClientAspNetCoreConstants.Tokens.BackchannelRefreshToken + } => true, + + // Ignore the other tokens. + _ => false + }); + + var properties = new AuthenticationProperties + { + RedirectUri = result.Properties.RedirectUri + }; + + properties.StoreTokens(tokens); + + // Note: "return SignIn(...)" cannot be directly used in this case, as the cookies handler doesn't allow + // redirecting from an endpoint that doesn't match the path set in CookieAuthenticationOptions.LoginPath. + // For more information about this restriction, visit https://github.com/dotnet/aspnetcore/issues/36934. + await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), properties); + + return Redirect(properties.RedirectUri ?? "/"); + } + + [HttpGet("~/logout"), HttpPost("~/logout")] + public ActionResult LogOut() + { + // Ask the cookies middleware to delete the local cookie created when the user agent + // is redirected from the identity provider after a successful authorization flow. + var properties = new AuthenticationProperties + { + RedirectUri = "/" + }; + + return SignOut(properties, CookieAuthenticationDefaults.AuthenticationScheme); + } +} diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/ErrorController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/ErrorController.cs new file mode 100644 index 00000000..d71cbcee --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/ErrorController.cs @@ -0,0 +1,32 @@ +/* + * 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; +using Microsoft.AspNetCore.Mvc; +using OpenIddict.Sandbox.AspNetCore.Client.ViewModels.Shared; + +namespace OpenIddict.Sandbox.AspNetCore.Client; + +public class ErrorController : Controller +{ + [HttpGet, HttpPost, Route("~/error")] + public IActionResult Error() + { + // If the error was not caused by an invalid + // OIDC request, display a generic error page. + var response = HttpContext.GetOpenIddictClientResponse(); + if (response is null) + { + return View(new ErrorViewModel()); + } + + return View(new ErrorViewModel + { + Error = response.Error, + ErrorDescription = response.ErrorDescription + }); + } +} diff --git a/sandbox/Mvc.Client/Controllers/HomeController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs similarity index 71% rename from sandbox/Mvc.Client/Controllers/HomeController.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs index 5dbe668c..cc73ed83 100644 --- a/sandbox/Mvc.Client/Controllers/HomeController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs @@ -3,9 +3,9 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using OpenIddict.Client.AspNetCore; -namespace Mvc.Client.Controllers; +namespace OpenIddict.Sandbox.AspNetCore.Client.Controllers; public class HomeController : Controller { @@ -20,12 +20,8 @@ public class HomeController : Controller [Authorize, HttpPost("~/")] public async Task Index(CancellationToken cancellationToken) { - var token = await HttpContext.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectParameterNames.AccessToken); - if (string.IsNullOrEmpty(token)) - { - throw new InvalidOperationException("The access token cannot be found in the authentication ticket. " + - "Make sure that SaveTokens is set to true in the OIDC options."); - } + var token = await HttpContext.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme, + OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken); using var client = _httpClientFactory.CreateClient(); diff --git a/sandbox/Mvc.Client/Mvc.Client.csproj b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj similarity index 71% rename from sandbox/Mvc.Client/Mvc.Client.csproj rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj index 4fdb3060..59fa1395 100644 --- a/sandbox/Mvc.Client/Mvc.Client.csproj +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj @@ -6,6 +6,10 @@ disable + + + + diff --git a/sandbox/Mvc.Server/Program.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Program.cs similarity index 86% rename from sandbox/Mvc.Server/Program.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/Program.cs index 8624d7c0..d35cfa85 100644 --- a/sandbox/Mvc.Server/Program.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Program.cs @@ -1,4 +1,4 @@ -namespace Mvc.Server; +namespace OpenIddict.Sandbox.AspNetCore.Client; public static class Program { diff --git a/sandbox/Mvc.Client/Properties/launchSettings.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Properties/launchSettings.json similarity index 92% rename from sandbox/Mvc.Client/Properties/launchSettings.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/Properties/launchSettings.json index 1c7dcc5a..d85eb522 100644 --- a/sandbox/Mvc.Client/Properties/launchSettings.json +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Properties/launchSettings.json @@ -15,7 +15,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "Mvc.Client": { + "OpenIddict.Sandbox.AspNetCore.Client": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs new file mode 100644 index 00000000..5ce31718 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using OpenIddict.Abstractions; +using OpenIddict.Client; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace OpenIddict.Sandbox.AspNetCore.Client; + +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }) + + .AddCookie(options => + { + options.LoginPath = "/login"; + options.LogoutPath = "/logout"; + options.ExpireTimeSpan = TimeSpan.FromMinutes(50); + options.SlidingExpiration = false; + }); + + services.AddOpenIddict() + .AddClient(options => + { + // Add a client registration matching the client application definition in the server project. + options.AddRegistration(new OpenIddictClientRegistration + { + Issuer = new Uri("https://localhost:44395/", UriKind.Absolute), + + ClientId = "mvc", + ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", + RedirectUri = new Uri("https://localhost:44381/signin-oidc", UriKind.Absolute), + Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" } + }); + + // Enable the redirection endpoint needed to handle the callback stage. + options.SetRedirectionEndpointUris(new Uri("/signin-oidc", UriKind.Relative)); + + // Register the ASP.NET Core host and configure the ASP.NET Core-specific options. + options.UseAspNetCore() + .EnableStatusCodePagesIntegration() + .EnableRedirectionEndpointPassthrough(); + + // Register the signing and encryption credentials used to protect + // sensitive data like the state tokens produced by OpenIddict. + options.AddDevelopmentEncryptionCertificate() + .AddDevelopmentSigningCertificate(); + + // Register the System.Net.Http integration. + options.UseSystemNetHttp(); + }); + + services.AddHttpClient(); + + services.AddControllersWithViews(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + + app.UseStaticFiles(); + + app.UseStatusCodePagesWithReExecute("/error"); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(options => + { + options.MapControllers(); + options.MapDefaultControllerRoute(); + }); + } +} diff --git a/sandbox/Mvc.Server/ViewModels/Shared/ErrorViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/ViewModels/Shared/ErrorViewModel.cs similarity index 78% rename from sandbox/Mvc.Server/ViewModels/Shared/ErrorViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/ViewModels/Shared/ErrorViewModel.cs index 62b9851a..8e0e68be 100644 --- a/sandbox/Mvc.Server/ViewModels/Shared/ErrorViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/ViewModels/Shared/ErrorViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Mvc.Server.ViewModels.Shared; +namespace OpenIddict.Sandbox.AspNetCore.Client.ViewModels.Shared; public class ErrorViewModel { diff --git a/sandbox/Mvc.Server/Views/Shared/Error.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Shared/Error.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Shared/Error.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Shared/Error.cshtml diff --git a/sandbox/Mvc.Client/Views/Shared/Home.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Shared/Home.cshtml similarity index 100% rename from sandbox/Mvc.Client/Views/Shared/Home.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Shared/Home.cshtml diff --git a/sandbox/Mvc.Client/Views/Shared/_Layout.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Shared/_Layout.cshtml similarity index 80% rename from sandbox/Mvc.Client/Views/Shared/_Layout.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Shared/_Layout.cshtml index 3c29994c..ab3a76b4 100644 --- a/sandbox/Mvc.Client/Views/Shared/_Layout.cshtml +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Shared/_Layout.cshtml @@ -7,7 +7,7 @@ - Mvc.Client + OpenIddict.Sandbox.AspNetCore.Client @@ -16,7 +16,7 @@
-

Your application (Mvc.Client)

+

Your application (OpenIddict.Sandbox.AspNetCore.Client)

@RenderBody() diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/_ViewImports.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/_ViewImports.cshtml new file mode 100644 index 00000000..bb090cca --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using OpenIddict.Sandbox.AspNetCore.Client +@using OpenIddict.Sandbox.AspNetCore.Client.ViewModels.Shared +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/sandbox/Mvc.Client/Views/_ViewStart.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/_ViewStart.cshtml similarity index 100% rename from sandbox/Mvc.Client/Views/_ViewStart.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/_ViewStart.cshtml diff --git a/sandbox/Mvc.Client/appsettings.Development.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/appsettings.Development.json similarity index 100% rename from sandbox/Mvc.Client/appsettings.Development.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/appsettings.Development.json diff --git a/sandbox/Mvc.Client/appsettings.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/appsettings.json similarity index 100% rename from sandbox/Mvc.Client/appsettings.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/appsettings.json diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/web.config b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/web.config new file mode 100644 index 00000000..c128c496 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/web.config @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sandbox/Mvc.Client/wwwroot/fonts/glyphicons-halflings-regular.eot b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/fonts/glyphicons-halflings-regular.eot similarity index 100% rename from sandbox/Mvc.Client/wwwroot/fonts/glyphicons-halflings-regular.eot rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/fonts/glyphicons-halflings-regular.eot diff --git a/sandbox/Mvc.Client/wwwroot/fonts/glyphicons-halflings-regular.svg b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/fonts/glyphicons-halflings-regular.svg similarity index 100% rename from sandbox/Mvc.Client/wwwroot/fonts/glyphicons-halflings-regular.svg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/fonts/glyphicons-halflings-regular.svg diff --git a/sandbox/Mvc.Client/wwwroot/fonts/glyphicons-halflings-regular.ttf b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/fonts/glyphicons-halflings-regular.ttf similarity index 100% rename from sandbox/Mvc.Client/wwwroot/fonts/glyphicons-halflings-regular.ttf rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/fonts/glyphicons-halflings-regular.ttf diff --git a/sandbox/Mvc.Client/wwwroot/fonts/glyphicons-halflings-regular.woff b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/fonts/glyphicons-halflings-regular.woff similarity index 100% rename from sandbox/Mvc.Client/wwwroot/fonts/glyphicons-halflings-regular.woff rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/fonts/glyphicons-halflings-regular.woff diff --git a/sandbox/Mvc.Client/wwwroot/scripts/bootstrap.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/scripts/bootstrap.js similarity index 100% rename from sandbox/Mvc.Client/wwwroot/scripts/bootstrap.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/scripts/bootstrap.js diff --git a/sandbox/Mvc.Client/wwwroot/scripts/bootstrap.min.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/scripts/bootstrap.min.js similarity index 100% rename from sandbox/Mvc.Client/wwwroot/scripts/bootstrap.min.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/scripts/bootstrap.min.js diff --git a/sandbox/Mvc.Client/wwwroot/scripts/jquery-1.9.0.intellisense.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/scripts/jquery-1.9.0.intellisense.js similarity index 100% rename from sandbox/Mvc.Client/wwwroot/scripts/jquery-1.9.0.intellisense.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/scripts/jquery-1.9.0.intellisense.js diff --git a/sandbox/Mvc.Client/wwwroot/scripts/jquery-1.9.0.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/scripts/jquery-1.9.0.js similarity index 100% rename from sandbox/Mvc.Client/wwwroot/scripts/jquery-1.9.0.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/scripts/jquery-1.9.0.js diff --git a/sandbox/Mvc.Client/wwwroot/scripts/jquery-1.9.0.min.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/scripts/jquery-1.9.0.min.js similarity index 100% rename from sandbox/Mvc.Client/wwwroot/scripts/jquery-1.9.0.min.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/scripts/jquery-1.9.0.min.js diff --git a/sandbox/Mvc.Client/wwwroot/scripts/jquery-1.9.0.min.map b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/scripts/jquery-1.9.0.min.map similarity index 100% rename from sandbox/Mvc.Client/wwwroot/scripts/jquery-1.9.0.min.map rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/scripts/jquery-1.9.0.min.map diff --git a/sandbox/Mvc.Client/wwwroot/stylesheets/bootstrap-theme.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/bootstrap-theme.css similarity index 100% rename from sandbox/Mvc.Client/wwwroot/stylesheets/bootstrap-theme.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/bootstrap-theme.css diff --git a/sandbox/Mvc.Client/wwwroot/stylesheets/bootstrap-theme.css.map b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/bootstrap-theme.css.map similarity index 100% rename from sandbox/Mvc.Client/wwwroot/stylesheets/bootstrap-theme.css.map rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/bootstrap-theme.css.map diff --git a/sandbox/Mvc.Client/wwwroot/stylesheets/bootstrap-theme.min.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/bootstrap-theme.min.css similarity index 100% rename from sandbox/Mvc.Client/wwwroot/stylesheets/bootstrap-theme.min.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/bootstrap-theme.min.css diff --git a/sandbox/Mvc.Client/wwwroot/stylesheets/bootstrap.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/bootstrap.css similarity index 100% rename from sandbox/Mvc.Client/wwwroot/stylesheets/bootstrap.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/bootstrap.css diff --git a/sandbox/Mvc.Client/wwwroot/stylesheets/bootstrap.css.map b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/bootstrap.css.map similarity index 100% rename from sandbox/Mvc.Client/wwwroot/stylesheets/bootstrap.css.map rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/bootstrap.css.map diff --git a/sandbox/Mvc.Client/wwwroot/stylesheets/bootstrap.min.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/bootstrap.min.css similarity index 100% rename from sandbox/Mvc.Client/wwwroot/stylesheets/bootstrap.min.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/bootstrap.min.css diff --git a/sandbox/Mvc.Client/wwwroot/stylesheets/jumbotron-narrow.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/jumbotron-narrow.css similarity index 100% rename from sandbox/Mvc.Client/wwwroot/stylesheets/jumbotron-narrow.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Client/wwwroot/stylesheets/jumbotron-narrow.css diff --git a/sandbox/Mvc.Server/.bowerrc b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/.bowerrc similarity index 100% rename from sandbox/Mvc.Server/.bowerrc rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/.bowerrc diff --git a/sandbox/Mvc.Server/Controllers/AccountController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AccountController.cs similarity index 98% rename from sandbox/Mvc.Server/Controllers/AccountController.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AccountController.cs index 08cb0465..9e275281 100644 --- a/sandbox/Mvc.Server/Controllers/AccountController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AccountController.cs @@ -3,11 +3,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; -using Mvc.Server.Models; -using Mvc.Server.Services; -using Mvc.Server.ViewModels.Account; +using OpenIddict.Sandbox.AspNetCore.Server.Models; +using OpenIddict.Sandbox.AspNetCore.Server.Services; +using OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Account; -namespace Mvc.Server.Controllers; +namespace OpenIddict.Sandbox.AspNetCore.Server.Controllers; [Authorize] public class AccountController : Controller diff --git a/sandbox/Mvc.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs similarity index 99% rename from sandbox/Mvc.Server/Controllers/AuthorizationController.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs index 7556377e..6d7f9dcc 100644 --- a/sandbox/Mvc.Server/Controllers/AuthorizationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs @@ -11,14 +11,14 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; -using Mvc.Server.Helpers; -using Mvc.Server.Models; -using Mvc.Server.ViewModels.Authorization; +using OpenIddict.Sandbox.AspNetCore.Server.Helpers; +using OpenIddict.Sandbox.AspNetCore.Server.Models; +using OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Authorization; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Mvc.Server; +namespace OpenIddict.Sandbox.AspNetCore.Server; public class AuthorizationController : Controller { diff --git a/sandbox/Mvc.Server/Controllers/ErrorController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/ErrorController.cs similarity index 88% rename from sandbox/Mvc.Server/Controllers/ErrorController.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/ErrorController.cs index 116953ee..da824852 100644 --- a/sandbox/Mvc.Server/Controllers/ErrorController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/ErrorController.cs @@ -6,9 +6,9 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Mvc; -using Mvc.Server.ViewModels.Shared; +using OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Shared; -namespace Mvc.Server; +namespace OpenIddict.Sandbox.AspNetCore.Server; public class ErrorController : Controller { diff --git a/sandbox/Mvc.Server/Controllers/HomeController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/HomeController.cs similarity index 89% rename from sandbox/Mvc.Server/Controllers/HomeController.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/HomeController.cs index 1bdf3f56..bd765a1e 100644 --- a/sandbox/Mvc.Server/Controllers/HomeController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/HomeController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; -namespace Mvc.Server.Controllers; +namespace OpenIddict.Sandbox.AspNetCore.Server.Controllers; public class HomeController : Controller { diff --git a/sandbox/Mvc.Server/Controllers/ManageController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/ManageController.cs similarity index 98% rename from sandbox/Mvc.Server/Controllers/ManageController.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/ManageController.cs index 00938b93..96626d86 100644 --- a/sandbox/Mvc.Server/Controllers/ManageController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/ManageController.cs @@ -1,11 +1,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Mvc.Server.Models; -using Mvc.Server.Services; -using Mvc.Server.ViewModels.Manage; +using OpenIddict.Sandbox.AspNetCore.Server.Models; +using OpenIddict.Sandbox.AspNetCore.Server.Services; +using OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Manage; -namespace Mvc.Server.Controllers; +namespace OpenIddict.Sandbox.AspNetCore.Server.Controllers; [Authorize] public class ManageController : Controller diff --git a/sandbox/Mvc.Server/Controllers/ResourceController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/ResourceController.cs similarity index 95% rename from sandbox/Mvc.Server/Controllers/ResourceController.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/ResourceController.cs index d9acb0be..92bd7157 100644 --- a/sandbox/Mvc.Server/Controllers/ResourceController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/ResourceController.cs @@ -2,12 +2,12 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Mvc.Server.Models; +using OpenIddict.Sandbox.AspNetCore.Server.Models; using OpenIddict.Abstractions; using OpenIddict.Validation.AspNetCore; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Mvc.Server.Controllers; +namespace OpenIddict.Sandbox.AspNetCore.Server.Controllers; [Route("api")] public class ResourceController : Controller diff --git a/sandbox/Mvc.Server/Controllers/UserinfoController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/UserinfoController.cs similarity index 95% rename from sandbox/Mvc.Server/Controllers/UserinfoController.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/UserinfoController.cs index cd499639..44b874da 100644 --- a/sandbox/Mvc.Server/Controllers/UserinfoController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/UserinfoController.cs @@ -2,12 +2,12 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Mvc.Server.Models; +using OpenIddict.Sandbox.AspNetCore.Server.Models; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Mvc.Server.Controllers; +namespace OpenIddict.Sandbox.AspNetCore.Server.Controllers; public class UserinfoController : Controller { diff --git a/sandbox/Mvc.Server/Helpers/AsyncEnumerableExtensions.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Helpers/AsyncEnumerableExtensions.cs similarity index 89% rename from sandbox/Mvc.Server/Helpers/AsyncEnumerableExtensions.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Helpers/AsyncEnumerableExtensions.cs index 1b2fb756..946d35e7 100644 --- a/sandbox/Mvc.Server/Helpers/AsyncEnumerableExtensions.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Helpers/AsyncEnumerableExtensions.cs @@ -1,4 +1,4 @@ -namespace Mvc.Server.Helpers; +namespace OpenIddict.Sandbox.AspNetCore.Server.Helpers; public static class AsyncEnumerableExtensions { diff --git a/sandbox/Mvc.Server/Helpers/FormValueRequiredAttribute.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Helpers/FormValueRequiredAttribute.cs similarity index 95% rename from sandbox/Mvc.Server/Helpers/FormValueRequiredAttribute.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Helpers/FormValueRequiredAttribute.cs index c7ef0599..9328d924 100644 --- a/sandbox/Mvc.Server/Helpers/FormValueRequiredAttribute.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Helpers/FormValueRequiredAttribute.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; -namespace Mvc.Server.Helpers; +namespace OpenIddict.Sandbox.AspNetCore.Server.Helpers; public sealed class FormValueRequiredAttribute : ActionMethodSelectorAttribute { diff --git a/sandbox/Mvc.Server/Models/ApplicationDbContext.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Models/ApplicationDbContext.cs similarity index 91% rename from sandbox/Mvc.Server/Models/ApplicationDbContext.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Models/ApplicationDbContext.cs index af3e843b..2b361345 100644 --- a/sandbox/Mvc.Server/Models/ApplicationDbContext.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Models/ApplicationDbContext.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; -namespace Mvc.Server.Models; +namespace OpenIddict.Sandbox.AspNetCore.Server.Models; public class ApplicationDbContext : IdentityDbContext { diff --git a/sandbox/Mvc.Server/Models/ApplicationUser.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Models/ApplicationUser.cs similarity index 76% rename from sandbox/Mvc.Server/Models/ApplicationUser.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Models/ApplicationUser.cs index 6a98a66a..7b128502 100644 --- a/sandbox/Mvc.Server/Models/ApplicationUser.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Models/ApplicationUser.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; -namespace Mvc.Server.Models; +namespace OpenIddict.Sandbox.AspNetCore.Server.Models; // Add profile data for application users by adding properties to the ApplicationUser class public class ApplicationUser : IdentityUser { } diff --git a/sandbox/Mvc.Server/Mvc.Server.csproj b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj similarity index 100% rename from sandbox/Mvc.Server/Mvc.Server.csproj rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj diff --git a/sandbox/Mvc.Client/Program.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs similarity index 86% rename from sandbox/Mvc.Client/Program.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs index c11b1115..2d5b2d60 100644 --- a/sandbox/Mvc.Client/Program.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs @@ -1,4 +1,4 @@ -namespace Mvc.Client; +namespace OpenIddict.Sandbox.AspNetCore.Server; public static class Program { diff --git a/sandbox/Mvc.Server/Properties/launchSettings.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Properties/launchSettings.json similarity index 92% rename from sandbox/Mvc.Server/Properties/launchSettings.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Properties/launchSettings.json index 9e078ae9..a84423f7 100644 --- a/sandbox/Mvc.Server/Properties/launchSettings.json +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Properties/launchSettings.json @@ -15,7 +15,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "Mvc.Server": { + "OpenIddict.Sandbox.AspNetCore.Server": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { diff --git a/sandbox/Mvc.Server/Services/IEmailSender.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/IEmailSender.cs similarity index 63% rename from sandbox/Mvc.Server/Services/IEmailSender.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/IEmailSender.cs index f589b295..28f27587 100644 --- a/sandbox/Mvc.Server/Services/IEmailSender.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/IEmailSender.cs @@ -1,4 +1,4 @@ -namespace Mvc.Server.Services; +namespace OpenIddict.Sandbox.AspNetCore.Server.Services; public interface IEmailSender { diff --git a/sandbox/Mvc.Server/Services/ISmsSender.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/ISmsSender.cs similarity index 59% rename from sandbox/Mvc.Server/Services/ISmsSender.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/ISmsSender.cs index e9df4f3b..68091b8f 100644 --- a/sandbox/Mvc.Server/Services/ISmsSender.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/ISmsSender.cs @@ -1,4 +1,4 @@ -namespace Mvc.Server.Services; +namespace OpenIddict.Sandbox.AspNetCore.Server.Services; public interface ISmsSender { diff --git a/sandbox/Mvc.Server/Services/MessageServices.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/MessageServices.cs similarity index 91% rename from sandbox/Mvc.Server/Services/MessageServices.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/MessageServices.cs index 2ec1c49f..c672f2f8 100644 --- a/sandbox/Mvc.Server/Services/MessageServices.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Services/MessageServices.cs @@ -1,4 +1,4 @@ -namespace Mvc.Server.Services; +namespace OpenIddict.Sandbox.AspNetCore.Server.Services; // This class is used by the application to send Email and SMS // when you turn on two-factor authentication in ASP.NET Identity. diff --git a/sandbox/Mvc.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs similarity index 98% rename from sandbox/Mvc.Server/Startup.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs index 8b0ff97a..7e8ccfe4 100644 --- a/sandbox/Mvc.Server/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs @@ -1,11 +1,11 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using Mvc.Server.Models; -using Mvc.Server.Services; +using OpenIddict.Sandbox.AspNetCore.Server.Models; +using OpenIddict.Sandbox.AspNetCore.Server.Services; using Quartz; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Mvc.Server; +namespace OpenIddict.Sandbox.AspNetCore.Server; public class Startup { diff --git a/sandbox/Mvc.Server/ViewModels/Account/ExternalLoginConfirmationViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/ExternalLoginConfirmationViewModel.cs similarity index 72% rename from sandbox/Mvc.Server/ViewModels/Account/ExternalLoginConfirmationViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/ExternalLoginConfirmationViewModel.cs index 3ef5483c..85466d65 100644 --- a/sandbox/Mvc.Server/ViewModels/Account/ExternalLoginConfirmationViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/ExternalLoginConfirmationViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Mvc.Server.ViewModels.Account; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Account; public class ExternalLoginConfirmationViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Account/ForgotPasswordViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/ForgotPasswordViewModel.cs similarity index 70% rename from sandbox/Mvc.Server/ViewModels/Account/ForgotPasswordViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/ForgotPasswordViewModel.cs index 9c7080b1..2447f4b3 100644 --- a/sandbox/Mvc.Server/ViewModels/Account/ForgotPasswordViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/ForgotPasswordViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Mvc.Server.ViewModels.Account; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Account; public class ForgotPasswordViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Account/LoginViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/LoginViewModel.cs similarity index 82% rename from sandbox/Mvc.Server/ViewModels/Account/LoginViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/LoginViewModel.cs index 8b178672..0454720e 100644 --- a/sandbox/Mvc.Server/ViewModels/Account/LoginViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/LoginViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Mvc.Server.ViewModels.Account; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Account; public class LoginViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Account/RegisterViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/RegisterViewModel.cs similarity index 90% rename from sandbox/Mvc.Server/ViewModels/Account/RegisterViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/RegisterViewModel.cs index 2ddd989d..980d84a6 100644 --- a/sandbox/Mvc.Server/ViewModels/Account/RegisterViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/RegisterViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Mvc.Server.ViewModels.Account; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Account; public class RegisterViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Account/ResetPasswordViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/ResetPasswordViewModel.cs similarity index 90% rename from sandbox/Mvc.Server/ViewModels/Account/ResetPasswordViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/ResetPasswordViewModel.cs index 84bedccb..57ea8920 100644 --- a/sandbox/Mvc.Server/ViewModels/Account/ResetPasswordViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/ResetPasswordViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Mvc.Server.ViewModels.Account; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Account; public class ResetPasswordViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Account/SendCodeViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/SendCodeViewModel.cs similarity index 80% rename from sandbox/Mvc.Server/ViewModels/Account/SendCodeViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/SendCodeViewModel.cs index c1c6a80f..d65df331 100644 --- a/sandbox/Mvc.Server/ViewModels/Account/SendCodeViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/SendCodeViewModel.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc.Rendering; -namespace Mvc.Server.ViewModels.Account; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Account; public class SendCodeViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Account/VerifyCodeViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/VerifyCodeViewModel.cs similarity index 86% rename from sandbox/Mvc.Server/ViewModels/Account/VerifyCodeViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/VerifyCodeViewModel.cs index 52e37c9a..943d27d4 100644 --- a/sandbox/Mvc.Server/ViewModels/Account/VerifyCodeViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Account/VerifyCodeViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Mvc.Server.ViewModels.Account; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Account; public class VerifyCodeViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Authorization/AuthorizeViewModel.cs similarity index 76% rename from sandbox/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Authorization/AuthorizeViewModel.cs index 7cf90736..af4f6392 100644 --- a/sandbox/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Authorization/AuthorizeViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Mvc.Server.ViewModels.Authorization; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Authorization; public class AuthorizeViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Authorization/VerifyViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Authorization/VerifyViewModel.cs similarity index 90% rename from sandbox/Mvc.Server/ViewModels/Authorization/VerifyViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Authorization/VerifyViewModel.cs index 7a9db5aa..c1cfc228 100644 --- a/sandbox/Mvc.Server/ViewModels/Authorization/VerifyViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Authorization/VerifyViewModel.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using OpenIddict.Abstractions; -namespace Mvc.Server.ViewModels.Authorization; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Authorization; public class VerifyViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Manage/AddPhoneNumberViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/AddPhoneNumberViewModel.cs similarity index 75% rename from sandbox/Mvc.Server/ViewModels/Manage/AddPhoneNumberViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/AddPhoneNumberViewModel.cs index 2bddb4d1..898aefdd 100644 --- a/sandbox/Mvc.Server/ViewModels/Manage/AddPhoneNumberViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/AddPhoneNumberViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Mvc.Server.ViewModels.Manage; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Manage; public class AddPhoneNumberViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Manage/ChangePasswordViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/ChangePasswordViewModel.cs similarity index 91% rename from sandbox/Mvc.Server/ViewModels/Manage/ChangePasswordViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/ChangePasswordViewModel.cs index 6e2691f8..73335fe4 100644 --- a/sandbox/Mvc.Server/ViewModels/Manage/ChangePasswordViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/ChangePasswordViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Mvc.Server.ViewModels.Manage; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Manage; public class ChangePasswordViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Manage/ConfigureTwoFactorViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/ConfigureTwoFactorViewModel.cs similarity index 75% rename from sandbox/Mvc.Server/ViewModels/Manage/ConfigureTwoFactorViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/ConfigureTwoFactorViewModel.cs index 85f3dc02..3f9524ef 100644 --- a/sandbox/Mvc.Server/ViewModels/Manage/ConfigureTwoFactorViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/ConfigureTwoFactorViewModel.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc.Rendering; -namespace Mvc.Server.ViewModels.Manage; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Manage; public class ConfigureTwoFactorViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Manage/FactorViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/FactorViewModel.cs similarity index 51% rename from sandbox/Mvc.Server/ViewModels/Manage/FactorViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/FactorViewModel.cs index dc190ceb..db9ea182 100644 --- a/sandbox/Mvc.Server/ViewModels/Manage/FactorViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/FactorViewModel.cs @@ -1,4 +1,4 @@ -namespace Mvc.Server.ViewModels.Manage; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Manage; public class FactorViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Manage/IndexViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/IndexViewModel.cs similarity index 82% rename from sandbox/Mvc.Server/ViewModels/Manage/IndexViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/IndexViewModel.cs index 86091e7f..ae5c8ff1 100644 --- a/sandbox/Mvc.Server/ViewModels/Manage/IndexViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/IndexViewModel.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; -namespace Mvc.Server.ViewModels.Manage; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Manage; public class IndexViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Manage/ManageLoginsViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/ManageLoginsViewModel.cs similarity index 79% rename from sandbox/Mvc.Server/ViewModels/Manage/ManageLoginsViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/ManageLoginsViewModel.cs index bbfcae9c..5656fbc0 100644 --- a/sandbox/Mvc.Server/ViewModels/Manage/ManageLoginsViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/ManageLoginsViewModel.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; -namespace Mvc.Server.ViewModels.Manage; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Manage; public class ManageLoginsViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Manage/RemoveLoginViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/RemoveLoginViewModel.cs similarity index 65% rename from sandbox/Mvc.Server/ViewModels/Manage/RemoveLoginViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/RemoveLoginViewModel.cs index a784f759..b15ca2b0 100644 --- a/sandbox/Mvc.Server/ViewModels/Manage/RemoveLoginViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/RemoveLoginViewModel.cs @@ -1,4 +1,4 @@ -namespace Mvc.Server.ViewModels.Manage; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Manage; public class RemoveLoginViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Manage/SetPasswordViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/SetPasswordViewModel.cs similarity index 89% rename from sandbox/Mvc.Server/ViewModels/Manage/SetPasswordViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/SetPasswordViewModel.cs index ec866186..d1bf8a2e 100644 --- a/sandbox/Mvc.Server/ViewModels/Manage/SetPasswordViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/SetPasswordViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Mvc.Server.ViewModels.Manage; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Manage; public class SetPasswordViewModel { diff --git a/sandbox/Mvc.Server/ViewModels/Manage/VerifyPhoneNumberViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/VerifyPhoneNumberViewModel.cs similarity index 79% rename from sandbox/Mvc.Server/ViewModels/Manage/VerifyPhoneNumberViewModel.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/VerifyPhoneNumberViewModel.cs index 4254a4ef..c960a5cb 100644 --- a/sandbox/Mvc.Server/ViewModels/Manage/VerifyPhoneNumberViewModel.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Manage/VerifyPhoneNumberViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Mvc.Server.ViewModels.Manage; +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Manage; public class VerifyPhoneNumberViewModel { diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Shared/ErrorViewModel.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Shared/ErrorViewModel.cs new file mode 100644 index 00000000..17af0c68 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/ViewModels/Shared/ErrorViewModel.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Shared; + +public class ErrorViewModel +{ + [Display(Name = "Error")] + public string Error { get; set; } + + [Display(Name = "Description")] + public string ErrorDescription { get; set; } +} diff --git a/sandbox/Mvc.Server/Views/Account/ConfirmEmail.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ConfirmEmail.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Account/ConfirmEmail.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ConfirmEmail.cshtml diff --git a/sandbox/Mvc.Server/Views/Account/ExternalLoginConfirmation.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ExternalLoginConfirmation.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Account/ExternalLoginConfirmation.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ExternalLoginConfirmation.cshtml diff --git a/sandbox/Mvc.Server/Views/Account/ExternalLoginFailure.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ExternalLoginFailure.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Account/ExternalLoginFailure.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ExternalLoginFailure.cshtml diff --git a/sandbox/Mvc.Server/Views/Account/ForgotPassword.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ForgotPassword.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Account/ForgotPassword.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ForgotPassword.cshtml diff --git a/sandbox/Mvc.Server/Views/Account/ForgotPasswordConfirmation.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ForgotPasswordConfirmation.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Account/ForgotPasswordConfirmation.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ForgotPasswordConfirmation.cshtml diff --git a/sandbox/Mvc.Server/Views/Account/Lockout.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/Lockout.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Account/Lockout.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/Lockout.cshtml diff --git a/sandbox/Mvc.Server/Views/Account/Login.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/Login.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Account/Login.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/Login.cshtml diff --git a/sandbox/Mvc.Server/Views/Account/Register.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/Register.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Account/Register.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/Register.cshtml diff --git a/sandbox/Mvc.Server/Views/Account/ResetPassword.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ResetPassword.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Account/ResetPassword.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ResetPassword.cshtml diff --git a/sandbox/Mvc.Server/Views/Account/ResetPasswordConfirmation.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ResetPasswordConfirmation.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Account/ResetPasswordConfirmation.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/ResetPasswordConfirmation.cshtml diff --git a/sandbox/Mvc.Server/Views/Account/SendCode.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/SendCode.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Account/SendCode.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/SendCode.cshtml diff --git a/sandbox/Mvc.Server/Views/Account/VerifyCode.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/VerifyCode.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Account/VerifyCode.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Account/VerifyCode.cshtml diff --git a/sandbox/Mvc.Server/Views/Authorization/Authorize.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Authorization/Authorize.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Authorization/Authorize.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Authorization/Authorize.cshtml diff --git a/sandbox/Mvc.Server/Views/Authorization/Logout.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Authorization/Logout.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Authorization/Logout.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Authorization/Logout.cshtml diff --git a/sandbox/Mvc.Server/Views/Authorization/Verify.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Authorization/Verify.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Authorization/Verify.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Authorization/Verify.cshtml diff --git a/sandbox/Mvc.Server/Views/Home/About.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Home/About.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Home/About.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Home/About.cshtml diff --git a/sandbox/Mvc.Server/Views/Home/Contact.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Home/Contact.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Home/Contact.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Home/Contact.cshtml diff --git a/sandbox/Mvc.Server/Views/Home/Index.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Home/Index.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Home/Index.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Home/Index.cshtml diff --git a/sandbox/Mvc.Server/Views/Manage/AddPhoneNumber.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/AddPhoneNumber.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Manage/AddPhoneNumber.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/AddPhoneNumber.cshtml diff --git a/sandbox/Mvc.Server/Views/Manage/ChangePassword.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/ChangePassword.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Manage/ChangePassword.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/ChangePassword.cshtml diff --git a/sandbox/Mvc.Server/Views/Manage/Index.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/Index.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Manage/Index.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/Index.cshtml diff --git a/sandbox/Mvc.Server/Views/Manage/ManageLogins.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/ManageLogins.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Manage/ManageLogins.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/ManageLogins.cshtml diff --git a/sandbox/Mvc.Server/Views/Manage/RemoveLogin.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/RemoveLogin.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Manage/RemoveLogin.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/RemoveLogin.cshtml diff --git a/sandbox/Mvc.Server/Views/Manage/SetPassword.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/SetPassword.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Manage/SetPassword.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/SetPassword.cshtml diff --git a/sandbox/Mvc.Server/Views/Manage/VerifyPhoneNumber.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/VerifyPhoneNumber.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Manage/VerifyPhoneNumber.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Manage/VerifyPhoneNumber.cshtml diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Shared/Error.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Shared/Error.cshtml new file mode 100644 index 00000000..1b5c5de8 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Shared/Error.cshtml @@ -0,0 +1,14 @@ +@model ErrorViewModel + +
+

Ooooops, something went really bad! :(

+

+ @if (!string.IsNullOrEmpty(Model.Error)) { + @Model.Error + } + + @if (!string.IsNullOrEmpty(Model.ErrorDescription)) { + @Model.ErrorDescription + } +

+
\ No newline at end of file diff --git a/sandbox/Mvc.Server/Views/Shared/_Layout.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Shared/_Layout.cshtml similarity index 95% rename from sandbox/Mvc.Server/Views/Shared/_Layout.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Shared/_Layout.cshtml index 12f9b717..c7cd4cc6 100644 --- a/sandbox/Mvc.Server/Views/Shared/_Layout.cshtml +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Shared/_Layout.cshtml @@ -3,7 +3,7 @@ - @ViewData["Title"] - Mvc.Server + @ViewData["Title"] - OpenIddict.Sandbox.AspNetCore.Server @@ -29,7 +29,7 @@ - Mvc.Server + OpenIddict.Sandbox.AspNetCore.Server
diff --git a/sandbox/Mvc.Server/Views/Shared/_LoginPartial.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Shared/_LoginPartial.cshtml similarity index 94% rename from sandbox/Mvc.Server/Views/Shared/_LoginPartial.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Shared/_LoginPartial.cshtml index 1f1658bb..aab41de7 100644 --- a/sandbox/Mvc.Server/Views/Shared/_LoginPartial.cshtml +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Shared/_LoginPartial.cshtml @@ -1,5 +1,5 @@ @using Microsoft.AspNetCore.Identity -@using Mvc.Server.Models +@using OpenIddict.Sandbox.AspNetCore.Server.Models @inject SignInManager SignInManager @inject UserManager UserManager diff --git a/sandbox/Mvc.Server/Views/Shared/_ValidationScriptsPartial.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Shared/_ValidationScriptsPartial.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/Shared/_ValidationScriptsPartial.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/Shared/_ValidationScriptsPartial.cshtml diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/_ViewImports.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/_ViewImports.cshtml new file mode 100644 index 00000000..5751c3f9 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/_ViewImports.cshtml @@ -0,0 +1,8 @@ +@using OpenIddict.Sandbox.AspNetCore.Server +@using OpenIddict.Sandbox.AspNetCore.Server.Models +@using OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Account +@using OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Authorization +@using OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Manage +@using OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Shared +@using Microsoft.AspNetCore.Identity +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/sandbox/Mvc.Server/Views/_ViewStart.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/_ViewStart.cshtml similarity index 100% rename from sandbox/Mvc.Server/Views/_ViewStart.cshtml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Views/_ViewStart.cshtml diff --git a/sandbox/Mvc.Server/Worker.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs similarity index 98% rename from sandbox/Mvc.Server/Worker.cs rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs index 4e525c0c..a10cb10b 100644 --- a/sandbox/Mvc.Server/Worker.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs @@ -1,9 +1,9 @@ using System.Globalization; -using Mvc.Server.Models; +using OpenIddict.Sandbox.AspNetCore.Server.Models; using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Mvc.Server; +namespace OpenIddict.Sandbox.AspNetCore.Server; public class Worker : IHostedService { diff --git a/sandbox/Mvc.Server/appsettings.Development.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/appsettings.Development.json similarity index 100% rename from sandbox/Mvc.Server/appsettings.Development.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/appsettings.Development.json diff --git a/sandbox/Mvc.Server/appsettings.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/appsettings.json similarity index 100% rename from sandbox/Mvc.Server/appsettings.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/appsettings.json diff --git a/sandbox/Mvc.Server/bower.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/bower.json similarity index 100% rename from sandbox/Mvc.Server/bower.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/bower.json diff --git a/sandbox/Mvc.Server/gulpfile.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/gulpfile.js similarity index 100% rename from sandbox/Mvc.Server/gulpfile.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/gulpfile.js diff --git a/sandbox/Mvc.Server/package.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/package.json similarity index 100% rename from sandbox/Mvc.Server/package.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/package.json diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/web.config b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/web.config new file mode 100644 index 00000000..c128c496 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/web.config @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sandbox/Mvc.Server/wwwroot/css/site.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/css/site.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/css/site.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/css/site.css diff --git a/sandbox/Mvc.Server/wwwroot/favicon.ico b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/favicon.ico similarity index 100% rename from sandbox/Mvc.Server/wwwroot/favicon.ico rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/favicon.ico diff --git a/sandbox/Mvc.Server/wwwroot/images/ASP-NET-Banners-01.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/images/ASP-NET-Banners-01.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/images/ASP-NET-Banners-01.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/images/ASP-NET-Banners-01.png diff --git a/sandbox/Mvc.Server/wwwroot/images/ASP-NET-Banners-02.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/images/ASP-NET-Banners-02.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/images/ASP-NET-Banners-02.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/images/ASP-NET-Banners-02.png diff --git a/sandbox/Mvc.Server/wwwroot/images/Banner-01-Azure.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/images/Banner-01-Azure.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/images/Banner-01-Azure.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/images/Banner-01-Azure.png diff --git a/sandbox/Mvc.Server/wwwroot/images/Banner-02-VS.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/images/Banner-02-VS.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/images/Banner-02-VS.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/images/Banner-02-VS.png diff --git a/sandbox/Mvc.Server/wwwroot/js/site.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/js/site.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/js/site.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/js/site.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/.bower.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/.bower.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/.bower.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/.bower.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/CHANGELOG.md b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/CHANGELOG.md similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/CHANGELOG.md rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/CHANGELOG.md diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/Gruntfile.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/Gruntfile.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/Gruntfile.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/Gruntfile.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/LICENSE-MIT b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/LICENSE-MIT similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/LICENSE-MIT rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/LICENSE-MIT diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/README.md b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/README.md similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/README.md rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/README.md diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/bower.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/bower.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/bower.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/bower.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/dist/css/bootstrap-touch-carousel.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/dist/css/bootstrap-touch-carousel.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/dist/css/bootstrap-touch-carousel.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/dist/css/bootstrap-touch-carousel.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/dist/js/bootstrap-touch-carousel.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/dist/js/bootstrap-touch-carousel.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/dist/js/bootstrap-touch-carousel.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/dist/js/bootstrap-touch-carousel.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/package.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/package.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/package.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/package.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/src/js/touch-carousel.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/src/js/touch-carousel.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/src/js/touch-carousel.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/src/js/touch-carousel.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/src/js/transition.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/src/js/transition.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/src/js/transition.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/src/js/transition.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/src/less/carousel.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/src/less/carousel.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/src/less/carousel.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/src/less/carousel.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/src/less/elements.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/src/less/elements.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap-touch-carousel/src/less/elements.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap-touch-carousel/src/less/elements.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/.bower.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/.bower.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/.bower.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/.bower.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/CNAME b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/CNAME similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/CNAME rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/CNAME diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/CONTRIBUTING.md b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/CONTRIBUTING.md similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/CONTRIBUTING.md rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/CONTRIBUTING.md diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/Gruntfile.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/Gruntfile.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/Gruntfile.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/Gruntfile.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/LICENSE b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/LICENSE similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/LICENSE rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/LICENSE diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/README.md b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/README.md similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/README.md rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/README.md diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_config.yml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_config.yml similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_config.yml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_config.yml diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/ads.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/ads.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/ads.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/ads.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/footer.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/footer.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/footer.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/footer.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/header.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/header.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/header.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/header.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/nav-components.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/nav-components.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/nav-components.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/nav-components.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/nav-css.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/nav-css.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/nav-css.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/nav-css.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/nav-customize.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/nav-customize.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/nav-customize.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/nav-customize.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/nav-getting-started.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/nav-getting-started.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/nav-getting-started.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/nav-getting-started.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/nav-javascript.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/nav-javascript.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/nav-javascript.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/nav-javascript.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/nav-main.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/nav-main.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/nav-main.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/nav-main.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/old-bs-docs.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/old-bs-docs.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/old-bs-docs.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/old-bs-docs.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/social-buttons.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/social-buttons.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_includes/social-buttons.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_includes/social-buttons.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_layouts/default.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_layouts/default.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_layouts/default.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_layouts/default.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/_layouts/home.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_layouts/home.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/_layouts/home.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/_layouts/home.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/css/docs.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/css/docs.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/css/docs.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/css/docs.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/css/pygments-manni.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/css/pygments-manni.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/css/pygments-manni.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/css/pygments-manni.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-114-precomposed.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-114-precomposed.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-114-precomposed.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-114-precomposed.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-144-precomposed.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-144-precomposed.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-144-precomposed.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-144-precomposed.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-57-precomposed.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-57-precomposed.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-57-precomposed.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-57-precomposed.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-72-precomposed.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-72-precomposed.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-72-precomposed.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/ico/apple-touch-icon-72-precomposed.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/ico/favicon.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/ico/favicon.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/ico/favicon.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/ico/favicon.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/application.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/application.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/application.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/application.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/customizer.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/customizer.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/customizer.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/customizer.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/filesaver.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/filesaver.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/filesaver.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/filesaver.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/holder.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/holder.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/holder.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/holder.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/html5shiv.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/html5shiv.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/html5shiv.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/html5shiv.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/jquery.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/jquery.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/jquery.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/jquery.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/jszip.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/jszip.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/jszip.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/jszip.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/less.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/less.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/less.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/less.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/raw-files.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/raw-files.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/raw-files.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/raw-files.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/respond.min.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/respond.min.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/respond.min.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/respond.min.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/uglify.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/uglify.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/assets/js/uglify.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/assets/js/uglify.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/bower.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/bower.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/bower.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/bower.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/browserstack.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/browserstack.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/browserstack.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/browserstack.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/components.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/components.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/components.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/components.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/composer.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/composer.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/composer.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/composer.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/css.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/css.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/css.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/css.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/customize.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/customize.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/customize.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/customize.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/carousel/carousel.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/carousel/carousel.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/carousel/carousel.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/carousel/carousel.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/carousel/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/carousel/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/carousel/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/carousel/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/grid/grid.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/grid/grid.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/grid/grid.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/grid/grid.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/grid/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/grid/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/grid/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/grid/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/jumbotron-narrow/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/jumbotron-narrow/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/jumbotron-narrow/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/jumbotron-narrow/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/jumbotron-narrow/jumbotron-narrow.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/jumbotron-narrow/jumbotron-narrow.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/jumbotron-narrow/jumbotron-narrow.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/jumbotron-narrow/jumbotron-narrow.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/jumbotron/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/jumbotron/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/jumbotron/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/jumbotron/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/jumbotron/jumbotron.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/jumbotron/jumbotron.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/jumbotron/jumbotron.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/jumbotron/jumbotron.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/justified-nav/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/justified-nav/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/justified-nav/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/justified-nav/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/justified-nav/justified-nav.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/justified-nav/justified-nav.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/justified-nav/justified-nav.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/justified-nav/justified-nav.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/navbar-fixed-top/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/navbar-fixed-top/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/navbar-fixed-top/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/navbar-fixed-top/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/navbar-fixed-top/navbar-fixed-top.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/navbar-fixed-top/navbar-fixed-top.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/navbar-fixed-top/navbar-fixed-top.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/navbar-fixed-top/navbar-fixed-top.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/navbar-static-top/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/navbar-static-top/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/navbar-static-top/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/navbar-static-top/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/navbar-static-top/navbar-static-top.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/navbar-static-top/navbar-static-top.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/navbar-static-top/navbar-static-top.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/navbar-static-top/navbar-static-top.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/navbar/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/navbar/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/navbar/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/navbar/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/navbar/navbar.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/navbar/navbar.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/navbar/navbar.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/navbar/navbar.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/non-responsive/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/non-responsive/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/non-responsive/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/non-responsive/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/non-responsive/non-responsive.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/non-responsive/non-responsive.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/non-responsive/non-responsive.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/non-responsive/non-responsive.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/offcanvas/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/offcanvas/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/offcanvas/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/offcanvas/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/offcanvas/offcanvas.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/offcanvas/offcanvas.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/offcanvas/offcanvas.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/offcanvas/offcanvas.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/offcanvas/offcanvas.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/offcanvas/offcanvas.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/offcanvas/offcanvas.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/offcanvas/offcanvas.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/carousel.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/carousel.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/carousel.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/carousel.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/grid.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/grid.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/grid.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/grid.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/jumbotron-narrow.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/jumbotron-narrow.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/jumbotron-narrow.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/jumbotron-narrow.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/jumbotron.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/jumbotron.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/jumbotron.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/jumbotron.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/justified-nav.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/justified-nav.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/justified-nav.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/justified-nav.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/navbar-fixed.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/navbar-fixed.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/navbar-fixed.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/navbar-fixed.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/navbar-static.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/navbar-static.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/navbar-static.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/navbar-static.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/navbar.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/navbar.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/navbar.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/navbar.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/non-responsive.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/non-responsive.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/non-responsive.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/non-responsive.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/offcanvas.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/offcanvas.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/offcanvas.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/offcanvas.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/sign-in.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/sign-in.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/sign-in.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/sign-in.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/starter-template.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/starter-template.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/starter-template.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/starter-template.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/sticky-footer-navbar.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/sticky-footer-navbar.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/sticky-footer-navbar.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/sticky-footer-navbar.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/sticky-footer.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/sticky-footer.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/sticky-footer.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/sticky-footer.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/theme.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/theme.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/screenshots/theme.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/screenshots/theme.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/signin/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/signin/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/signin/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/signin/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/signin/signin.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/signin/signin.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/signin/signin.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/signin/signin.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/starter-template/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/starter-template/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/starter-template/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/starter-template/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/starter-template/starter-template.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/starter-template/starter-template.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/starter-template/starter-template.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/starter-template/starter-template.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/sticky-footer-navbar/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/sticky-footer-navbar/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/sticky-footer-navbar/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/sticky-footer-navbar/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/sticky-footer-navbar/sticky-footer-navbar.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/sticky-footer-navbar/sticky-footer-navbar.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/sticky-footer-navbar/sticky-footer-navbar.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/sticky-footer-navbar/sticky-footer-navbar.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/sticky-footer/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/sticky-footer/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/sticky-footer/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/sticky-footer/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/sticky-footer/sticky-footer.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/sticky-footer/sticky-footer.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/sticky-footer/sticky-footer.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/sticky-footer/sticky-footer.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/theme/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/theme/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/theme/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/theme/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/theme/theme.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/theme/theme.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/examples/theme/theme.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/examples/theme/theme.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.svg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.svg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.svg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.svg diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/getting-started.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/getting-started.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/getting-started.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/getting-started.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/javascript.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/javascript.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/javascript.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/javascript.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/affix.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/affix.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/affix.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/affix.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/alert.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/alert.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/alert.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/alert.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/button.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/button.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/button.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/button.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/carousel.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/carousel.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/carousel.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/carousel.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/collapse.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/collapse.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/collapse.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/collapse.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/dropdown.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/dropdown.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/dropdown.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/dropdown.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/modal.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/modal.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/modal.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/modal.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/popover.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/popover.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/popover.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/popover.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/scrollspy.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/scrollspy.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/scrollspy.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/scrollspy.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tab.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tab.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tab.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tab.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/phantom.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/phantom.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/phantom.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/phantom.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/server.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/server.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/server.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/server.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/affix.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/affix.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/affix.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/affix.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/alert.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/alert.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/alert.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/alert.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/button.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/button.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/button.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/button.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/carousel.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/carousel.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/carousel.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/carousel.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/collapse.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/collapse.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/collapse.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/collapse.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/dropdown.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/dropdown.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/dropdown.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/dropdown.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/modal.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/modal.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/modal.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/modal.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/phantom.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/phantom.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/phantom.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/phantom.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/popover.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/popover.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/popover.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/popover.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/scrollspy.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/scrollspy.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/scrollspy.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/scrollspy.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/tab.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/tab.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/tab.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/tab.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/tooltip.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/tooltip.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/tooltip.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/tooltip.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/transition.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/transition.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/unit/transition.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/unit/transition.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/vendor/jquery.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/vendor/jquery.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/vendor/jquery.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/vendor/jquery.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/vendor/qunit.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/vendor/qunit.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/vendor/qunit.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/vendor/qunit.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/vendor/qunit.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/vendor/qunit.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tests/vendor/qunit.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tests/vendor/qunit.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tooltip.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tooltip.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/tooltip.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/tooltip.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/transition.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/transition.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/js/transition.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/js/transition.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/alerts.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/alerts.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/alerts.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/alerts.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/badges.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/badges.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/badges.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/badges.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/bootstrap.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/bootstrap.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/bootstrap.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/bootstrap.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/breadcrumbs.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/breadcrumbs.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/breadcrumbs.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/breadcrumbs.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/button-groups.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/button-groups.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/button-groups.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/button-groups.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/buttons.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/buttons.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/buttons.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/buttons.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/carousel.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/carousel.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/carousel.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/carousel.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/close.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/close.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/close.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/close.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/code.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/code.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/code.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/code.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/component-animations.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/component-animations.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/component-animations.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/component-animations.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/dropdowns.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/dropdowns.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/dropdowns.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/dropdowns.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/forms.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/forms.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/forms.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/forms.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/glyphicons.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/glyphicons.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/glyphicons.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/glyphicons.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/grid.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/grid.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/grid.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/grid.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/input-groups.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/input-groups.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/input-groups.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/input-groups.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/jumbotron.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/jumbotron.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/jumbotron.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/jumbotron.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/labels.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/labels.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/labels.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/labels.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/list-group.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/list-group.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/list-group.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/list-group.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/media.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/media.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/media.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/media.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/mixins.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/mixins.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/mixins.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/mixins.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/modals.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/modals.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/modals.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/modals.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/navbar.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/navbar.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/navbar.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/navbar.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/navs.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/navs.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/navs.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/navs.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/normalize.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/normalize.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/normalize.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/normalize.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/pager.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/pager.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/pager.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/pager.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/pagination.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/pagination.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/pagination.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/pagination.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/panels.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/panels.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/panels.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/panels.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/popovers.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/popovers.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/popovers.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/popovers.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/print.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/print.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/print.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/print.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/progress-bars.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/progress-bars.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/progress-bars.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/progress-bars.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/responsive-utilities.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/responsive-utilities.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/responsive-utilities.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/responsive-utilities.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/scaffolding.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/scaffolding.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/scaffolding.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/scaffolding.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/tables.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/tables.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/tables.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/tables.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/theme.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/theme.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/theme.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/theme.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/thumbnails.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/thumbnails.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/thumbnails.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/thumbnails.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/tooltip.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/tooltip.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/tooltip.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/tooltip.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/type.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/type.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/type.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/type.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/utilities.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/utilities.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/utilities.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/utilities.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/variables.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/variables.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/variables.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/variables.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/wells.less b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/wells.less similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/less/wells.less rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/less/wells.less diff --git a/sandbox/Mvc.Server/wwwroot/lib/bootstrap/package.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/package.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/bootstrap/package.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/bootstrap/package.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/.bower.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/.bower.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/.bower.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/.bower.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/.bowerrc b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/.bowerrc similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/.bowerrc rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/.bowerrc diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/.gitignore b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/.gitignore similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/.gitignore rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/.gitignore diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/.jscsrc b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/.jscsrc similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/.jscsrc rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/.jscsrc diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/.jshintrc b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/.jshintrc similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/.jshintrc rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/.jshintrc diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/.travis.yml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/.travis.yml similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/.travis.yml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/.travis.yml diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/CHANGELOG.md b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/CHANGELOG.md similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/CHANGELOG.md rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/CHANGELOG.md diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/CONTRIBUTING.md b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/CONTRIBUTING.md similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/CONTRIBUTING.md rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/CONTRIBUTING.md diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/Gruntfile.coffee b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/Gruntfile.coffee similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/Gruntfile.coffee rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/Gruntfile.coffee diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/LICENSE.md b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/LICENSE.md similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/LICENSE.md rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/LICENSE.md diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/README.md b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/README.md similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/README.md rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/README.md diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/bower.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/bower.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/bower.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/bower.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/component.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/component.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/component.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/component.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/hammer.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/hammer.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/hammer.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/hammer.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/hammer.min.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/hammer.min.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/hammer.min.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/hammer.min.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/hammer.min.map b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/hammer.min.map similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/hammer.min.map rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/hammer.min.map diff --git a/sandbox/Mvc.Server/wwwroot/lib/hammer.js/package.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/package.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/hammer.js/package.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/hammer.js/package.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/.bower.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/.bower.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/.bower.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/.bower.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/.gitignore b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/.gitignore similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/.gitignore rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/.gitignore diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/CONTRIBUTING.md b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/CONTRIBUTING.md similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/CONTRIBUTING.md rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/CONTRIBUTING.md diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/README.md b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/README.md similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/README.md rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/README.md diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/bower.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/bower.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/bower.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/bower.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/.bower.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/.bower.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/.bower.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/.bower.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/.gitattributes b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/.gitattributes similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/.gitattributes rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/.gitattributes diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/.gitignore b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/.gitignore similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/.gitignore rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/.gitignore diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/.travis.yml b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/.travis.yml similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/.travis.yml rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/.travis.yml diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/CONTRIBUTING.md b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/CONTRIBUTING.md similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/CONTRIBUTING.md rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/CONTRIBUTING.md diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/Gruntfile.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/Gruntfile.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/Gruntfile.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/Gruntfile.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/README.md b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/README.md similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/README.md rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/README.md diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/additional-methods.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/additional-methods.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/additional-methods.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/additional-methods.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/changelog.txt b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/changelog.txt similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/changelog.txt rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/changelog.txt diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/ajaxSubmit-integration-demo.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/ajaxSubmit-integration-demo.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/ajaxSubmit-integration-demo.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/ajaxSubmit-integration-demo.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/captcha.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/captcha.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/captcha.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/captcha.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/fonts/Anorexia.ttf b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/fonts/Anorexia.ttf similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/fonts/Anorexia.ttf rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/fonts/Anorexia.ttf diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/image_req.php b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/image_req.php similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/image_req.php rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/image_req.php diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/images/.htaccess b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/images/.htaccess similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/images/.htaccess rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/images/.htaccess diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/images/button.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/images/button.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/images/button.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/images/button.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/images/image.php b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/images/image.php similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/images/image.php rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/images/image.php diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/index.php b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/index.php similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/index.php rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/index.php diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/newsession.php b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/newsession.php similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/newsession.php rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/newsession.php diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/process.php b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/process.php similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/process.php rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/process.php diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/rand.php b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/rand.php similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/rand.php rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/rand.php diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/style.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/style.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/captcha/style.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/captcha/style.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/css/cmxform.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/css/cmxform.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/css/cmxform.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/css/cmxform.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/css/cmxformTemplate.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/css/cmxformTemplate.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/css/cmxformTemplate.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/css/cmxformTemplate.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/css/core.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/css/core.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/css/core.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/css/core.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/css/reset.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/css/reset.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/css/reset.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/css/reset.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/css/screen.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/css/screen.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/css/screen.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/css/screen.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/custom-messages-data-demo.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/custom-messages-data-demo.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/custom-messages-data-demo.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/custom-messages-data-demo.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/custom-methods-demo.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/custom-methods-demo.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/custom-methods-demo.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/custom-methods-demo.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/dynamic-totals.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/dynamic-totals.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/dynamic-totals.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/dynamic-totals.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/errorcontainer-demo.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/errorcontainer-demo.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/errorcontainer-demo.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/errorcontainer-demo.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/file_input.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/file_input.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/file_input.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/file_input.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/images/bg.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/images/bg.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/images/bg.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/images/bg.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/images/checked.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/images/checked.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/images/checked.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/images/checked.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/images/cmxform-divider.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/images/cmxform-divider.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/images/cmxform-divider.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/images/cmxform-divider.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/images/cmxform-fieldset.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/images/cmxform-fieldset.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/images/cmxform-fieldset.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/images/cmxform-fieldset.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/images/loading.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/images/loading.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/images/loading.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/images/loading.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/images/unchecked.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/images/unchecked.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/images/unchecked.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/images/unchecked.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/jquerymobile.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/jquerymobile.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/jquerymobile.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/jquerymobile.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/login/images/bg.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/login/images/bg.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/login/images/bg.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/login/images/bg.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/login/images/header1.jpg b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/login/images/header1.jpg similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/login/images/header1.jpg rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/login/images/header1.jpg diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/login/images/page.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/login/images/page.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/login/images/page.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/login/images/page.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/login/images/required_star.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/login/images/required_star.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/login/images/required_star.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/login/images/required_star.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/login/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/login/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/login/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/login/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/login/screen.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/login/screen.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/login/screen.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/login/screen.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/backRequiredGray.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/backRequiredGray.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/backRequiredGray.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/backRequiredGray.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/back_green-fade.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/back_green-fade.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/back_green-fade.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/back_green-fade.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/back_nav_blue.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/back_nav_blue.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/back_nav_blue.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/back_nav_blue.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/blank.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/blank.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/blank.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/blank.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/button-submit.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/button-submit.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/button-submit.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/button-submit.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/favicon.ico b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/favicon.ico similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/favicon.ico rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/favicon.ico diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/help.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/help.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/help.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/help.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/left-nav-callout-long.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/left-nav-callout-long.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/left-nav-callout-long.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/left-nav-callout-long.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/login-sprite.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/login-sprite.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/login-sprite.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/login-sprite.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/logo_marketo.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/logo_marketo.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/logo_marketo.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/logo_marketo.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/sf.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/sf.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/sf.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/sf.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/step1-24.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/step1-24.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/step1-24.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/step1-24.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/step2-24.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/step2-24.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/step2-24.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/step2-24.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/step3-24.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/step3-24.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/step3-24.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/step3-24.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/tab-sprite.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/tab-sprite.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/tab-sprite.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/tab-sprite.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/tab_green.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/tab_green.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/tab_green.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/tab_green.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/time.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/time.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/time.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/time.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/toggle.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/toggle.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/toggle.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/toggle.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/warning.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/warning.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/images/warning.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/images/warning.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/jquery.maskedinput.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/jquery.maskedinput.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/jquery.maskedinput.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/jquery.maskedinput.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/mktSignup.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/mktSignup.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/mktSignup.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/mktSignup.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/step2.htm b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/step2.htm similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/step2.htm rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/step2.htm diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/stylesheet.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/stylesheet.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/marketo/stylesheet.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/marketo/stylesheet.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/milk/bg.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/milk/bg.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/milk/bg.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/milk/bg.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/milk/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/milk/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/milk/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/milk/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/milk/left_white.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/milk/left_white.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/milk/left_white.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/milk/left_white.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/milk/milk.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/milk/milk.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/milk/milk.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/milk/milk.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/milk/milk.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/milk/milk.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/milk/milk.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/milk/milk.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/milk/right_white.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/milk/right_white.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/milk/right_white.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/milk/right_white.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/multipart/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/multipart/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/multipart/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/multipart/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/multipart/js/jquery.maskedinput-1.0.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/multipart/js/jquery.maskedinput-1.0.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/multipart/js/jquery.maskedinput-1.0.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/multipart/js/jquery.maskedinput-1.0.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/multipart/js/ui.accordion.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/multipart/js/ui.accordion.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/multipart/js/ui.accordion.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/multipart/js/ui.accordion.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/multipart/js/ui.core.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/multipart/js/ui.core.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/multipart/js/ui.core.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/multipart/js/ui.core.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/multipart/style.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/multipart/style.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/multipart/style.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/multipart/style.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/radio-checkbox-select-demo.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/radio-checkbox-select-demo.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/radio-checkbox-select-demo.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/radio-checkbox-select-demo.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tabs/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tabs/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tabs/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tabs/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/themerollered.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/themerollered.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/themerollered.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/themerollered.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tinymce/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tinymce/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tinymce/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tinymce/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/editor_template.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/editor_template.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/editor_template.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/editor_template.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/img/icons.gif b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/img/icons.gif similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/img/icons.gif rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/img/icons.gif diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/langs/en.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/langs/en.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/langs/en.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/langs/en.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/skins/default/ui.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/skins/default/ui.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/skins/default/ui.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tinymce/themes/simple/skins/default/ui.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tinymce/tiny_mce.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tinymce/tiny_mce.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/demo/tinymce/tiny_mce.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/demo/tinymce/tiny_mce.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/jquery.validate.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/jquery.validate.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/jquery.validate.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/jquery.validate.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery-1.6.4.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery-1.6.4.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery-1.6.4.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery-1.6.4.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery-1.7.2.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery-1.7.2.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery-1.7.2.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery-1.7.2.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery-1.8.3.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery-1.8.3.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery-1.8.3.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery-1.8.3.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery-1.9.0.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery-1.9.0.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery-1.9.0.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery-1.9.0.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery.form.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery.form.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery.form.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery.form.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery.mockjax.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery.mockjax.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/lib/jquery.mockjax.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/lib/jquery.mockjax.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ar.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ar.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ar.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ar.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_bg.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_bg.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_bg.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_bg.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ca.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ca.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ca.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ca.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_cs.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_cs.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_cs.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_cs.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_da.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_da.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_da.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_da.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_de.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_de.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_de.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_de.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_el.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_el.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_el.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_el.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_es.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_es.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_es.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_es.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_et.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_et.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_et.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_et.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_eu.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_eu.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_eu.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_eu.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_fa.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_fa.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_fa.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_fa.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_fi.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_fi.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_fi.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_fi.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_fr.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_fr.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_fr.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_fr.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_he.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_he.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_he.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_he.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_hr.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_hr.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_hr.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_hr.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_hu.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_hu.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_hu.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_hu.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_it.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_it.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_it.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_it.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ja.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ja.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ja.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ja.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ka.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ka.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ka.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ka.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_kk.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_kk.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_kk.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_kk.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ko.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ko.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ko.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ko.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_lt.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_lt.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_lt.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_lt.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_lv.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_lv.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_lv.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_lv.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_my.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_my.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_my.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_my.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_nl.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_nl.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_nl.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_nl.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_no.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_no.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_no.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_no.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_pl.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_pl.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_pl.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_pl.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_pt_BR.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_pt_BR.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_pt_BR.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_pt_BR.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_pt_PT.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_pt_PT.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_pt_PT.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_pt_PT.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ro.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ro.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ro.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ro.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ru.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ru.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_ru.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_ru.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_si.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_si.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_si.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_si.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_sk.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_sk.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_sk.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_sk.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_sl.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_sl.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_sl.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_sl.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_sr.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_sr.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_sr.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_sr.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_sv.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_sv.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_sv.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_sv.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_th.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_th.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_th.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_th.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_tr.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_tr.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_tr.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_tr.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_uk.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_uk.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_uk.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_uk.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_vi.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_vi.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_vi.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_vi.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_zh.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_zh.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_zh.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_zh.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_zh_TW.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_zh_TW.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/messages_zh_TW.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/messages_zh_TW.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/methods_de.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/methods_de.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/methods_de.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/methods_de.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/methods_nl.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/methods_nl.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/methods_nl.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/methods_nl.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/methods_pt.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/methods_pt.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/localization/methods_pt.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/localization/methods_pt.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/package.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/package.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/package.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/package.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/events.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/events.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/events.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/events.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/errorIcon.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/errorIcon.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/errorIcon.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/errorIcon.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/firebug.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/firebug.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/firebug.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/firebug.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/firebug.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/firebug.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/firebug.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/firebug.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/firebug.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/firebug.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/firebug.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/firebug.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/firebugx.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/firebugx.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/firebugx.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/firebugx.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/infoIcon.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/infoIcon.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/infoIcon.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/infoIcon.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/warningIcon.png b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/warningIcon.png similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/firebug/warningIcon.png rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/firebug/warningIcon.png diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/jquery.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/jquery.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/jquery.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/jquery.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/large.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/large.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/large.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/large.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/messages.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/messages.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/messages.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/messages.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/methods.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/methods.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/methods.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/methods.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/qunit/qunit.css b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/qunit/qunit.css similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/qunit/qunit.css rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/qunit/qunit.css diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/qunit/qunit.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/qunit/qunit.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/qunit/qunit.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/qunit/qunit.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/rules.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/rules.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/rules.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/rules.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/selects/index.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/selects/index.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/selects/index.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/selects/index.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/tabs.html b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/tabs.html similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/tabs.html rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/tabs.html diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/test.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/test.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/test/test.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/test/test.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/todo b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/todo similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/todo rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/todo diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery-validation/validation.jquery.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/validation.jquery.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery-validation/validation.jquery.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery-validation/validation.jquery.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/.bower.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/.bower.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/.bower.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/.bower.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/MIT-LICENSE.txt b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/MIT-LICENSE.txt similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/MIT-LICENSE.txt rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/MIT-LICENSE.txt diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/bower.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/bower.json similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/bower.json rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/bower.json diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/dist/jquery.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/dist/jquery.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/dist/jquery.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/dist/jquery.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/dist/jquery.min.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/dist/jquery.min.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/dist/jquery.min.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/dist/jquery.min.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/dist/jquery.min.map b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/dist/jquery.min.map similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/dist/jquery.min.map rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/dist/jquery.min.map diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/jsonp.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/jsonp.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/jsonp.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/jsonp.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/load.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/load.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/load.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/load.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/parseJSON.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/parseJSON.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/parseJSON.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/parseJSON.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/parseXML.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/parseXML.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/parseXML.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/parseXML.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/script.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/script.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/script.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/script.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/var/nonce.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/var/nonce.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/var/nonce.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/var/nonce.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/var/rquery.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/var/rquery.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/var/rquery.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/var/rquery.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/xhr.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/xhr.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/ajax/xhr.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/ajax/xhr.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/attributes.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/attributes.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/attributes.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/attributes.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/attributes/attr.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/attributes/attr.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/attributes/attr.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/attributes/attr.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/attributes/classes.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/attributes/classes.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/attributes/classes.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/attributes/classes.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/attributes/prop.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/attributes/prop.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/attributes/prop.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/attributes/prop.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/attributes/support.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/attributes/support.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/attributes/support.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/attributes/support.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/attributes/val.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/attributes/val.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/attributes/val.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/attributes/val.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/callbacks.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/callbacks.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/callbacks.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/callbacks.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/core.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/core.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/core.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/core.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/core/access.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/core/access.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/core/access.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/core/access.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/core/init.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/core/init.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/core/init.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/core/init.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/core/parseHTML.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/core/parseHTML.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/core/parseHTML.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/core/parseHTML.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/core/ready.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/core/ready.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/core/ready.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/core/ready.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/core/var/rsingleTag.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/core/var/rsingleTag.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/core/var/rsingleTag.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/core/var/rsingleTag.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/css.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/css.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/addGetHookIf.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/addGetHookIf.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/addGetHookIf.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/addGetHookIf.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/curCSS.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/curCSS.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/curCSS.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/curCSS.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/defaultDisplay.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/defaultDisplay.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/defaultDisplay.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/defaultDisplay.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/hiddenVisibleSelectors.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/hiddenVisibleSelectors.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/hiddenVisibleSelectors.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/hiddenVisibleSelectors.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/support.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/support.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/support.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/support.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/swap.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/swap.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/swap.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/swap.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/var/cssExpand.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/var/cssExpand.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/var/cssExpand.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/var/cssExpand.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/var/getStyles.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/var/getStyles.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/var/getStyles.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/var/getStyles.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/var/isHidden.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/var/isHidden.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/var/isHidden.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/var/isHidden.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/var/rmargin.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/var/rmargin.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/var/rmargin.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/var/rmargin.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/var/rnumnonpx.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/var/rnumnonpx.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/css/var/rnumnonpx.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/css/var/rnumnonpx.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/data.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/data.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/data.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/data.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/data/Data.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/data/Data.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/data/Data.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/data/Data.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/data/accepts.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/data/accepts.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/data/accepts.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/data/accepts.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/data/var/data_priv.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/data/var/data_priv.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/data/var/data_priv.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/data/var/data_priv.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/data/var/data_user.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/data/var/data_user.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/data/var/data_user.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/data/var/data_user.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/deferred.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/deferred.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/deferred.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/deferred.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/deprecated.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/deprecated.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/deprecated.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/deprecated.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/dimensions.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/dimensions.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/dimensions.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/dimensions.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/effects.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/effects.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/effects.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/effects.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/effects/Tween.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/effects/Tween.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/effects/Tween.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/effects/Tween.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/effects/animatedSelector.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/effects/animatedSelector.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/effects/animatedSelector.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/effects/animatedSelector.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/event.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/event.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/event.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/event.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/event/ajax.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/event/ajax.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/event/ajax.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/event/ajax.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/event/alias.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/event/alias.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/event/alias.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/event/alias.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/event/support.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/event/support.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/event/support.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/event/support.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/exports/amd.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/exports/amd.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/exports/amd.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/exports/amd.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/exports/global.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/exports/global.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/exports/global.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/exports/global.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/intro.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/intro.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/intro.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/intro.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/jquery.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/jquery.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/jquery.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/jquery.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/manipulation.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/manipulation.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/manipulation.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/manipulation.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/manipulation/_evalUrl.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/manipulation/_evalUrl.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/manipulation/_evalUrl.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/manipulation/_evalUrl.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/manipulation/support.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/manipulation/support.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/manipulation/support.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/manipulation/support.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/manipulation/var/rcheckableType.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/manipulation/var/rcheckableType.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/manipulation/var/rcheckableType.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/manipulation/var/rcheckableType.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/offset.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/offset.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/offset.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/offset.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/outro.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/outro.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/outro.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/outro.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/queue.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/queue.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/queue.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/queue.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/queue/delay.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/queue/delay.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/queue/delay.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/queue/delay.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/selector-native.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/selector-native.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/selector-native.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/selector-native.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/selector-sizzle.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/selector-sizzle.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/selector-sizzle.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/selector-sizzle.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/selector.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/selector.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/selector.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/selector.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/serialize.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/serialize.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/serialize.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/serialize.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/sizzle/dist/sizzle.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/sizzle/dist/sizzle.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/sizzle/dist/sizzle.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/sizzle/dist/sizzle.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/sizzle/dist/sizzle.min.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/sizzle/dist/sizzle.min.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/sizzle/dist/sizzle.min.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/sizzle/dist/sizzle.min.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/sizzle/dist/sizzle.min.map b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/sizzle/dist/sizzle.min.map similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/sizzle/dist/sizzle.min.map rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/sizzle/dist/sizzle.min.map diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/traversing.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/traversing.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/traversing.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/traversing.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/traversing/findFilter.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/traversing/findFilter.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/traversing/findFilter.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/traversing/findFilter.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/traversing/var/rneedsContext.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/traversing/var/rneedsContext.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/traversing/var/rneedsContext.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/traversing/var/rneedsContext.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/arr.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/arr.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/arr.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/arr.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/class2type.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/class2type.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/class2type.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/class2type.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/concat.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/concat.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/concat.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/concat.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/hasOwn.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/hasOwn.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/hasOwn.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/hasOwn.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/indexOf.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/indexOf.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/indexOf.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/indexOf.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/pnum.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/pnum.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/pnum.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/pnum.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/push.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/push.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/push.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/push.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/rnotwhite.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/rnotwhite.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/rnotwhite.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/rnotwhite.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/slice.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/slice.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/slice.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/slice.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/strundefined.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/strundefined.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/strundefined.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/strundefined.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/support.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/support.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/support.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/support.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/toString.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/toString.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/var/toString.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/var/toString.js diff --git a/sandbox/Mvc.Server/wwwroot/lib/jquery/src/wrap.js b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/wrap.js similarity index 100% rename from sandbox/Mvc.Server/wwwroot/lib/jquery/src/wrap.js rename to sandbox/OpenIddict.Sandbox.AspNetCore.Server/wwwroot/lib/jquery/src/wrap.js diff --git a/src/OpenIddict.Abstractions/OpenIddict.Abstractions.csproj b/src/OpenIddict.Abstractions/OpenIddict.Abstractions.csproj index 9dcbe0a3..d261bf00 100644 --- a/src/OpenIddict.Abstractions/OpenIddict.Abstractions.csproj +++ b/src/OpenIddict.Abstractions/OpenIddict.Abstractions.csproj @@ -12,6 +12,7 @@ + diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index 4b59d442..98b6973d 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -63,6 +63,7 @@ public static class OpenIddictConstants public const string AuthenticationContextReference = "acr"; public const string AuthenticationMethodReference = "amr"; public const string AuthenticationTime = "auth_time"; + public const string AuthorizationServer = "as"; public const string AuthorizedParty = "azp"; public const string Birthdate = "birthdate"; public const string ClientId = "client_id"; @@ -93,10 +94,12 @@ public static class OpenIddictConstants public const string PreferredUsername = "preferred_username"; public const string Profile = "profile"; public const string Region = "region"; + public const string RequestForgeryProtection = "rfp"; public const string Role = "role"; public const string Scope = "scope"; public const string StreetAddress = "street_address"; public const string Subject = "sub"; + public const string TargetLinkUri = "target_link_uri"; public const string TokenType = "token_type"; public const string TokenUsage = "token_usage"; public const string UpdatedAt = "updated_at"; @@ -118,17 +121,22 @@ public static class OpenIddictConstants public const string ClaimDestinationsMap = "oi_cl_dstn"; public const string CodeChallenge = "oi_cd_chlg"; public const string CodeChallengeMethod = "oi_cd_chlg_meth"; + public const string CodeVerifier = "oi_cd_vrf"; public const string CreationDate = "oi_crt_dt"; public const string DeviceCodeId = "oi_dvc_id"; public const string DeviceCodeLifetime = "oi_dvc_lft"; public const string ExpirationDate = "oi_exp_dt"; + public const string GrantType = "oi_grt_typ"; public const string IdentityTokenLifetime = "oi_idt_lft"; public const string Nonce = "oi_nce"; public const string Presenter = "oi_prst"; public const string RedirectUri = "oi_reduri"; public const string RefreshTokenLifetime = "oi_reft_lft"; public const string Resource = "oi_rsrc"; + public const string ResponseType = "oi_rsp_typ"; + public const string SigningAlgorithm = "oi_sign_alg"; public const string Scope = "oi_scp"; + public const string StateTokenLifetime = "oi_stet_lft"; public const string TokenId = "oi_tkn_id"; public const string TokenType = "oi_tkn_typ"; public const string UserCodeLifetime = "oi_usrc_lft"; @@ -223,6 +231,7 @@ public static class OpenIddictConstants public const string AuthorizationCode = "oi_auc+jwt"; public const string DeviceCode = "oi_dvc+jwt"; public const string RefreshToken = "oi_reft+jwt"; + public const string StateToken = "oi_stet+jwt"; public const string UserCode = "oi_usrc+jwt"; } } @@ -231,6 +240,7 @@ public static class OpenIddictConstants { public const string AcrValuesSupported = "acr_values_supported"; public const string AuthorizationEndpoint = "authorization_endpoint"; + public const string AuthorizationResponseIssParameterSupported = "authorization_response_iss_parameter_supported"; public const string ClaimsLocalesSupported = "claims_locales_supported"; public const string ClaimsParameterSupported = "claims_parameter_supported"; public const string ClaimsSupported = "claims_supported"; @@ -301,6 +311,7 @@ public static class OpenIddictConstants public const string IdentityProvider = "identity_provider"; public const string IdToken = "id_token"; public const string IdTokenHint = "id_token_hint"; + public const string Iss = "iss"; public const string LoginHint = "login_hint"; public const string Keys = "keys"; public const string MaxAge = "max_age"; @@ -427,6 +438,7 @@ public static class OpenIddictConstants { public static readonly char[] Ampersand = { '&' }; public static readonly char[] Dash = { '-' }; + public static readonly char[] Dot = { '.' }; public static readonly char[] Space = { ' ' }; } @@ -469,6 +481,7 @@ public static class OpenIddictConstants public const string DeviceCode = "device_code"; public const string IdToken = "id_token"; public const string RefreshToken = "refresh_token"; + public const string StateToken = "state_token"; public const string UserCode = "user_code"; } diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 3cdfcb4c..08ee8d03 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1119,6 +1119,75 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag The payload of this authentication ticket was serialized using an unsupported formatter version. + + The OpenIddict ASP.NET Core client handler cannot be registered as an authentication scheme. +This may indicate that an instance of another handler was registered with the same scheme. + + + The OpenIddict ASP.NET Core client handler cannot be used as the default authentication/sign-in/sign-out handler. +Make sure that neither DefaultAuthenticateScheme, DefaultSignInScheme, DefaultSignOutScheme nor DefaultScheme point to an instance of the OpenIddict ASP.NET Core client handler. + + + An identity cannot be extracted from this request. +This generally indicates that the OpenIddict client stack was asked to validate a token for an endpoint it doesn't manage. +To validate tokens received by custom API endpoints, the OpenIddict validation handler (e.g OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme or OpenIddictValidationOwinDefaults.AuthenticationType) must be used instead. + + + The authorization server information cannot be extracted from the state principal. + + + The client registration corresponding to the specified issuer cannot be found in the client options. + + + The signing algorithm cannot be resolved from the specified frontchannel identity token. + + + The negotiated grant type cannot be extracted from the state principal. + + + The signing algorithm cannot be resolved from the specified backchannel identity token. + + + The specified grant type is not supported. + + + A common grant type supported by both the client and the server couldn't be negotiated automatically. If the error persists, consider specifying a list of allowed grant types in the client registration and ensure the supported grant types listed in the authorization server configuration are appropriate. + + + A common response type combination supported by both the client and the server couldn't be negotiated automatically. If the error persists, consider specifying a list of allowed response type combinations in the client registration and ensure the supported response type combinations listed in the authorization server configuration are appropriate. + + + A common response mode supported by both the client and the server couldn't be negotiated automatically. If the error persists, consider specifying a list of allowed response modes in the client registration and ensure the supported response modes listed in the authorization server configuration are appropriate. + + + A redirection URI must be specified in the client registration options when using OpenID Connect. + + + The '{0}' cannot be resolved from the authorization server configuration or doesn't represent a valid absolute URI, which may indicate this endpoint is not supported or is not enabled in the server configuration. + + + The redirection request was not correctly extracted. +To extract authorization requests, create a class implementing 'IOpenIddictClientHandler<ExtractRedirectionRequestContext>' and register it using 'services.AddOpenIddict().AddClient().AddEventHandler()'. + + + The redirection response was not correctly applied. +To apply redirection responses, create a class implementing 'IOpenIddictClientHandler<ApplyRedirectionResponseContext>' and register it using 'services.AddOpenIddict().AddClient().AddEventHandler()'. + + + No client registration was found in the client options. To add a registration, use 'services.AddOpenIddict().AddClient().AddRegistration()'. + + + No issuer was specified in the challenge properties. When multiple clients are registered, an issuer must be specified in the challenge properties. + + + The specified issuer is not a valid or absolute URL. + + + The issuer extracted from the server configuration metadata doesn't match the expected value. + + + The specified list of valid token types is not valid. + The security token is missing. @@ -1420,10 +1489,7 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag No JWKS endpoint could be found in the server configuration. - A server configuration containing an invalid JWKS endpoint URL was returned. - - - A server configuration containing an invalid introspection endpoint URL was returned. + A server configuration containing an invalid '{0}' URL was returned. The JWKS document didn't contain a valid '{0}' node with at least one key. @@ -1476,6 +1542,42 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag The token usage returned by the authorization server is not supported. + + The specified '{0}' parameter doesn't match the authorization server the authorization request was initially sent to. + + + The '{0}' parameter cannot be used when '{1}' is not supported by the authorization server. + + + The '{0}' claim extracted from the specified frontchannel identity token is malformed or isn't of the expected type. + + + The mandatory '{0}' claim cannot be found in the specified frontchannel identity token. + + + The specified frontchannel identity token cannot be used with this client application. + + + The '{0}' claim returned in the specified frontchannel identity token doesn't match the expected value. + + + The '{0}' claim extracted from the specified backchannel identity token is malformed or isn't of the expected type. + + + The mandatory '{0}' claim cannot be found in the specified backchannel identity token. + + + The specified backchannel identity token cannot be used with this client application. + + + The '{0}' claim returned in the specified backchannel identity token doesn't match the expected value. + + + No correlation cookie associated with the specified state can be found. Please try logging in again. If the error persists, please contact the website owner. + + + The specified state token is not valid in this context. + The '{0}' parameter shouldn't be null or empty at this point. @@ -1515,6 +1617,9 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag EC-based keys should have a non-null OID raw value or friendly name. + + The issuer should be a valid absolute URL at this point. + An error occurred while validating the token '{Token}'. @@ -2009,6 +2114,15 @@ This may indicate that the hashed entry is corrupted or malformed. The authorization request was rejected because the application '{ClientId}' was not allowed to use the '{ResponseType}' response type. + + The redirection request was successfully extracted: {Request}. + + + The redirection request was successfully validated. + + + The redirection request was successfully validated. + https://documentation.openiddict.com/errors/{0} diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs new file mode 100644 index 00000000..7f75f244 --- /dev/null +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.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 Microsoft.IdentityModel.Tokens; + +namespace OpenIddict.Abstractions; + +/// +/// Represents the configuration of an authorization server. +/// +/// +/// Note: depending on the stack used to produce this instance, only a few select properties may be available. +/// +public class OpenIddictConfiguration +{ + /// + /// Gets or sets the address of the authorization endpoint. + /// + public Uri? AuthorizationEndpoint { get; set; } + + /// + /// Gets or sets a boolean indicating whether the "iss" parameter is returned in authorization responses. + /// + public bool? AuthorizationResponseIssParameterSupported { get; set; } + + /// + /// Gets the code challenge methods supported by the server. + /// + public HashSet CodeChallengeMethodsSupported { get; } = new(StringComparer.Ordinal); + + /// + /// Gets the grant types supported by the server. + /// + public HashSet GrantTypesSupported { get; } = new(StringComparer.Ordinal); + + /// + /// Gets or sets the address of the introspection endpoint. + /// + public Uri? IntrospectionEndpoint { get; set; } + + /// + /// Gets the client authentication methods supported by the introspection endpoint. + /// + public HashSet IntrospectionEndpointAuthMethodsSupported { get; } = new(StringComparer.Ordinal); + + /// + /// Gets or sets the address of the issuer. + /// + public Uri? Issuer { get; set; } + + /// + /// Gets or sets the JSON Web Key set containing the keys exposed by the server. + /// + public JsonWebKeySet? JsonWebKeySet { get; set; } + + /// + /// Gets or sets the address of the JWKS endpoint. + /// + public Uri? JwksUri { get; set; } + + /// + /// Gets the response mode supported by the server. + /// + public HashSet ResponseModesSupported { get; } = new(StringComparer.Ordinal); + + /// + /// Gets the response types supported by the server. + /// + public HashSet ResponseTypesSupported { get; } = new(StringComparer.Ordinal); + + /// + /// Gets the scopes supported by the server. + /// + public HashSet ScopesSupported { get; } = new(StringComparer.Ordinal); + + /// + /// Gets the signing keys extracted from the JSON Web Key set. + /// + public List SigningKeys { get; } = new(); + + /// + /// Gets or sets the address of the token endpoint. + /// + public Uri? TokenEndpoint { get; set; } + + /// + /// Gets the client authentication methods supported by the token endpoint. + /// + public HashSet TokenEndpointAuthMethodsSupported { get; } = new(StringComparer.Ordinal); +} diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs index 9224898e..f4407188 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs @@ -1262,6 +1262,14 @@ public static class OpenIddictExtensions public static TimeSpan? GetRefreshTokenLifetime(this ClaimsPrincipal principal) => GetLifetime(principal, Claims.Private.RefreshTokenLifetime); + /// + /// Gets the state token lifetime associated with the claims principal. + /// + /// The claims principal. + /// The state token lifetime or null if the claim cannot be found. + public static TimeSpan? GetStateTokenLifetime(this ClaimsPrincipal principal) + => GetLifetime(principal, Claims.Private.StateTokenLifetime); + /// /// Gets the user code lifetime associated with the claims principal. /// @@ -1582,6 +1590,15 @@ public static class OpenIddictExtensions public static ClaimsPrincipal SetRefreshTokenLifetime(this ClaimsPrincipal principal, TimeSpan? lifetime) => principal.SetClaim(Claims.Private.RefreshTokenLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture)); + /// + /// Sets the state token lifetime associated with the claims principal. + /// + /// The claims principal. + /// The state token lifetime to store. + /// The claims principal. + public static ClaimsPrincipal SetStateTokenLifetime(this ClaimsPrincipal principal, TimeSpan? lifetime) + => principal.SetClaim(Claims.Private.StateTokenLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture)); + /// /// Sets the user code lifetime associated with the claims principal. /// diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs index c41e451f..b7bc993a 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs @@ -243,6 +243,15 @@ public class OpenIddictRequest : OpenIddictMessage set => SetParameter(OpenIddictConstants.Parameters.IdentityProvider, 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 "id_token_hint" parameter. /// diff --git a/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj b/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj index ed547915..e73cdcdf 100644 --- a/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj +++ b/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj @@ -8,11 +8,12 @@ Versatile OpenID Connect stack for ASP.NET Core. - $(PackageTags);aspnetcore;server;validation + $(PackageTags);aspnetcore;client;server;validation + diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj b/src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj new file mode 100644 index 00000000..40b1e439 --- /dev/null +++ b/src/OpenIddict.Client.AspNetCore/OpenIddict.Client.AspNetCore.csproj @@ -0,0 +1,44 @@ + + + + net461;netcoreapp3.1;net5.0;net6.0 + + + + ASP.NET Core integration package for the OpenIddict client services. + $(PackageTags);client;aspnetcore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs new file mode 100644 index 00000000..552f6cc2 --- /dev/null +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs @@ -0,0 +1,103 @@ +/* + * 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.ComponentModel; +using Microsoft.AspNetCore; +using OpenIddict.Client.AspNetCore; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Exposes the necessary methods required to configure +/// the OpenIddict client ASP.NET Core integration. +/// +public class OpenIddictClientAspNetCoreBuilder +{ + /// + /// Initializes a new instance of . + /// + /// The services collection. + public OpenIddictClientAspNetCoreBuilder(IServiceCollection services) + => Services = services ?? throw new ArgumentNullException(nameof(services)); + + /// + /// Gets the services collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IServiceCollection Services { get; } + + /// + /// Amends the default OpenIddict client ASP.NET Core configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public OpenIddictClientAspNetCoreBuilder Configure(Action configuration) + { + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(configuration); + + return this; + } + + /// + /// Enables the pass-through mode for the OpenID Connect redirection 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 OpenIddictClientAspNetCoreBuilder EnableRedirectionEndpointPassthrough() + => Configure(options => options.EnableRedirectionEndpointPassthrough = 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 . + /// + /// + /// Important: the error pass-through mode cannot be used when the status code pages integration is enabled. + /// + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictClientAspNetCoreBuilder EnableErrorPassthrough() + => Configure(options => options.EnableErrorPassthrough = true); + + /// + /// Enables status code pages integration support. Once enabled, errors + /// generated by the interactive endpoints can be handled by ASP.NET Core. + /// + /// The . + public OpenIddictClientAspNetCoreBuilder EnableStatusCodePagesIntegration() + => Configure(options => options.EnableStatusCodePagesIntegration = 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(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.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs new file mode 100644 index 00000000..349ae54f --- /dev/null +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.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 Microsoft.Extensions.Options; + +namespace OpenIddict.Client.AspNetCore; + +/// +/// Contains the methods required to ensure that the OpenIddict client configuration is valid. +/// +public class OpenIddictClientAspNetCoreConfiguration : IConfigureOptions, + IConfigureOptions, + IPostConfigureOptions +{ + /// + /// Registers the OpenIddict client handler in the global authentication options. + /// + /// The options instance to initialize. + public void Configure(AuthenticationOptions options) + { + if (options is 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(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme, out var builder) && + builder.HandlerType != typeof(OpenIddictClientAspNetCoreHandler)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0288)); + } + + options.AddScheme( + OpenIddictClientAspNetCoreDefaults.AuthenticationScheme, displayName: null); + } + + public void Configure(OpenIddictClientOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + // Register the built-in event handlers used by the OpenIddict ASP.NET Core client components. + options.Handlers.AddRange(OpenIddictClientAspNetCoreHandlers.DefaultHandlers); + } + + /// + /// 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(string name, AuthenticationOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (!TryValidate(options.SchemeMap, options.DefaultAuthenticateScheme) || + !TryValidate(options.SchemeMap, options.DefaultScheme) || + !TryValidate(options.SchemeMap, options.DefaultSignInScheme) || + !TryValidate(options.SchemeMap, options.DefaultSignOutScheme)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0289)); + } + + static bool TryValidate(IDictionary map, string? scheme) + { + // If the scheme was not set or if it cannot be found in the map, return true. + if (string.IsNullOrEmpty(scheme) || !map.TryGetValue(scheme, out var builder)) + { + return true; + } + + return builder.HandlerType != typeof(OpenIddictClientAspNetCoreHandler); + } + } +} diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs new file mode 100644 index 00000000..55ed7def --- /dev/null +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs @@ -0,0 +1,39 @@ +/* + * 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.Client.AspNetCore; + +/// +/// Exposes common constants used by the OpenIddict ASP.NET Core host. +/// +public static class OpenIddictClientAspNetCoreConstants +{ + public static class Properties + { + public const string BackchannelAccessTokenPrincipal = ".backchannel_access_token_principal"; + public const string BackchannelIdentityTokenPrincipal = ".backchannel_id_token_principal"; + public const string BackchannelRefreshTokenPrincipal = ".backchannel_refresh_token_principal"; + public const string FrontchannelAccessTokenPrincipal = ".frontchannel_access_token_principal"; + public const string FrontchannelAuthorizationCodePrincipal = ".frontchannel_authorization_code_principal"; + public const string FrontchannelIdentityTokenPrincipal = ".frontchannel_id_token_principal"; + public const string FrontchannelStateTokenPrincipal = ".frontchannel_state_token_principal"; + public const string Issuer = ".issuer"; + public const string Error = ".error"; + public const string ErrorDescription = ".error_description"; + public const string ErrorUri = ".error_uri"; + } + + public static class Tokens + { + public const string BackchannelAccessToken = "backchannel_access_token"; + public const string BackchannelIdentityToken = "backchannel_id_token"; + public const string BackchannelRefreshToken = "backchannel_refresh_token"; + public const string FrontchannelAccessToken = "frontchannel_access_token"; + public const string FrontchannelAuthorizationCode = "frontchannel_authorization_code"; + public const string FrontchannelIdentityToken = "frontchannel_id_token"; + public const string FrontchannelStateToken = "frontchannel_state_token"; + } +} diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreDefaults.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreDefaults.cs new file mode 100644 index 00000000..cdb9b64e --- /dev/null +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreDefaults.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. + */ + +namespace OpenIddict.Client.AspNetCore; + +/// +/// Exposes the default values used by the OpenIddict client handler. +/// +public static class OpenIddictClientAspNetCoreDefaults +{ + /// + /// Default value for . + /// + public const string AuthenticationScheme = "OpenIddict.Client.AspNetCore"; +} diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs new file mode 100644 index 00000000..d9f7e705 --- /dev/null +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs @@ -0,0 +1,83 @@ +/* + * 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.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using OpenIddict.Client; +using OpenIddict.Client.AspNetCore; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Exposes extensions allowing to register the OpenIddict client services. +/// +public static class OpenIddictClientAspNetCoreExtensions +{ + /// + /// Registers the OpenIddict client 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 OpenIddictClientAspNetCoreBuilder UseAspNetCore(this OpenIddictClientBuilder builder) + { + if (builder is 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 client components. + // Note: the order used here is not important, as the actual order is set in the options. + builder.Services.TryAdd(OpenIddictClientAspNetCoreHandlers.DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); + + // Register the built-in filters used by the default OpenIddict ASP.NET Core client event handlers. + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + + // Register the option initializer used by the OpenIddict ASP.NET Core client integration services. + // Note: TryAddEnumerable() is used here to ensure the initializers are only registered once. + builder.Services.TryAddEnumerable(new[] + { + ServiceDescriptor.Singleton, OpenIddictClientAspNetCoreConfiguration>(), + ServiceDescriptor.Singleton, OpenIddictClientAspNetCoreConfiguration>(), + + ServiceDescriptor.Singleton, OpenIddictClientAspNetCoreConfiguration>() + }); + + return new OpenIddictClientAspNetCoreBuilder(builder.Services); + } + + /// + /// Registers the OpenIddict client 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 client services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictClientBuilder UseAspNetCore( + this OpenIddictClientBuilder builder, Action configuration) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + configuration(builder.UseAspNetCore()); + + return builder; + } +} diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreFeature.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreFeature.cs new file mode 100644 index 00000000..28875f65 --- /dev/null +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreFeature.cs @@ -0,0 +1,19 @@ +/* + * 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.Client.AspNetCore; + +/// +/// Exposes the current client transaction to the ASP.NET Core host. +/// +public class OpenIddictClientAspNetCoreFeature +{ + /// + /// Gets or sets the client transaction that encapsulates all specific + /// information about an individual OpenID Connect client request. + /// + public OpenIddictClientTransaction? Transaction { get; set; } +} diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs new file mode 100644 index 00000000..d9c76139 --- /dev/null +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs @@ -0,0 +1,317 @@ +/* + * 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.Text.Encodings.Web; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using static OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants; +using Properties = OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants.Properties; + +namespace OpenIddict.Client.AspNetCore; + +/// +/// Provides the logic necessary to extract, validate and handle OpenID Connect requests. +/// +public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler, + IAuthenticationRequestHandler +{ + private readonly IOpenIddictClientDispatcher _dispatcher; + private readonly IOpenIddictClientFactory _factory; + + /// + /// Creates a new instance of the class. + /// + public OpenIddictClientAspNetCoreHandler( + IOpenIddictClientDispatcher dispatcher, + IOpenIddictClientFactory factory, + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) + : base(options, logger, encoder, clock) + { + _dispatcher = dispatcher; + _factory = factory; + } + + /// + 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 is null) + { + // Create a new transaction and attach the HTTP request to make it available to the ASP.NET Core handlers. + transaction = await _factory.CreateTransactionAsync(); + transaction.Properties[typeof(HttpRequest).FullName!] = new WeakReference(Request); + + // Attach the OpenIddict client transaction to the ASP.NET Core features + // so that it can retrieved while performing sign-in/sign-out operations. + Context.Features.Set(new OpenIddictClientAspNetCoreFeature { Transaction = transaction }); + } + + var context = new ProcessRequestContext(transaction); + await _dispatcher.DispatchAsync(context); + + if (context.IsRequestHandled) + { + return true; + } + + else if (context.IsRequestSkipped) + { + return false; + } + + else if (context.IsRejected) + { + var notification = new ProcessErrorContext(transaction) + { + Error = context.Error ?? Errors.InvalidRequest, + ErrorDescription = context.ErrorDescription, + ErrorUri = context.ErrorUri, + Response = new OpenIddictResponse() + }; + + await _dispatcher.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + return true; + } + + else if (notification.IsRequestSkipped) + { + return false; + } + + throw new InvalidOperationException(SR.GetResourceString(SR.ID0111)); + } + + return false; + } + + /// + protected override async Task HandleAuthenticateAsync() + { + var transaction = Context.Features.Get()?.Transaction ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0112)); + + // Note: in many cases, the authentication token was already validated by the time this action is called + // (generally later in the pipeline, when using the pass-through mode). To avoid having to re-validate it, + // the authentication context is resolved from the transaction. If it's not available, a new one is created. + var context = transaction.GetProperty(typeof(ProcessAuthenticationContext).FullName!); + if (context is null) + { + context = new ProcessAuthenticationContext(transaction); + await _dispatcher.DispatchAsync(context); + + // Store the context object in the transaction so it can be later retrieved by handlers + // that want to access the authentication result without triggering a new authentication flow. + transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName!, context); + } + + if (context.IsRequestHandled || context.IsRequestSkipped) + { + return AuthenticateResult.NoResult(); + } + + else if (context.IsRejected) + { + var properties = new AuthenticationProperties(new Dictionary + { + [Properties.Error] = context.Error, + [Properties.ErrorDescription] = context.ErrorDescription, + [Properties.ErrorUri] = context.ErrorUri + }); + + return AuthenticateResult.Fail(SR.GetResourceString(SR.ID0113), properties); + } + + else + { + // A single main claims-based principal instance can be attached to an authentication ticket. + // To return the most appropriate one, the principal is selected based on the endpoint type. + // Independently of the selected main principal, all principals resolved from validated tokens + // are attached to the authentication properties bag so they can be accessed from user code. + var principal = context.EndpointType switch + { + // Note: the OpenIddict client handler can be used as a pure OAuth 2.0-only stack for + // delegation scenarios where the identity of the user is not needed. In this case, + // since no principal can be resolved from a token or a userinfo response to construct + // a user identity, a fake one containing an "unauthenticated" identity (i.e with its + // AuthenticationType property deliberately left to null) is used to allow ASP.NET Core + // to return a "successful" authentication result for these delegation-only scenarios. + OpenIddictClientEndpointType.Redirection => + context.BackchannelIdentityTokenPrincipal ?? + context.FrontchannelIdentityTokenPrincipal ?? + new ClaimsPrincipal(new ClaimsIdentity()), + + _ => null + }; + + if (principal is null) + { + return AuthenticateResult.NoResult(); + } + + var properties = new AuthenticationProperties + { + ExpiresUtc = principal.GetExpirationDate(), + IssuedUtc = principal.GetCreationDate(), + + // Restore the return URL using the "target_link_uri" that was stored + // in the state token when the challenge operation started, if available. + RedirectUri = context.FrontchannelStateTokenPrincipal?.GetClaim(Claims.TargetLinkUri) + }; + + List? tokens = null; + + // Attach the tokens to allow any ASP.NET Core component (e.g a controller) + // to retrieve them (e.g to make an API request to another application). + + if (!string.IsNullOrEmpty(context.BackchannelAccessToken)) + { + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken + { + Name = Tokens.BackchannelAccessToken, + Value = context.BackchannelAccessToken + }); + + properties.SetParameter(Properties.BackchannelAccessTokenPrincipal, context.BackchannelAccessTokenPrincipal); + } + + if (!string.IsNullOrEmpty(context.BackchannelIdentityToken)) + { + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken + { + Name = Tokens.BackchannelIdentityToken, + Value = context.BackchannelIdentityToken + }); + + properties.SetParameter(Properties.BackchannelIdentityTokenPrincipal, context.BackchannelIdentityTokenPrincipal); + } + + if (!string.IsNullOrEmpty(context.BackchannelRefreshToken)) + { + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken + { + Name = Tokens.BackchannelRefreshToken, + Value = context.BackchannelRefreshToken + }); + + properties.SetParameter(Properties.BackchannelRefreshTokenPrincipal, context.BackchannelRefreshTokenPrincipal); + } + + if (!string.IsNullOrEmpty(context.FrontchannelAccessToken)) + { + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken + { + Name = Tokens.FrontchannelAccessToken, + Value = context.FrontchannelAccessToken + }); + + properties.SetParameter(Properties.FrontchannelAccessTokenPrincipal, context.FrontchannelAccessTokenPrincipal); + } + + if (!string.IsNullOrEmpty(context.FrontchannelAuthorizationCode)) + { + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken + { + Name = Tokens.FrontchannelAuthorizationCode, + Value = context.FrontchannelAuthorizationCode + }); + + properties.SetParameter(Properties.FrontchannelAuthorizationCodePrincipal, context.FrontchannelAuthorizationCodePrincipal); + } + + if (!string.IsNullOrEmpty(context.FrontchannelIdentityToken)) + { + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken + { + Name = Tokens.FrontchannelIdentityToken, + Value = context.FrontchannelIdentityToken + }); + + properties.SetParameter(Properties.FrontchannelIdentityTokenPrincipal, context.FrontchannelIdentityTokenPrincipal); + } + + if (!string.IsNullOrEmpty(context.FrontchannelStateToken)) + { + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken + { + Name = Tokens.FrontchannelStateToken, + Value = context.FrontchannelStateToken + }); + + properties.SetParameter(Properties.FrontchannelStateTokenPrincipal, context.FrontchannelStateTokenPrincipal); + } + + if (tokens is { Count: > 0 }) + { + properties.StoreTokens(tokens); + } + + return AuthenticateResult.Success(new AuthenticationTicket(principal, properties, + OpenIddictClientAspNetCoreDefaults.AuthenticationScheme)); + } + } + + /// + protected override async Task HandleChallengeAsync(AuthenticationProperties? properties) + { + var transaction = Context.Features.Get()?.Transaction ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0112)); + + transaction.Properties[typeof(AuthenticationProperties).FullName!] = properties ?? new AuthenticationProperties(); + + var context = new ProcessChallengeContext(transaction) + { + Principal = new ClaimsPrincipal(new ClaimsIdentity()), + Request = new OpenIddictRequest() + }; + + await _dispatcher.DispatchAsync(context); + + if (context.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + else if (context.IsRejected) + { + var notification = new ProcessErrorContext(transaction) + { + Error = context.Error ?? Errors.InvalidRequest, + ErrorDescription = context.ErrorDescription, + ErrorUri = context.ErrorUri, + Response = new OpenIddictResponse() + }; + + await _dispatcher.DispatchAsync(notification); + + if (notification.IsRequestHandled || context.IsRequestSkipped) + { + return; + } + + throw new InvalidOperationException(SR.GetResourceString(SR.ID0111)); + } + } + + /// + protected override Task HandleForbiddenAsync(AuthenticationProperties? properties) + => HandleChallengeAsync(properties); +} diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlerFilters.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlerFilters.cs new file mode 100644 index 00000000..975ca33b --- /dev/null +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlerFilters.cs @@ -0,0 +1,98 @@ +/* + * 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.ComponentModel; +using Microsoft.AspNetCore; +using Microsoft.Extensions.Options; + +namespace OpenIddict.Client.AspNetCore; + +/// +/// Contains a collection of event handler filters commonly used by the ASP.NET Core handlers. +/// +[EditorBrowsable(EditorBrowsableState.Advanced)] +public static class OpenIddictClientAspNetCoreHandlerFilters +{ + /// + /// Represents a filter that excludes the associated handlers if the + /// pass-through mode was not enabled for the authorization endpoint. + /// + public class RequireRedirectionEndpointPassthroughEnabled : IOpenIddictClientHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireRedirectionEndpointPassthroughEnabled(IOptionsMonitor options) + => _options = options; + + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(_options.CurrentValue.EnableRedirectionEndpointPassthrough); + } + } + + /// + /// Represents a filter that excludes the associated handlers if error pass-through was not enabled. + /// + public class RequireErrorPassthroughEnabled : IOpenIddictClientHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireErrorPassthroughEnabled(IOptionsMonitor options) + => _options = options; + + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(_options.CurrentValue.EnableErrorPassthrough); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no ASP.NET Core request can be found. + /// + public class RequireHttpRequest : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.Transaction.GetHttpRequest() is not null); + } + } + + /// + /// Represents a filter that excludes the associated handlers if status code pages support was not enabled. + /// + public class RequireStatusCodePagesIntegrationEnabled : IOpenIddictClientHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireStatusCodePagesIntegrationEnabled(IOptionsMonitor options) + => _options = options; + + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(_options.CurrentValue.EnableStatusCodePagesIntegration); + } + } +} diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.Authentication.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.Authentication.cs new file mode 100644 index 00000000..0b826564 --- /dev/null +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.Authentication.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.Collections.Immutable; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.WebUtilities; + +namespace OpenIddict.Client.AspNetCore; + +public static partial class OpenIddictClientAspNetCoreHandlers +{ + public static class Authentication + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Authorization request processing: + */ + ProcessQueryRequest.Descriptor, + + /* + * Redirection request extraction: + */ + ExtractGetOrPostRequest.Descriptor, + + /* + * Redirection request handling: + */ + EnablePassthroughMode.Descriptor, + + /* + * Redirection response handling: + */ + AttachHttpResponseCode.Descriptor, + AttachCacheControlHeader.Descriptor, + ProcessPassthroughErrorResponse.Descriptor, + ProcessStatusCodePagesErrorResponse.Descriptor, + ProcessLocalErrorResponse.Descriptor); + + /// + /// Contains the logic responsible of processing authorization requests using 302 redirects. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessQueryRequest : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(50_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ApplyAuthorizationRequestContext context) + { + if (context is 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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + } + + // 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. + +#if SUPPORTS_MULTIPLE_VALUES_IN_QUERYHELPERS + var location = QueryHelpers.AddQueryString(context.AuthorizationEndpoint, + from parameter in context.Request.GetParameters() + let values = (string?[]?) parameter.Value + where values is not null + from value in values + where !string.IsNullOrEmpty(value) + select KeyValuePair.Create(parameter.Key, value)); +#else + var location = context.AuthorizationEndpoint; + + foreach (var (key, value) in + from parameter in context.Request.GetParameters() + let values = (string?[]?) parameter.Value + where values is not null + from value in values + where !string.IsNullOrEmpty(value) + select (parameter.Key, Value: value)) + { + location = QueryHelpers.AddQueryString(location, key, value); + } +#endif + response.Redirect(location); + context.HandleRequest(); + + return default; + } + } + } +} diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs new file mode 100644 index 00000000..322192c4 --- /dev/null +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs @@ -0,0 +1,919 @@ +/* + * 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.ComponentModel; +using System.Diagnostics; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Properties = OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants.Properties; + +namespace OpenIddict.Client.AspNetCore; + +[EditorBrowsable(EditorBrowsableState.Never)] +public static partial class OpenIddictClientAspNetCoreHandlers +{ + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Top-level request processing: + */ + InferEndpointType.Descriptor, + + /* + * Authentication processing: + */ + ValidateCorrelationCookie.Descriptor, + + /* + * Challenge processing: + */ + GenerateCorrelationCookie.Descriptor, + ResolveHostChallengeParameters.Descriptor) + .AddRange(Authentication.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 : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MinValue + 50_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is 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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + } + + context.EndpointType = + Matches(request, context.Options.RedirectionEndpointUris) ? OpenIddictClientEndpointType.Redirection : + OpenIddictClientEndpointType.Unknown; + + return default; + + static bool Matches(HttpRequest request, IReadOnlyList addresses) + { + for (var index = 0; index < addresses.Count; index++) + { + var address = addresses[index]; + if (address.IsAbsoluteUri) + { + // If the request host is not available (e.g because HTTP/1.0 was used), ignore absolute URLs. + if (!request.Host.HasValue) + { + continue; + } + + // Create a Uri instance using the request scheme and raw host and compare the two base addresses. + if (!Uri.TryCreate(request.Scheme + Uri.SchemeDelimiter + request.Host, UriKind.Absolute, out Uri? uri) || + !uri.IsWellFormedOriginalString() || uri.Port != address.Port || + !string.Equals(uri.Scheme, address.Scheme, StringComparison.OrdinalIgnoreCase) || + !string.Equals(uri.Host, address.Host, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var path = PathString.FromUriComponent(address); + if (AreEquivalent(path, request.PathBase + request.Path)) + { + return true; + } + } + + else if (address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { + var path = new PathString(address.OriginalString); + if (AreEquivalent(path, request.Path)) + { + return true; + } + } + } + + return false; + + // ASP.NET Core's routing system ignores trailing slashes when determining + // whether the request path matches a registered route, which is not the case + // with PathString, that treats /connect/token and /connect/token/ as different + // addresses. To mitigate this inconsistency, a manual check is used here. + static bool AreEquivalent(PathString left, PathString right) + => left.Equals(right, StringComparison.OrdinalIgnoreCase) || + left.Equals(right + "/", StringComparison.OrdinalIgnoreCase) || + right.Equals(left + "/", StringComparison.OrdinalIgnoreCase); + } + } + } + + /// + /// 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 : IOpenIddictClientHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is 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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + } + + if (HttpMethods.IsGet(request.Method)) + { + context.Transaction.Request = new OpenIddictRequest(request.Query); + } + + else + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6137), request.Method); + + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2084), + uri: SR.FormatID8000(SR.ID2084)); + + return default; + } + + return default; + } + } + + /// + /// 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 : IOpenIddictClientHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractGetRequest.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(TContext context) + { + if (context is 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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + } + + if (HttpMethods.IsGet(request.Method)) + { + context.Transaction.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.LogInformation(SR.GetResourceString(SR.ID6138), HeaderNames.ContentType); + + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2081(HeaderNames.ContentType), + uri: SR.FormatID8000(SR.ID2081)); + + return; + } + + // May have media/type; charset=utf-8, allow partial match. + if (!request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6139), HeaderNames.ContentType, request.ContentType); + + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2082(HeaderNames.ContentType), + uri: SR.FormatID8000(SR.ID2082)); + + return; + } + + context.Transaction.Request = new OpenIddictRequest(await request.ReadFormAsync(request.HttpContext.RequestAborted)); + } + + else + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6137), request.Method); + + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2084), + uri: SR.FormatID8000(SR.ID2084)); + + 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 : IOpenIddictClientHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractGetOrPostRequest.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(TContext context) + { + if (context is 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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + } + + if (HttpMethods.IsPost(request.Method)) + { + // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization + if (string.IsNullOrEmpty(request.ContentType)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6138), HeaderNames.ContentType); + + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2081(HeaderNames.ContentType), + uri: SR.FormatID8000(SR.ID2081)); + + return; + } + + // May have media/type; charset=utf-8, allow partial match. + if (!request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6139), HeaderNames.ContentType, request.ContentType); + + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2082(HeaderNames.ContentType), + uri: SR.FormatID8000(SR.ID2082)); + + return; + } + + context.Transaction.Request = new OpenIddictRequest(await request.ReadFormAsync(request.HttpContext.RequestAborted)); + } + + else + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6137), request.Method); + + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2084), + uri: SR.FormatID8000(SR.ID2084)); + + return; + } + } + } + + /// + /// Contains the logic responsible of validating the correlation cookie that serves as a CSRF protection. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ValidateCorrelationCookie : IOpenIddictClientHandler + { + private readonly IOptionsMonitor _options; + + public ValidateCorrelationCookie(IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateFrontchannelStateToken.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + } + + // Resolve the request forgery protection from the state token principal. + // If the claim cannot be found, this means the protection was disabled + // using a custom event handler. In this case, bypass the validation. + var claim = context.FrontchannelStateTokenPrincipal.GetClaim(Claims.RequestForgeryProtection); + if (string.IsNullOrEmpty(claim)) + { + return default; + } + + // Compute the name of the cookie name based on the prefix set in the options + // and the random request forgery protection claim restored from the state. + var name = new StringBuilder(_options.CurrentValue.CookieBuilder.Name) + .Append(Separators.Dot) + .Append(claim) + .ToString(); + + // Try to find the cookie matching the request forgery protection stored in the state. + // + // If the cookie cannot be found, this may indicate that the authorization response + // is unsolicited and potentially malicious. This may also be caused by an unadequate + // same-site configuration. In any case, the authentication demand MUST be rejected + // as it's impossible to ensure it's not a session fixation attack without the cookie. + var value = request.Cookies[name]; + if (string.IsNullOrEmpty(value) || !string.Equals(value, "v1", StringComparison.Ordinal)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2129), + uri: SR.FormatID8000(SR.ID2129)); + + return default; + } + + // Note: when deleting a cookie, the same options used when creating it MUST be specified. + var options = _options.CurrentValue.CookieBuilder.Build(request.HttpContext); + + // Return a response header asking the browser to delete the state cookie. + request.HttpContext.Response.Cookies.Delete(name, options); + + return default; + } + } + + /// + /// Contains the logic responsible of resolving the additional challenge parameters stored in the ASP.NET + /// Core authentication properties specified by the application that triggered the challenge operation. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ResolveHostChallengeParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateChallengeDemand.Descriptor.Order - 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var properties = context.Transaction.GetProperty(typeof(AuthenticationProperties).FullName!); + if (properties is null) + { + return default; + } + + // If an issuer was explicitly set, update the challenge context to use it. + if (properties.Items.TryGetValue(Properties.Issuer, out string? issuer) && !string.IsNullOrEmpty(issuer)) + { + // Ensure the issuer set by the application is a valid absolute URI. + if (!Uri.TryCreate(issuer, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString()) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0306)); + } + + context.Issuer = uri; + } + + // If a return URL was specified, use it as the target_link_uri claim. + if (!string.IsNullOrEmpty(properties.RedirectUri)) + { + context.TargetLinkUri = properties.RedirectUri; + } + + foreach (var parameter in properties.Parameters) + { + context.Parameters[parameter.Key] = parameter.Value switch + { + OpenIddictParameter value => value, + JsonElement value => new OpenIddictParameter(value), + bool value => new OpenIddictParameter(value), + int value => new OpenIddictParameter(value), + long value => new OpenIddictParameter(value), + string value => new OpenIddictParameter(value), + string[] value => new OpenIddictParameter(value), + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0115)) + }; + } + + return default; + } + } + + /// + /// Contains the logic responsible of creating a correlation cookie that serves as a CSRF countermeasure. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class GenerateCorrelationCookie : IOpenIddictClientHandler + { + private readonly IOptionsMonitor _options; + + public GenerateCorrelationCookie(IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachChallengeParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: using a correlation cookie serves as an antiforgery protection as the request will + // always be rejected if a cookie corresponding to the request forgery protection claim + // persisted in the state token cannot be found. This protection is considered essential + // in OpenIddict and cannot be disabled via the options. Applications that prefer implementing + // a different protection strategy can set the request forgery protection claim to null or + // remove this handler from the handlers list and add a custom one using a different approach. + + if (string.IsNullOrEmpty(context.RequestForgeryProtection)) + { + return default; + } + + Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + Debug.Assert(!string.IsNullOrEmpty(context.StateToken), SR.GetResourceString(SR.ID4010)); + + // 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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + } + + var options = _options.CurrentValue.CookieBuilder.Build(response.HttpContext); + + // Unless a value was explicitly set in the options, use the expiration date + // of the state token principal as the expiration date of the correlation cookie. + options.Expires ??= context.StateTokenPrincipal.GetExpirationDate(); + + // Compute a collision-resistant and hard-to-guess cookie name based on the prefix set + // in the options and the random request forgery protection claim generated earlier. + var name = new StringBuilder(_options.CurrentValue.CookieBuilder.Name) + .Append(Separators.Dot) + .Append(context.RequestForgeryProtection) + .ToString(); + + // Add the correlation cookie to the response headers. + response.Cookies.Append(name, "v1", options); + + return default; + } + } + + /// + /// 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 : IOpenIddictClientHandler + where TContext : BaseRequestContext + where TFilter : IOpenIddictClientHandlerFilter + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MaxValue - 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.SkipRequest(); + + return default; + } + } + + /// + /// Contains the logic responsible of attaching an appropriate HTTP status code. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class AttachHttpResponseCode : IOpenIddictClientHandler where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is 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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + } + + Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007)); + + response.StatusCode = context.Transaction.Response.Error switch + { + null => 200, // Note: the default code may be replaced by another handler (e.g when doing redirects). + + _ => 400 + }; + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the appropriate HTTP response cache headers. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class AttachCacheControlHeader : IOpenIddictClientHandler where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(AttachHttpResponseCode.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is 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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + } + + // Prevent the response from being cached. + response.Headers[HeaderNames.CacheControl] = "no-store"; + response.Headers[HeaderNames.Pragma] = "no-cache"; + response.Headers[HeaderNames.Expires] = "Thu, 01 Jan 1970 00:00:00 GMT"; + + return default; + } + } + + /// + /// Contains the logic responsible of processing OpenID Connect 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 : IOpenIddictClientHandler + where TContext : BaseRequestContext + where TFilter : IOpenIddictClientHandlerFilter + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is 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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + } + + Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007)); + + if (string.IsNullOrEmpty(context.Transaction.Response.Error)) + { + return default; + } + + context.SkipRequest(); + + return default; + } + } + + /// + /// Contains the logic responsible of processing OpenID Connect 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 : IOpenIddictClientHandler + where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ProcessPassthroughErrorResponse>.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is 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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + } + + Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007)); + + if (string.IsNullOrEmpty(context.Transaction.Response.Error)) + { + return default; + } + + // Determine if the status code pages middleware has been enabled for this request. + // If it was not registered or enabled, let the default OpenIddict client handlers render + // a default error page instead of delegating the rendering to the status code middleware. + var feature = response.HttpContext.Features.Get(); + if (feature is null || !feature.Enabled) + { + return default; + } + + // Mark the request as fully handled to prevent the other OpenIddict client 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 default; + } + } + + /// + /// Contains the logic responsible of processing context 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 : IOpenIddictClientHandler + where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ProcessStatusCodePagesErrorResponse.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(TContext context) + { + if (context is 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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + } + + Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007)); + + if (string.IsNullOrEmpty(context.Transaction.Response.Error)) + { + return; + } + + // Don't return the state originally sent by the client application. + context.Transaction.Response.State = null; + + context.Logger.LogInformation(SR.GetResourceString(SR.ID6143), context.Transaction.Response); + + using var stream = new MemoryStream(); + using var writer = new StreamWriter(stream); + + foreach (var parameter in context.Transaction.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.Write(parameter.Key); + writer.Write(':'); + writer.Write(value); + writer.WriteLine(); + } + + writer.Flush(); + + response.ContentLength = stream.Length; + response.ContentType = "text/plain;charset=UTF-8"; + + stream.Seek(offset: 0, loc: SeekOrigin.Begin); + await stream.CopyToAsync(response.Body, 4096, response.HttpContext.RequestAborted); + + context.HandleRequest(); + } + } + + /// + /// Contains the logic responsible of processing OpenID Connect responses that don't specify any parameter. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessEmptyResponse : IOpenIddictClientHandler + where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MaxValue - 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.Logger.LogInformation(SR.GetResourceString(SR.ID6145)); + context.HandleRequest(); + + return default; + } + } +} diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHelpers.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHelpers.cs new file mode 100644 index 00000000..c2188c59 --- /dev/null +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHelpers.cs @@ -0,0 +1,86 @@ +/* + * 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 OpenIddict.Client; +using OpenIddict.Client.AspNetCore; + +namespace Microsoft.AspNetCore; + +/// +/// Exposes companion extensions for the OpenIddict/ASP.NET Core integration. +/// +public static class OpenIddictClientAspNetCoreHelpers +{ + /// + /// Retrieves the instance stored in the properties. + /// + /// The transaction instance. + /// The instance or null if it couldn't be found. + public static HttpRequest? GetHttpRequest(this OpenIddictClientTransaction transaction) + { + if (transaction is 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 OpenIddictClientEndpointType GetOpenIddictClientEndpointType(this HttpContext context) + { + if (context is 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? GetOpenIddictClientRequest(this HttpContext context) + { + if (context is 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? GetOpenIddictClientResponse(this HttpContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return context.Features.Get()?.Transaction?.Response; + } +} diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreOptions.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreOptions.cs new file mode 100644 index 00000000..f2b305ba --- /dev/null +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreOptions.cs @@ -0,0 +1,54 @@ +/* + * 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; + +namespace OpenIddict.Client.AspNetCore; + +/// +/// Provides various settings needed to configure the OpenIddict ASP.NET Core client integration. +/// +public class OpenIddictClientAspNetCoreOptions : AuthenticationSchemeOptions +{ + /// + /// Gets or sets a boolean indicating whether the pass-through mode is enabled for the redirection 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 EnableRedirectionEndpointPassthrough { 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 . + /// + /// + /// Important: the error pass-through mode cannot be used when the status code pages integration is enabled. + /// + public bool EnableErrorPassthrough { 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 endpoints (e.g authorization or logout) can be handled by ASP.NET Core. + /// + public bool EnableStatusCodePagesIntegration { get; set; } + + /// + /// Gets or sets the cookie builder used to create the cookies that are + /// used to protect against forged requests/session fixation attacks. + /// + public CookieBuilder CookieBuilder { get; set; } = new() + { + HttpOnly = true, + IsEssential = true, + Name = "OpenIddict.Client.RequestForgeryProtection", + SameSite = SameSiteMode.None, + SecurePolicy = CookieSecurePolicy.SameAsRequest + }; +} diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddict.Client.SystemNetHttp.csproj b/src/OpenIddict.Client.SystemNetHttp/OpenIddict.Client.SystemNetHttp.csproj new file mode 100644 index 00000000..ed9ca382 --- /dev/null +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddict.Client.SystemNetHttp.csproj @@ -0,0 +1,33 @@ + + + + net461;netcoreapp3.1;net5.0;net6.0;netstandard2.0;netstandard2.1 + + + + System.Net.Http integration package for the OpenIddict client services. + $(PackageTags);http;httpclient;validation + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs new file mode 100644 index 00000000..84e937b5 --- /dev/null +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs @@ -0,0 +1,78 @@ +/* + * 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.ComponentModel; +using OpenIddict.Client.SystemNetHttp; +using Polly; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Exposes the necessary methods required to configure the OpenIddict client/System.Net.Http integration. +/// +public class OpenIddictClientSystemNetHttpBuilder +{ + /// + /// Initializes a new instance of . + /// + /// The services collection. + public OpenIddictClientSystemNetHttpBuilder(IServiceCollection services) + => Services = services ?? throw new ArgumentNullException(nameof(services)); + + /// + /// Gets the services collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IServiceCollection Services { get; } + + /// + /// Amends the default OpenIddict client/server integration configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public OpenIddictClientSystemNetHttpBuilder Configure(Action configuration) + { + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(configuration); + + return this; + } + + /// + /// Replaces the default HTTP error policy used by the OpenIddict client services. + /// + /// The HTTP Polly error policy. + /// The . + public OpenIddictClientSystemNetHttpBuilder SetHttpErrorPolicy(IAsyncPolicy policy) + => Configure(options => options.HttpErrorPolicy = 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(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.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs new file mode 100644 index 00000000..20976f44 --- /dev/null +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs @@ -0,0 +1,77 @@ +/* + * 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.Net.Http.Headers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; +using Microsoft.Extensions.Options; + +namespace OpenIddict.Client.SystemNetHttp; + +/// +/// Contains the methods required to ensure that the OpenIddict client/System.Net.Http integration configuration is valid. +/// +public class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptions, + IConfigureNamedOptions +{ +#if !SUPPORTS_SERVICE_PROVIDER_IN_HTTP_MESSAGE_HANDLER_BUILDER + private readonly IServiceProvider _serviceProvider; + + public OpenIddictClientSystemNetHttpConfiguration(IServiceProvider serviceProvider) + => _serviceProvider = serviceProvider; +#endif + + public void Configure(OpenIddictClientOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + // Register the built-in event handlers used by the OpenIddict System.Net.Http client components. + options.Handlers.AddRange(OpenIddictClientSystemNetHttpHandlers.DefaultHandlers); + } + + public void Configure(HttpClientFactoryOptions options) + => Debug.Fail("This infrastructure method shouldn't be called."); + + public void Configure(string name, HttpClientFactoryOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + var assembly = typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName(); + + if (!string.Equals(name, assembly.Name, StringComparison.Ordinal)) + { + return; + } + + options.HttpClientActions.Add(client => + { + client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue( + productName: assembly.Name!, + productVersion: assembly.Version!.ToString())); + }); + + options.HttpMessageHandlerBuilderActions.Add(builder => + { +#if SUPPORTS_SERVICE_PROVIDER_IN_HTTP_MESSAGE_HANDLER_BUILDER + var options = builder.Services.GetRequiredService>(); +#else + var options = _serviceProvider.GetRequiredService>(); +#endif + var policy = options.CurrentValue.HttpErrorPolicy; + if (policy is not null) + { + builder.AdditionalHandlers.Add(new PolicyHttpMessageHandler(policy)); + } + }); + } +} diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs new file mode 100644 index 00000000..cded095c --- /dev/null +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpExtensions.cs @@ -0,0 +1,76 @@ +/* + * 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.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http; +using Microsoft.Extensions.Options; +using OpenIddict.Client; +using OpenIddict.Client.SystemNetHttp; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Exposes extensions allowing to register the OpenIddict client/System.Net.Http integration services. +/// +public static class OpenIddictClientSystemNetHttpExtensions +{ + /// + /// Registers the OpenIddict client/System.Net.Http integration 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 OpenIddictClientSystemNetHttpBuilder UseSystemNetHttp(this OpenIddictClientBuilder builder) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Services.AddHttpClient(); + + // Register the built-in validation event handlers used by the OpenIddict System.Net.Http components. + // Note: the order used here is not important, as the actual order is set in the options. + builder.Services.TryAdd(OpenIddictClientSystemNetHttpHandlers.DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); + + // Register the built-in filters used by the default OpenIddict System.Net.Http event handlers. + builder.Services.TryAddSingleton(); + + // Note: TryAddEnumerable() is used here to ensure the initializers are registered only once. + builder.Services.TryAddEnumerable(new[] + { + ServiceDescriptor.Singleton, OpenIddictClientSystemNetHttpConfiguration>(), + ServiceDescriptor.Singleton, OpenIddictClientSystemNetHttpConfiguration>() + }); + + return new OpenIddictClientSystemNetHttpBuilder(builder.Services); + } + + /// + /// Registers the OpenIddict client/System.Net.Http integration services in the DI container. + /// + /// The services builder used by OpenIddict to register new services. + /// The configuration delegate used to configure the client services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictClientBuilder UseSystemNetHttp( + this OpenIddictClientBuilder builder, Action configuration) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + configuration(builder.UseSystemNetHttp()); + + return builder; + } +} diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlerFilters.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlerFilters.cs new file mode 100644 index 00000000..af1afac4 --- /dev/null +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlerFilters.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 System.ComponentModel; + +namespace OpenIddict.Client.SystemNetHttp; + +[EditorBrowsable(EditorBrowsableState.Advanced)] +public static class OpenIddictClientSystemNetHttpHandlerFilters +{ + /// + /// Represents a filter that excludes the associated handlers if the metadata address of the issuer is not available. + /// + public class RequireHttpMetadataAddress : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(BaseExternalContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask( + string.Equals(context.Address?.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || + string.Equals(context.Address?.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Discovery.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Discovery.cs new file mode 100644 index 00000000..c928a5f5 --- /dev/null +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Discovery.cs @@ -0,0 +1,44 @@ +/* + * 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.Client.SystemNetHttp; + +public static partial class OpenIddictClientSystemNetHttpHandlers +{ + public static class Discovery + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Configuration request processing: + */ + PrepareGetHttpRequest.Descriptor, + AttachQueryStringParameters.Descriptor, + SendHttpRequest.Descriptor, + DisposeHttpRequest.Descriptor, + + /* + * Configuration response processing: + */ + ExtractJsonHttpResponse.Descriptor, + DisposeHttpResponse.Descriptor, + + /* + * Cryptography request processing: + */ + PrepareGetHttpRequest.Descriptor, + AttachQueryStringParameters.Descriptor, + SendHttpRequest.Descriptor, + DisposeHttpRequest.Descriptor, + + /* + * Configuration response processing: + */ + ExtractJsonHttpResponse.Descriptor, + DisposeHttpResponse.Descriptor); + } +} diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs new file mode 100644 index 00000000..c0d2e499 --- /dev/null +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs @@ -0,0 +1,115 @@ +/* + * 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.Diagnostics; +using System.Net.Http.Headers; +using System.Text; + +namespace OpenIddict.Client.SystemNetHttp; + +public static partial class OpenIddictClientSystemNetHttpHandlers +{ + public static class Exchange + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Token request processing: + */ + PreparePostHttpRequest.Descriptor, + AttachBasicAuthenticationCredentials.Descriptor, + AttachFormParameters.Descriptor, + SendHttpRequest.Descriptor, + DisposeHttpRequest.Descriptor, + + /* + * Token response processing: + */ + ExtractJsonHttpResponse.Descriptor, + DisposeHttpResponse.Descriptor); + + /// + /// Contains the logic responsible of attaching the client credentials to the HTTP Authorization header. + /// + public class AttachBasicAuthenticationCredentials : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachFormParameters.Descriptor.Order - 1000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(PrepareTokenRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008)); + + // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved, + // this may indicate that the request was incorrectly processed by another client stack. + var request = context.Transaction.GetHttpRequestMessage(); + if (request is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); + } + + // If no client identifier was attached to the request, skip the following logic. + if (string.IsNullOrEmpty(context.Request.ClientId)) + { + return; + } + + var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + // Ensure the issuer resolved from the configuration matches the expected value. + if (configuration.Issuer != context.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + } + + // The OAuth 2.0 specification recommends sending the client credentials using basic authentication. + // However, this authentication method is known to have compatibility issues with the way the + // client credentials are encoded (they MUST be formURL-encoded before being base64-encoded). + // To guarantee that the OpenIddict client handler can be used with servers implementing + // non-standard encoding, the client_secret_post is always preferred when it's explicitly + // listed as a supported client authentication method for the token endpoint. + // If client_secret_post is not listed or if the server returned an empty methods list, + // client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers. + // + // See https://tools.ietf.org/html/rfc8414#section-2 + // and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information. + if (!configuration.TokenEndpointAuthMethodsSupported.Contains(ClientAuthenticationMethods.ClientSecretPost)) + { + // Important: the credentials MUST be formURL-encoded before being base64-encoded. + var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder() + .Append(EscapeDataString(context.Request.ClientId)) + .Append(':') + .Append(EscapeDataString(context.Request.ClientSecret)) + .ToString())); + + // Attach the authorization header containing the client credentials to the HTTP request. + request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Basic, credentials); + + // Remove the client credentials from the request payload to ensure they are not sent twice. + context.Request.ClientId = context.Request.ClientSecret = null; + } + + static string? EscapeDataString(string? value) + => value is not null ? Uri.EscapeDataString(value).Replace("%20", "+") : null; + } + } + } +} diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs new file mode 100644 index 00000000..cb1807f6 --- /dev/null +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs @@ -0,0 +1,401 @@ +/* + * 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.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; +using System.Text; + +namespace OpenIddict.Client.SystemNetHttp; + +[EditorBrowsable(EditorBrowsableState.Never)] +public static partial class OpenIddictClientSystemNetHttpHandlers +{ + public static ImmutableArray DefaultHandlers { get; } + = ImmutableArray.Create() + .AddRange(Discovery.DefaultHandlers) + .AddRange(Exchange.DefaultHandlers); + + /// + /// Contains the logic responsible of preparing an HTTP GET request message. + /// + public class PrepareGetHttpRequest : IOpenIddictClientHandler where TContext : BaseExternalContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The HTTP request message is disposed later by a dedicated handler.")] + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var request = new HttpRequestMessage(HttpMethod.Get, context.Address) + { + Headers = + { + Accept = { new MediaTypeWithQualityHeaderValue("application/json") }, + AcceptCharset = { new StringWithQualityHeaderValue("utf-8") } + } + }; + + // Store the HttpRequestMessage in the transaction properties. + context.Transaction.SetProperty(typeof(HttpRequestMessage).FullName!, request); + + return default; + } + } + + /// + /// Contains the logic responsible of preparing an HTTP POST request message. + /// + public class PreparePostHttpRequest : IOpenIddictClientHandler where TContext : BaseExternalContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(PrepareGetHttpRequest.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The HTTP request message is disposed later by a dedicated handler.")] + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var request = new HttpRequestMessage(HttpMethod.Post, context.Address) + { + Headers = + { + Accept = { new MediaTypeWithQualityHeaderValue("application/json") }, + AcceptCharset = { new StringWithQualityHeaderValue("utf-8") } + } + }; + + // Store the HttpRequestMessage in the transaction properties. + context.Transaction.SetProperty(typeof(HttpRequestMessage).FullName!, request); + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the query string parameters to the HTTP request. + /// + public class AttachQueryStringParameters : IOpenIddictClientHandler where TContext : BaseExternalContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(AttachFormParameters.Descriptor.Order - 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008)); + + // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved, + // this may indicate that the request was incorrectly processed by another client stack. + var request = context.Transaction.GetHttpRequestMessage(); + if (request is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); + } + + if (request.RequestUri is null || context.Transaction.Request.Count == 0) + { + return default; + } + + var builder = new StringBuilder(); + + foreach (var (key, value) in + from parameter in context.Transaction.Request.GetParameters() + let values = (string?[]?) parameter.Value + where values is not null + from value in values + where !string.IsNullOrEmpty(value) + select (parameter.Key, Value: value)) + { + if (builder.Length > 0) + { + builder.Append('&'); + } + + builder.Append(Uri.EscapeDataString(key)); + builder.Append('='); + builder.Append(Uri.EscapeDataString(value)); + } + + // Compute the final request URI using the base address and the query string. + request.RequestUri = new UriBuilder(request.RequestUri) { Query = builder.ToString() }.Uri; + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the form parameters to the HTTP request. + /// + public class AttachFormParameters : IOpenIddictClientHandler where TContext : BaseExternalContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MaxValue - 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008)); + + // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved, + // this may indicate that the request was incorrectly processed by another client stack. + var request = context.Transaction.GetHttpRequestMessage(); + if (request is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); + } + + request.Content = new FormUrlEncodedContent( + from parameter in context.Transaction.Request.GetParameters() + let values = (string[]?) parameter.Value + where values is not null + from value in values + where !string.IsNullOrEmpty(value) + select new KeyValuePair(parameter.Key, value)); + + return default; + } + } + + /// + /// Contains the logic responsible of sending the HTTP request to the remote server. + /// + public class SendHttpRequest : IOpenIddictClientHandler where TContext : BaseExternalContext + { + private readonly IHttpClientFactory _factory; + + public SendHttpRequest(IHttpClientFactory factory) + => _factory = factory; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(DisposeHttpRequest.Descriptor.Order - 50_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved, + // this may indicate that the request was incorrectly processed by another client stack. + var request = context.Transaction.GetHttpRequestMessage(); + if (request is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); + } + + var assembly = typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName(); + using var client = _factory.CreateClient(assembly.Name!); + if (client is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0174)); + } + +#if SUPPORTS_HTTP_CLIENT_DEFAULT_REQUEST_VERSION + // If supported, import the HTTP version from the client instance. + request.Version = client.DefaultRequestVersion; +#endif + var response = await client.SendAsync(request, HttpCompletionOption.ResponseContentRead); + if (response is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0175)); + } + + // Store the HttpResponseMessage in the transaction properties. + context.Transaction.SetProperty(typeof(HttpResponseMessage).FullName!, response); + } + } + + /// + /// Contains the logic responsible of disposing of the HTTP request message. + /// + public class DisposeHttpRequest : IOpenIddictClientHandler where TContext : BaseExternalContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MaxValue - 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved, + // this may indicate that the request was incorrectly processed by another client stack. + var request = context.Transaction.GetHttpRequestMessage(); + if (request is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); + } + + request.Dispose(); + + // Remove the request from the transaction properties. + context.Transaction.SetProperty(typeof(HttpRequestMessage).FullName!, null); + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the response from the JSON-encoded HTTP body. + /// + public class ExtractJsonHttpResponse : IOpenIddictClientHandler where TContext : BaseExternalContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(DisposeHttpResponse.Descriptor.Order - 50_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to System.Net.Http requests. If the HTTP response cannot be resolved, + // this may indicate that the request was incorrectly processed by another client stack. + var response = context.Transaction.GetHttpResponseMessage(); + if (response is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); + } + + // The status code is deliberately not validated to ensure even errored responses + // (typically in the 4xx range) can be deserialized and handled by the event handlers. + + // Note: ReadFromJsonAsync() automatically validates the content type and the content encoding + // and transcode the response stream if a non-UTF-8 response is returned by the remote server. + context.Transaction.Response = await response.Content.ReadFromJsonAsync(); + } + } + + /// + /// Contains the logic responsible of disposing of the HTTP response message. + /// + public class DisposeHttpResponse : IOpenIddictClientHandler where TContext : BaseExternalContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MaxValue - 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to System.Net.Http requests. If the HTTP response cannot be resolved, + // this may indicate that the request was incorrectly processed by another client stack. + var response = context.Transaction.GetHttpResponseMessage(); + if (response is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); + } + + response.Dispose(); + + // Remove the response from the transaction properties. + context.Transaction.SetProperty(typeof(HttpResponseMessage).FullName!, null); + + return default; + } + } +} diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHelpers.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHelpers.cs new file mode 100644 index 00000000..9b290cc7 --- /dev/null +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHelpers.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 OpenIddict.Client; + +namespace System.Net.Http; + +/// +/// Exposes companion extensions for the OpenIddict/System.Net.Http integration. +/// +public static class OpenIddictClientSystemNetHttpHelpers +{ + /// + /// Gets the associated with the current context. + /// + /// The transaction instance. + /// The instance or null if it couldn't be found. + public static HttpRequestMessage? GetHttpRequestMessage(this OpenIddictClientTransaction transaction) + => transaction.GetProperty(typeof(HttpRequestMessage).FullName!); + + /// + /// Gets the associated with the current context. + /// + /// The transaction instance. + /// The instance or null if it couldn't be found. + public static HttpResponseMessage? GetHttpResponseMessage(this OpenIddictClientTransaction transaction) + => transaction.GetProperty(typeof(HttpResponseMessage).FullName!); +} diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs new file mode 100644 index 00000000..c31806b3 --- /dev/null +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs @@ -0,0 +1,25 @@ +/* + * 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.Net; +using Polly; +using Polly.Extensions.Http; + +namespace OpenIddict.Client.SystemNetHttp; + +/// +/// Provides various settings needed to configure the OpenIddict client/System.Net.Http integration. +/// +public class OpenIddictClientSystemNetHttpOptions +{ + /// + /// Gets or sets the HTTP Polly error policy used by the internal OpenIddict HTTP clients. + /// + public IAsyncPolicy HttpErrorPolicy { get; set; } + = HttpPolicyExtensions.HandleTransientHttpError() + .OrResult(response => response.StatusCode == HttpStatusCode.NotFound) + .WaitAndRetryAsync(4, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))); +} diff --git a/src/OpenIddict.Client/IOpenIddictClientDispatcher.cs b/src/OpenIddict.Client/IOpenIddictClientDispatcher.cs new file mode 100644 index 00000000..eb401649 --- /dev/null +++ b/src/OpenIddict.Client/IOpenIddictClientDispatcher.cs @@ -0,0 +1,12 @@ +/* + * 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.Client; + +public interface IOpenIddictClientDispatcher +{ + ValueTask DispatchAsync(TContext context) where TContext : BaseContext; +} diff --git a/src/OpenIddict.Client/IOpenIddictClientFactory.cs b/src/OpenIddict.Client/IOpenIddictClientFactory.cs new file mode 100644 index 00000000..f27906a6 --- /dev/null +++ b/src/OpenIddict.Client/IOpenIddictClientFactory.cs @@ -0,0 +1,12 @@ +/* + * 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.Client; + +public interface IOpenIddictClientFactory +{ + ValueTask CreateTransactionAsync(); +} diff --git a/src/OpenIddict.Client/IOpenIddictClientHandler.cs b/src/OpenIddict.Client/IOpenIddictClientHandler.cs new file mode 100644 index 00000000..c4948ff9 --- /dev/null +++ b/src/OpenIddict.Client/IOpenIddictClientHandler.cs @@ -0,0 +1,23 @@ +/* + * 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.Client; + +/// +/// Represents a handler able to process events. +/// +/// The type of the context associated with events handled by this instance. +public interface IOpenIddictClientHandler where TContext : BaseContext +{ + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + ValueTask HandleAsync(TContext context); +} diff --git a/src/OpenIddict.Client/IOpenIddictClientHandlerFilter.cs b/src/OpenIddict.Client/IOpenIddictClientHandlerFilter.cs new file mode 100644 index 00000000..2277a641 --- /dev/null +++ b/src/OpenIddict.Client/IOpenIddictClientHandlerFilter.cs @@ -0,0 +1,12 @@ +/* + * 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.Client; + +public interface IOpenIddictClientHandlerFilter where TContext : BaseContext +{ + ValueTask IsActiveAsync(TContext context); +} diff --git a/src/OpenIddict.Client/OpenIddict.Client.csproj b/src/OpenIddict.Client/OpenIddict.Client.csproj new file mode 100644 index 00000000..0b67cd48 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddict.Client.csproj @@ -0,0 +1,41 @@ + + + + net461;net472;net48;netcoreapp3.1;net5.0;net6.0;netstandard2.0;netstandard2.1 + + + + OpenIddict authorization client services. + +Note: this package only contains the generic/host-agnostic client components. +To use the client feature on ASP.NET Core or OWIN/Katana, reference the OpenIddict.Client.AspNetCore or OpenIddict.Client.Owin package. + + $(PackageTags);client + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OpenIddict.Client/OpenIddictClientBuilder.cs b/src/OpenIddict.Client/OpenIddictClientBuilder.cs new file mode 100644 index 00000000..5538c819 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientBuilder.cs @@ -0,0 +1,1067 @@ +/* + * 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.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Client; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Exposes the necessary methods required to configure the OpenIddict client services. +/// +public class OpenIddictClientBuilder +{ + /// + /// Initializes a new instance of . + /// + /// The services collection. + public OpenIddictClientBuilder(IServiceCollection services) + => Services = services ?? throw new ArgumentNullException(nameof(services)); + + /// + /// Gets the services collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IServiceCollection Services { get; } + + /// + /// Registers an event handler using the specified configuration delegate. + /// + /// The event context type. + /// The configuration delegate. + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictClientBuilder AddEventHandler( + Action> configuration) + where TContext : OpenIddictClientEvents.BaseContext + { + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + // Note: handlers registered using this API are assumed to be custom handlers by default. + var builder = OpenIddictClientHandlerDescriptor.CreateBuilder() + .SetType(OpenIddictClientHandlerType.Custom); + + configuration(builder); + + return AddEventHandler(builder.Build()); + } + + /// + /// Registers an event handler using the specified descriptor. + /// + /// The handler descriptor. + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictClientBuilder AddEventHandler(OpenIddictClientHandlerDescriptor descriptor) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + // Register the handler in the services collection. + Services.Add(descriptor.ServiceDescriptor); + + return Configure(options => options.Handlers.Add(descriptor)); + } + + /// + /// Removes the event handler that matches the specified descriptor. + /// + /// The descriptor corresponding to the handler to remove. + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictClientBuilder RemoveEventHandler(OpenIddictClientHandlerDescriptor descriptor) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + Services.RemoveAll(descriptor.ServiceDescriptor.ServiceType); + + Services.PostConfigure(options => + { + for (var index = options.Handlers.Count - 1; index >= 0; index--) + { + if (options.Handlers[index].ServiceDescriptor.ServiceType == descriptor.ServiceDescriptor.ServiceType) + { + options.Handlers.RemoveAt(index); + } + } + }); + + return this; + } + + /// + /// Amends the default OpenIddict client configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public OpenIddictClientBuilder Configure(Action configuration) + { + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(configuration); + + return this; + } + + /// + /// Registers encryption credentials. + /// + /// The encrypting credentials. + /// The . + public OpenIddictClientBuilder AddEncryptionCredentials(EncryptingCredentials credentials) + { + if (credentials is null) + { + throw new ArgumentNullException(nameof(credentials)); + } + + return Configure(options => options.EncryptionCredentials.Add(credentials)); + } + + /// + /// Registers an encryption key. + /// + /// The security key. + /// The . + public OpenIddictClientBuilder AddEncryptionKey(SecurityKey key) + { + if (key is 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(SR.GetResourceString(SR.ID0055)); + } + + if (key.IsSupportedAlgorithm(SecurityAlgorithms.Aes256KW)) + { + if (key.KeySize != 256) + { + throw new InvalidOperationException(SR.FormatID0283(256, key.KeySize)); + } + + return AddEncryptionCredentials(new EncryptingCredentials(key, + SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512)); + } + + if (key.IsSupportedAlgorithm(SecurityAlgorithms.RsaOAEP)) + { + return AddEncryptionCredentials(new EncryptingCredentials(key, + SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512)); + } + + throw new InvalidOperationException(SR.GetResourceString(SR.ID0056)); + } + + /// + /// Registers (and generates if necessary) a user-specific development encryption certificate. + /// + /// The . + public OpenIddictClientBuilder AddDevelopmentEncryptionCertificate() + => AddDevelopmentEncryptionCertificate(new X500DistinguishedName("CN=OpenIddict Client Encryption Certificate")); + + /// + /// Registers (and generates if necessary) a user-specific development encryption certificate. + /// + /// The subject name associated with the certificate. + /// The . + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The X.509 certificate is attached to the client options.")] + public OpenIddictClientBuilder AddDevelopmentEncryptionCertificate(X500DistinguishedName subject) + { + if (subject is null) + { + throw new ArgumentNullException(nameof(subject)); + } + + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadWrite); + + // Try to retrieve the existing development certificates from the specified store. + // If no valid existing certificate was found, create a new encryption certificate. + var certificates = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) + .OfType() + .ToList(); + + if (!certificates.Any(certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now)) + { +#if SUPPORTS_CERTIFICATE_GENERATION + 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)); + + var 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 Client 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; + } + + certificates.Insert(0, certificate = new X509Certificate2(data, string.Empty, flags)); + } + + finally + { + Array.Clear(data, 0, data.Length); + } + + store.Add(certificate); +#else + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); +#endif + } + + return Configure(options => options.EncryptionCredentials.AddRange( + from certificate in certificates + let key = new X509SecurityKey(certificate) + select new EncryptingCredentials(key, SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512))); + } + + /// + /// Registers a new ephemeral encryption key. Ephemeral encryption keys are automatically + /// discarded when the application shuts down and payloads 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 OpenIddictClientBuilder AddEphemeralEncryptionKey() + => AddEphemeralEncryptionKey(SecurityAlgorithms.RsaOAEP); + + /// + /// Registers a new ephemeral encryption key. Ephemeral encryption keys are automatically + /// discarded when the application shuts down and payloads 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 encryption key. + /// The . + public OpenIddictClientBuilder AddEphemeralEncryptionKey(string algorithm) + { + if (string.IsNullOrEmpty(algorithm)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0057), nameof(algorithm)); + } + + return algorithm switch + { + SecurityAlgorithms.Aes256KW + => AddEncryptionCredentials(new EncryptingCredentials(CreateSymmetricSecurityKey(256), + algorithm, SecurityAlgorithms.Aes256CbcHmacSha512)), + + SecurityAlgorithms.RsaOAEP or + SecurityAlgorithms.RsaOaepKeyWrap + => AddEncryptionCredentials(new EncryptingCredentials(CreateRsaSecurityKey(2048), + algorithm, SecurityAlgorithms.Aes256CbcHmacSha512)), + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0058)), + }; + + static SymmetricSecurityKey CreateSymmetricSecurityKey(int size) + { + 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); + } + + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The generated RSA key is attached to the client options.")] + 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(SR.GetResourceString(SR.ID0059)); + } + + return new RsaSecurityKey(algorithm); +#endif + } + } + + /// + /// Registers an encryption certificate. + /// + /// The encryption certificate. + /// The . + public OpenIddictClientBuilder AddEncryptionCertificate(X509Certificate2 certificate) + { + if (certificate is null) + { + throw new ArgumentNullException(nameof(certificate)); + } + + // If the certificate is a X.509v3 certificate that specifies at least one + // key usage, ensure that the certificate key can be used for key encryption. + if (certificate.Version >= 3) + { + var extensions = certificate.Extensions.OfType().ToList(); + if (extensions.Count != 0 && !extensions.Any(extension => extension.KeyUsages.HasFlag(X509KeyUsageFlags.KeyEncipherment))) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0060)); + } + } + + if (!certificate.HasPrivateKey) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0061)); + } + + return AddEncryptionKey(new X509SecurityKey(certificate)); + } + + /// + /// Registers an encryption certificate retrieved from an embedded resource. + /// + /// The assembly containing the certificate. + /// The name of the embedded resource. + /// The password used to open the certificate. + /// The . + public OpenIddictClientBuilder AddEncryptionCertificate(Assembly assembly, string resource, string? password) +#if SUPPORTS_EPHEMERAL_KEY_SETS + // Note: ephemeral key sets are currently not supported on macOS. + => AddEncryptionCertificate(assembly, resource, password, RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + X509KeyStorageFlags.MachineKeySet : + X509KeyStorageFlags.EphemeralKeySet); +#else + => AddEncryptionCertificate(assembly, resource, password, X509KeyStorageFlags.MachineKeySet); +#endif + + /// + /// Registers an encryption certificate retrieved from an embedded resource. + /// + /// 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 OpenIddictClientBuilder AddEncryptionCertificate( + Assembly assembly, string resource, + string? password, X509KeyStorageFlags flags) + { + if (assembly is null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + if (string.IsNullOrEmpty(resource)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0062), nameof(resource)); + } + + using var stream = assembly.GetManifestResourceStream(resource); + if (stream is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0064)); + } + + return AddEncryptionCertificate(stream, password, flags); + } + + /// + /// Registers an encryption certificate extracted from a stream. + /// + /// The stream containing the certificate. + /// The password used to open the certificate. + /// The . + public OpenIddictClientBuilder AddEncryptionCertificate(Stream stream, string? password) +#if SUPPORTS_EPHEMERAL_KEY_SETS + // Note: ephemeral key sets are currently not supported on macOS. + => AddEncryptionCertificate(stream, password, RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + X509KeyStorageFlags.MachineKeySet : + X509KeyStorageFlags.EphemeralKeySet); +#else + => AddEncryptionCertificate(stream, password, X509KeyStorageFlags.MachineKeySet); +#endif + + /// + /// Registers an encryption certificate extracted from a stream. + /// + /// 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 . + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The X.509 certificate is attached to the client options.")] + public OpenIddictClientBuilder AddEncryptionCertificate(Stream stream, string? password, X509KeyStorageFlags flags) + { + if (stream is null) + { + throw new ArgumentNullException(nameof(stream)); + } + + using var buffer = new MemoryStream(); + stream.CopyTo(buffer); + + return AddEncryptionCertificate(new X509Certificate2(buffer.ToArray(), password, flags)); + } + + /// + /// Registers an encryption certificate retrieved from the X.509 user or machine store. + /// + /// The thumbprint of the certificate used to identify it in the X.509 store. + /// The . + public OpenIddictClientBuilder AddEncryptionCertificate(string thumbprint) + { + if (string.IsNullOrEmpty(thumbprint)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0065), nameof(thumbprint)); + } + + var certificate = GetCertificate(StoreLocation.CurrentUser, thumbprint) ?? GetCertificate(StoreLocation.LocalMachine, thumbprint); + if (certificate is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)); + } + + 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 an encryption certificate retrieved from the specified X.509 store. + /// + /// 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 OpenIddictClientBuilder AddEncryptionCertificate(string thumbprint, StoreName name, StoreLocation location) + { + if (string.IsNullOrEmpty(thumbprint)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0065), 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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)); + } + + return AddEncryptionCertificate(certificate); + } + + /// + /// Registers signing credentials. + /// + /// The signing credentials. + /// The . + public OpenIddictClientBuilder AddSigningCredentials(SigningCredentials credentials) + { + if (credentials is null) + { + throw new ArgumentNullException(nameof(credentials)); + } + + return Configure(options => options.SigningCredentials.Add(credentials)); + } + + /// + /// Registers a signing key. + /// + /// The security key. + /// The . + public OpenIddictClientBuilder AddSigningKey(SecurityKey key) + { + if (key is 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(SR.GetResourceString(SR.ID0067)); + } + + if (key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256)) + { + return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.RsaSha256)); + } + + if (key.IsSupportedAlgorithm(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 (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256)) + { + return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256)); + } + + if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384)) + { + return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha384)); + } + + if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512)) + { + return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha512)); + } +#else + if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256) || + key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384) || + key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512)) + { + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0069)); + } +#endif + + throw new InvalidOperationException(SR.GetResourceString(SR.ID0068)); + } + + /// + /// Registers (and generates if necessary) a user-specific development signing certificate. + /// + /// The . + public OpenIddictClientBuilder AddDevelopmentSigningCertificate() + => AddDevelopmentSigningCertificate(new X500DistinguishedName("CN=OpenIddict Client Signing Certificate")); + + /// + /// Registers (and generates if necessary) a user-specific development signing certificate. + /// + /// The subject name associated with the certificate. + /// The . + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The X.509 certificate is attached to the client options.")] + public OpenIddictClientBuilder AddDevelopmentSigningCertificate(X500DistinguishedName subject) + { + if (subject is null) + { + throw new ArgumentNullException(nameof(subject)); + } + + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadWrite); + + // Try to retrieve the existing development certificates from the specified store. + // If no valid existing certificate was found, create a new signing certificate. + var certificates = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, subject.Name, validOnly: false) + .OfType() + .ToList(); + + if (!certificates.Any(certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now)) + { +#if SUPPORTS_CERTIFICATE_GENERATION + 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)); + + var 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 Client 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; + } + + certificates.Insert(0, certificate = new X509Certificate2(data, string.Empty, flags)); + } + + finally + { + Array.Clear(data, 0, data.Length); + } + + store.Add(certificate); +#else + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0264)); +#endif + } + + return Configure(options => options.SigningCredentials.AddRange( + from certificate in certificates + let key = new X509SecurityKey(certificate) + select new SigningCredentials(key, SecurityAlgorithms.RsaSha256))); + } + + /// + /// Registers a new ephemeral signing key. Ephemeral signing keys are automatically + /// discarded when the application shuts down and payloads 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 OpenIddictClientBuilder AddEphemeralSigningKey() + => AddEphemeralSigningKey(SecurityAlgorithms.RsaSha256); + + /// + /// Registers a new ephemeral signing key. Ephemeral signing keys are automatically + /// discarded when the application shuts down and payloads 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 . + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The X.509 certificate is attached to the client options.")] + public OpenIddictClientBuilder AddEphemeralSigningKey(string algorithm) + { + if (string.IsNullOrEmpty(algorithm)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0057), nameof(algorithm)); + } + + return algorithm switch + { + SecurityAlgorithms.RsaSha256 or + SecurityAlgorithms.RsaSha384 or + SecurityAlgorithms.RsaSha512 or + SecurityAlgorithms.RsaSha256Signature or + SecurityAlgorithms.RsaSha384Signature or + SecurityAlgorithms.RsaSha512Signature or + SecurityAlgorithms.RsaSsaPssSha256 or + SecurityAlgorithms.RsaSsaPssSha384 or + SecurityAlgorithms.RsaSsaPssSha512 or + SecurityAlgorithms.RsaSsaPssSha256Signature or + SecurityAlgorithms.RsaSsaPssSha384Signature or + SecurityAlgorithms.RsaSsaPssSha512Signature + => AddSigningCredentials(new SigningCredentials(CreateRsaSecurityKey(2048), algorithm)), + +#if SUPPORTS_ECDSA + SecurityAlgorithms.EcdsaSha256 or + SecurityAlgorithms.EcdsaSha256Signature + => AddSigningCredentials(new SigningCredentials(new ECDsaSecurityKey( + ECDsa.Create(ECCurve.NamedCurves.nistP256)), algorithm)), + + SecurityAlgorithms.EcdsaSha384 or + SecurityAlgorithms.EcdsaSha384Signature + => AddSigningCredentials(new SigningCredentials(new ECDsaSecurityKey( + ECDsa.Create(ECCurve.NamedCurves.nistP384)), algorithm)), + + SecurityAlgorithms.EcdsaSha512 or + SecurityAlgorithms.EcdsaSha512Signature + => AddSigningCredentials(new SigningCredentials(new ECDsaSecurityKey( + ECDsa.Create(ECCurve.NamedCurves.nistP521)), algorithm)), +#else + SecurityAlgorithms.EcdsaSha256 or + SecurityAlgorithms.EcdsaSha384 or + SecurityAlgorithms.EcdsaSha512 or + SecurityAlgorithms.EcdsaSha256Signature or + SecurityAlgorithms.EcdsaSha384Signature or + SecurityAlgorithms.EcdsaSha512Signature + => throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0069)), +#endif + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0058)), + }; + + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The generated RSA key is attached to the client options.")] + 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(SR.GetResourceString(SR.ID0059)); + } + + return new RsaSecurityKey(algorithm); +#endif + } + } + + /// + /// Registers a signing certificate. + /// + /// The signing certificate. + /// The . + public OpenIddictClientBuilder AddSigningCertificate(X509Certificate2 certificate) + { + if (certificate is null) + { + throw new ArgumentNullException(nameof(certificate)); + } + + // If the certificate is a X.509v3 certificate that specifies at least + // one key usage, ensure that the certificate key can be used for signing. + if (certificate.Version >= 3) + { + var extensions = certificate.Extensions.OfType().ToList(); + if (extensions.Count != 0 && !extensions.Any(extension => extension.KeyUsages.HasFlag(X509KeyUsageFlags.DigitalSignature))) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0070)); + } + } + + if (!certificate.HasPrivateKey) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0061)); + } + + return AddSigningKey(new X509SecurityKey(certificate)); + } + + /// + /// Registers a signing certificate retrieved from an embedded resource. + /// + /// The assembly containing the certificate. + /// The name of the embedded resource. + /// The password used to open the certificate. + /// The . + public OpenIddictClientBuilder AddSigningCertificate(Assembly assembly, string resource, string? password) +#if SUPPORTS_EPHEMERAL_KEY_SETS + // Note: ephemeral key sets are currently not supported on macOS. + => AddSigningCertificate(assembly, resource, password, RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + X509KeyStorageFlags.MachineKeySet : + X509KeyStorageFlags.EphemeralKeySet); +#else + => AddSigningCertificate(assembly, resource, password, X509KeyStorageFlags.MachineKeySet); +#endif + + /// + /// Registers a signing certificate retrieved from an embedded resource. + /// + /// 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 OpenIddictClientBuilder AddSigningCertificate( + Assembly assembly, string resource, + string? password, X509KeyStorageFlags flags) + { + if (assembly is null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + if (string.IsNullOrEmpty(resource)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0062), nameof(resource)); + } + + using var stream = assembly.GetManifestResourceStream(resource); + if (stream is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0064)); + } + + return AddSigningCertificate(stream, password, flags); + } + + /// + /// Registers a signing certificate extracted from a stream. + /// + /// The stream containing the certificate. + /// The password used to open the certificate. + /// The . + public OpenIddictClientBuilder AddSigningCertificate(Stream stream, string? password) +#if SUPPORTS_EPHEMERAL_KEY_SETS + // Note: ephemeral key sets are currently not supported on macOS. + => AddSigningCertificate(stream, password, RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + X509KeyStorageFlags.MachineKeySet : + X509KeyStorageFlags.EphemeralKeySet); +#else + => AddSigningCertificate(stream, password, X509KeyStorageFlags.MachineKeySet); +#endif + + /// + /// Registers a signing certificate extracted from a stream. + /// + /// 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 . + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The X.509 certificate is attached to the client options.")] + public OpenIddictClientBuilder AddSigningCertificate(Stream stream, string? password, X509KeyStorageFlags flags) + { + if (stream is null) + { + throw new ArgumentNullException(nameof(stream)); + } + + using var buffer = new MemoryStream(); + stream.CopyTo(buffer); + + return AddSigningCertificate(new X509Certificate2(buffer.ToArray(), password, flags)); + } + + /// + /// Registers a signing certificate retrieved from the X.509 user or machine store. + /// + /// The thumbprint of the certificate used to identify it in the X.509 store. + /// The . + public OpenIddictClientBuilder AddSigningCertificate(string thumbprint) + { + if (string.IsNullOrEmpty(thumbprint)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0065), nameof(thumbprint)); + } + + var certificate = GetCertificate(StoreLocation.CurrentUser, thumbprint) ?? GetCertificate(StoreLocation.LocalMachine, thumbprint); + if (certificate is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)); + } + + 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 signing certificate retrieved from the specified X.509 store. + /// + /// 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 OpenIddictClientBuilder AddSigningCertificate(string thumbprint, StoreName name, StoreLocation location) + { + if (string.IsNullOrEmpty(thumbprint)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0065), 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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)); + } + + return AddSigningCertificate(certificate); + } + + /// + /// Adds a new client registration. + /// + /// The client registration. + /// The . + public OpenIddictClientBuilder AddRegistration(OpenIddictClientRegistration registration) + { + if (registration is null) + { + throw new ArgumentNullException(nameof(registration)); + } + + return Configure(options => options.Registrations.Add(registration)); + } + + /// + /// Sets the relative or absolute URLs associated to the redirection 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 OpenIddictClientBuilder SetRedirectionEndpointUris(params string[] addresses) + { + if (addresses is null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + return SetRedirectionEndpointUris(addresses.Select(address => new Uri(address, UriKind.RelativeOrAbsolute)).ToArray()); + } + + /// + /// Sets the relative or absolute URLs associated to the redirection 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 OpenIddictClientBuilder SetRedirectionEndpointUris(params Uri[] addresses) + { + if (addresses is null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + if (addresses.Any(address => !address.IsWellFormedOriginalString())) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(addresses)); + } + + if (addresses.Any(address => address.OriginalString.StartsWith("~", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(SR.FormatID0081("~"), nameof(addresses)); + } + + return Configure(options => + { + options.RedirectionEndpointUris.Clear(); + options.RedirectionEndpointUris.AddRange(addresses); + }); + } + + /// + /// Sets the state token lifetime, after which authorization callbacks + /// using an expired state token will be automatically rejected by OpenIddict. + /// Using long-lived state tokens or tokens that never expire is not recommended. + /// While discouraged, null can be specified to issue tokens that never expire. + /// + /// The access token lifetime. + /// The . + public OpenIddictClientBuilder SetStateTokenLifetime(TimeSpan? lifetime) + => Configure(options => options.StateTokenLifetime = lifetime); + + /// + /// 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(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.Client/OpenIddictClientConfiguration.cs b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs new file mode 100644 index 00000000..bfc0c45b --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs @@ -0,0 +1,197 @@ +/* + * 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 Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Tokens; + +namespace OpenIddict.Client; + +/// +/// Contains the methods required to ensure that the OpenIddict client configuration is valid. +/// +public class OpenIddictClientConfiguration : IPostConfigureOptions +{ + private readonly OpenIddictClientService _service; + + public OpenIddictClientConfiguration(OpenIddictClientService service) + => _service = service; + + /// + /// Populates the default OpenIddict client 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(string name, OpenIddictClientOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (options.JsonWebTokenHandler is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0075)); + } + + foreach (var registration in options.Registrations) + { + if (registration.ConfigurationManager is null) + { + if (registration.Configuration is not null) + { + registration.Configuration.Issuer = registration.Issuer; + registration.ConfigurationManager = new StaticConfigurationManager(registration.Configuration); + } + + else + { + if (!options.Handlers.Any(descriptor => descriptor.ContextType == typeof(ApplyConfigurationRequestContext)) || + !options.Handlers.Any(descriptor => descriptor.ContextType == typeof(ApplyCryptographyRequestContext))) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0135)); + } + + if (registration.MetadataAddress is null) + { + registration.MetadataAddress = new Uri(".well-known/openid-configuration", UriKind.Relative); + } + + if (!registration.MetadataAddress.IsAbsoluteUri) + { + var issuer = registration.Issuer; + if (issuer is null || !issuer.IsAbsoluteUri) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0136)); + } + + if (!string.IsNullOrEmpty(issuer.Fragment) || !string.IsNullOrEmpty(issuer.Query)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0137)); + } + + if (!issuer.OriginalString.EndsWith("/", StringComparison.Ordinal)) + { + issuer = new Uri(issuer.OriginalString + "/", UriKind.Absolute); + } + + if (registration.MetadataAddress.OriginalString.StartsWith("/", StringComparison.Ordinal)) + { + registration.MetadataAddress = new Uri(registration.MetadataAddress.OriginalString.Substring( + 1, registration.MetadataAddress.OriginalString.Length - 1), UriKind.Relative); + } + + registration.MetadataAddress = new Uri(issuer, registration.MetadataAddress); + } + + registration.ConfigurationManager = new ConfigurationManager( + registration.MetadataAddress.AbsoluteUri, new OpenIddictClientRetriever(_service)) + { + AutomaticRefreshInterval = ConfigurationManager.DefaultAutomaticRefreshInterval, + RefreshInterval = ConfigurationManager.DefaultRefreshInterval + }; + } + } + } + + // Sort the handlers collection using the order associated with each handler. + options.Handlers.Sort((left, right) => left.Order.CompareTo(right.Order)); + + // Sort the encryption and signing credentials. + options.EncryptionCredentials.Sort((left, right) => Compare(left.Key, right.Key)); + options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key)); + + // Generate a key identifier for the encryption/signing keys that don't already have one. + foreach (var key in options.EncryptionCredentials.Select(credentials => credentials.Key) + .Concat(options.SigningCredentials.Select(credentials => credentials.Key)) + .Where(key => string.IsNullOrEmpty(key.KeyId))) + { + key.KeyId = GetKeyIdentifier(key); + } + + // Attach the signing credentials to the token validation parameters. + options.TokenValidationParameters.IssuerSigningKeys = + from credentials in options.SigningCredentials + select credentials.Key; + + // Attach the encryption credentials to the token validation parameters. + options.TokenValidationParameters.TokenDecryptionKeys = + from credentials in options.EncryptionCredentials + select credentials.Key; + + static int Compare(SecurityKey left, SecurityKey right) => (left, right) switch + { + // If the two keys refer to the same instances, return 0. + (SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0, + + // If one of the keys is a symmetric key, prefer it to the other one. + (SymmetricSecurityKey, SymmetricSecurityKey) => 0, + (SymmetricSecurityKey, SecurityKey) => -1, + (SecurityKey, SymmetricSecurityKey) => 1, + + // If one of the keys is backed by a X.509 certificate, don't prefer it if it's not valid yet. + (X509SecurityKey first, SecurityKey) when first.Certificate.NotBefore > DateTime.Now => 1, + (SecurityKey, X509SecurityKey second) when second.Certificate.NotBefore > DateTime.Now => 1, + + // If the two keys are backed by a X.509 certificate, prefer the one with the furthest expiration date. + (X509SecurityKey first, X509SecurityKey second) => -first.Certificate.NotAfter.CompareTo(second.Certificate.NotAfter), + + // If one of the keys is backed by a X.509 certificate, prefer the X.509 security key. + (X509SecurityKey, SecurityKey) => -1, + (SecurityKey, X509SecurityKey) => 1, + + // If the two keys are not backed by a X.509 certificate, none should be preferred to the other. + (SecurityKey, SecurityKey) => 0 + }; + + 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 is null) + { + parameters = rsaSecurityKey.Rsa.ExportParameters(includePrivateParameters: false); + + Debug.Assert(parameters.Modulus is not null, SR.GetResourceString(SR.ID4003)); + } + + // 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 is not null, SR.GetResourceString(SR.ID4004)); + + // 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.Client/OpenIddictClientDispatcher.cs b/src/OpenIddict.Client/OpenIddictClientDispatcher.cs new file mode 100644 index 00000000..961df906 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientDispatcher.cs @@ -0,0 +1,132 @@ +/* + * 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.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace OpenIddict.Client; + +public class OpenIddictClientDispatcher : IOpenIddictClientDispatcher +{ + private readonly ILogger _logger; + private readonly IOptionsMonitor _options; + private readonly IServiceProvider _provider; + + /// + /// Creates a new instance of the class. + /// + public OpenIddictClientDispatcher( + ILogger logger, + IOptionsMonitor options, + IServiceProvider provider) + { + _logger = logger; + _options = options; + _provider = provider; + } + + public async ValueTask DispatchAsync(TContext context) where TContext : BaseContext + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + await foreach (var handler in GetHandlersAsync()) + { + try + { + await handler.HandleAsync(context); + } + + catch (Exception exception) when (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug(exception, SR.GetResourceString(SR.ID6132), handler.GetType().FullName, typeof(TContext).FullName); + + throw; + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug(SR.GetResourceString(SR.ID6133), typeof(TContext).FullName, handler.GetType().FullName); + } + + switch (context) + { + case BaseRequestContext { IsRequestHandled: true }: + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug(SR.GetResourceString(SR.ID6134), typeof(TContext).FullName, handler.GetType().FullName); + } + return; + + case BaseRequestContext { IsRequestSkipped: true }: + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug(SR.GetResourceString(SR.ID6135), typeof(TContext).FullName, handler.GetType().FullName); + } + return; + + case BaseValidatingContext { IsRejected: true }: + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug(SR.GetResourceString(SR.ID6136), typeof(TContext).FullName, handler.GetType().FullName); + } + return; + + default: continue; + } + } + + async IAsyncEnumerable> GetHandlersAsync() + { + // Note: the descriptors collection is sorted during options initialization for performance reasons. + var descriptors = _options.CurrentValue.Handlers; + if (descriptors.Count == 0) + { + yield break; + } + + for (var index = 0; index < descriptors.Count; index++) + { + var descriptor = descriptors[index]; + if (descriptor.ContextType != typeof(TContext) || !await IsActiveAsync(descriptor)) + { + continue; + } + + var handler = descriptor.ServiceDescriptor.ImplementationInstance is not null ? + descriptor.ServiceDescriptor.ImplementationInstance as IOpenIddictClientHandler : + _provider.GetService(descriptor.ServiceDescriptor.ServiceType) as IOpenIddictClientHandler; + + if (handler is null) + { + throw new InvalidOperationException(SR.FormatID0138(descriptor.ServiceDescriptor.ServiceType)); + } + + yield return handler; + } + } + + async ValueTask IsActiveAsync(OpenIddictClientHandlerDescriptor descriptor) + { + for (var index = 0; index < descriptor.FilterTypes.Length; index++) + { + if (!(_provider.GetService(descriptor.FilterTypes[index]) is IOpenIddictClientHandlerFilter filter)) + { + throw new InvalidOperationException(SR.FormatID0099(descriptor.FilterTypes[index])); + } + + if (!await filter.IsActiveAsync(context)) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientEndpointType.cs b/src/OpenIddict.Client/OpenIddictClientEndpointType.cs new file mode 100644 index 00000000..23bddf08 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientEndpointType.cs @@ -0,0 +1,23 @@ +/* + * 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.Client; + +/// +/// Represents the type of an OpenIddict client endpoint. +/// +public enum OpenIddictClientEndpointType +{ + /// + /// Unknown endpoint. + /// + Unknown = 0, + + /// + /// Redirection endpoint. + /// + Redirection = 1 +} diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs b/src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs new file mode 100644 index 00000000..7ebf4a75 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs @@ -0,0 +1,190 @@ +/* + * 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; + +namespace OpenIddict.Client; + +public static partial class OpenIddictClientEvents +{ + /// + /// Represents an event called for each request to the authorization endpoint to give the user code + /// a chance to manually update the authorization request before it is sent to the identity provider. + /// + public class PrepareAuthorizationRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public PrepareAuthorizationRequestContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the principal containing the claims stored in the state object. + /// + public ClaimsPrincipal StatePrincipal { get; set; } = new ClaimsPrincipal(new ClaimsIdentity()); + } + + /// + /// Represents an event called for each request to the authorization endpoint + /// to give the user code a chance to manually send the authorization request. + /// + public class ApplyAuthorizationRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyAuthorizationRequestContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + public string AuthorizationEndpoint { get; set; } = null!; + } + + /// + /// Represents an event called for each request to the redirection endpoint to give the user code + /// a chance to manually extract the redirection request from the ambient HTTP context. + /// + public class ExtractRedirectionRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractRedirectionRequestContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request or null if it was extracted yet. + /// + public OpenIddictRequest? Request + { + get => Transaction.Request; + set => Transaction.Request = value; + } + } + + /// + /// Represents an event called for each request to the redirection endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public class ValidateRedirectionRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateRedirectionRequestContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the security principal extracted from the identity token, + /// if applicable to the current redirection request. If no identity token + /// is available at the validation stage, a token request will typically be + /// sent to retrieve a complete set of tokens (e.g authorization code flow). + /// + public ClaimsPrincipal? Principal { get; set; } + + /// + /// Gets or sets the security principal extracted from the state token. + /// + public ClaimsPrincipal? StateTokenPrincipal { get; set; } + } + + /// + /// Represents an event called for each validated redirection request + /// to allow the user code to decide how the request should be handled. + /// + public class HandleRedirectionRequestContext : BaseValidatingTicketContext + { + /// + /// Creates a new instance of the class. + /// + public HandleRedirectionRequestContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets the additional parameters returned to the client application. + /// + public Dictionary Parameters { get; private set; } + = new(StringComparer.Ordinal); + } + + /// + /// Represents an event called before the redirection response is returned to the caller. + /// + public class ApplyRedirectionResponseContext : BaseRequestContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyRedirectionResponseContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request, or null if it couldn't be extracted. + /// + public OpenIddictRequest? Request + { + get => Transaction.Request; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the response. + /// + public OpenIddictResponse Response + { + get => Transaction.Response!; + set => Transaction.Response = value; + } + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.Discovery.cs b/src/OpenIddict.Client/OpenIddictClientEvents.Discovery.cs new file mode 100644 index 00000000..17ba49d9 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientEvents.Discovery.cs @@ -0,0 +1,248 @@ +/* + * 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.IdentityModel.Tokens; + +namespace OpenIddict.Client; + +public static partial class OpenIddictClientEvents +{ + /// + /// Represents an event called for each request to the configuration endpoint + /// to give the user code a chance to add parameters to the configuration request. + /// + public class PrepareConfigurationRequestContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public PrepareConfigurationRequestContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + } + + /// + /// Represents an event called for each request to the configuration endpoint + /// to send the configuration request to the remote authorization server. + /// + public class ApplyConfigurationRequestContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyConfigurationRequestContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + } + + /// + /// Represents an event called for each configuration response + /// to extract the response parameters from the server response. + /// + public class ExtractConfigurationResponseContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractConfigurationResponseContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the response, or null if it wasn't extracted yet. + /// + public OpenIddictResponse? Response + { + get => Transaction.Response; + set => Transaction.Response = value; + } + } + + /// + /// Represents an event called for each validated configuration response. + /// + public class HandleConfigurationResponseContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public HandleConfigurationResponseContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the response. + /// + public OpenIddictResponse Response + { + get => Transaction.Response!; + set => Transaction.Response = value; + } + + /// + /// Gets the OpenID Connect configuration. + /// + public OpenIddictConfiguration Configuration { get; } = new OpenIddictConfiguration(); + } + + /// + /// Represents an event called for each request to the cryptography endpoint + /// to give the user code a chance to add parameters to the cryptography request. + /// + public class PrepareCryptographyRequestContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public PrepareCryptographyRequestContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + } + + /// + /// Represents an event called for each request to the cryptography endpoint + /// to send the cryptography request to the remote authorization server. + /// + public class ApplyCryptographyRequestContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyCryptographyRequestContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + } + + /// + /// Represents an event called for each cryptography response + /// to extract the response parameters from the server response. + /// + public class ExtractCryptographyResponseContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractCryptographyResponseContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the response, or null if it wasn't extracted yet. + /// + public OpenIddictResponse? Response + { + get => Transaction.Response; + set => Transaction.Response = value; + } + } + + /// + /// Represents an event called for each validated cryptography response. + /// + public class HandleCryptographyResponseContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public HandleCryptographyResponseContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the response. + /// + public OpenIddictResponse Response + { + get => Transaction.Response!; + set => Transaction.Response = value; + } + + /// + /// Gets the security keys. + /// + public JsonWebKeySet SecurityKeys { get; } = new JsonWebKeySet(); + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.Exchange.cs b/src/OpenIddict.Client/OpenIddictClientEvents.Exchange.cs new file mode 100644 index 00000000..394a802f --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientEvents.Exchange.cs @@ -0,0 +1,155 @@ +/* + * 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; + +namespace OpenIddict.Client; + +public static partial class OpenIddictClientEvents +{ + /// + /// Represents an event called for each request to the token endpoint + /// to give the user code a chance to add parameters to the token request. + /// + public class PrepareTokenRequestContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public PrepareTokenRequestContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the grant type sent to the token endpoint. + /// + public string? GrantType { get; set; } + + /// + /// Gets or sets the authorization code sent to the token endpoint, if applicable. + /// + public string? AuthorizationCode { get; set; } + } + + /// + /// Represents an event called for each request to the token endpoint + /// to send the token request to the remote authorization server. + /// + public class ApplyTokenRequestContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyTokenRequestContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + } + + /// + /// Represents an event called for each token response + /// to extract the response parameters from the server response. + /// + public class ExtractTokenResponseContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractTokenResponseContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the response, or null if it wasn't extracted yet. + /// + public OpenIddictResponse? Response + { + get => Transaction.Response; + set => Transaction.Response = value; + } + } + + /// + /// Represents an event called for each token response. + /// + public class HandleTokenResponseContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public HandleTokenResponseContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the response. + /// + public OpenIddictResponse Response + { + get => Transaction.Response!; + set => Transaction.Response = value; + } + + /// + /// Gets or sets the access token resolved from the token response. + /// + public string? AccessToken { get; set; } + + /// + /// Gets or sets the identity token resolved from the token response. + /// + public string? IdentityToken { get; set; } + + /// + /// Gets or sets the refresh token resolved from the token response. + /// + public string? RefreshToken { get; set; } + + /// + /// Gets or sets the principal containing the claims resolved from the token response. + /// + public ClaimsPrincipal? Principal { get; set; } + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs b/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs new file mode 100644 index 00000000..154c269f --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs @@ -0,0 +1,130 @@ +/* + * 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 Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; + +namespace OpenIddict.Client; + +public static partial class OpenIddictClientEvents +{ + /// + /// Represents an event called when generating a token. + /// + public class GenerateTokenContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public GenerateTokenContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request, or null if it is not available. + /// + public OpenIddictRequest? Request + { + get => Transaction.Request; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the security principal used to create the token. + /// + public ClaimsPrincipal Principal { get; set; } = default!; + + /// + /// Gets or sets the encryption credentials used to encrypt the token. + /// + public EncryptingCredentials? EncryptionCredentials { 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 security principal. + /// + public JsonWebTokenHandler SecurityTokenHandler { get; set; } = default!; + + /// + /// Gets or sets the token returned to the client application. + /// + public string? Token { get; set; } + + /// + /// Gets or sets the type of the token to create. + /// + public string TokenType { get; set; } = default!; + } + + /// + /// Represents an event called when validating a token. + /// + public class ValidateTokenContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateTokenContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request, or null if it is not available. + /// + public OpenIddictRequest? Request + { + get => Transaction.Request; + set => Transaction.Request = value; + } + + /// + /// Gets or sets a boolean indicating whether lifetime validation is disabled. + /// + public bool DisableLifetimeValidation { get; set; } + + /// + /// Gets or sets the security token handler used to validate the token. + /// + public JsonWebTokenHandler SecurityTokenHandler { get; set; } = default!; + + /// + /// Gets or sets the validation parameters used to verify the authenticity of tokens. + /// + public TokenValidationParameters TokenValidationParameters { get; set; } = default!; + + /// + /// Gets or sets the token to validate. + /// + public string Token { get; set; } = default!; + + /// + /// Gets or sets the token type hint specified by the client, if applicable. + /// + public string? TokenTypeHint { get; set; } = default!; + + /// + /// Gets or sets the token entry identifier associated with the token, if applicable. + /// + public string? TokenId { get; set; } + + /// + /// Gets or sets the security principal resolved from the token. + /// + public ClaimsPrincipal? Principal { get; set; } + + /// + /// Gets the token types that are considered valid. + /// + public HashSet ValidTokenTypes { get; } = new(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.cs b/src/OpenIddict.Client/OpenIddictClientEvents.cs new file mode 100644 index 00000000..7bc8d5ff --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientEvents.cs @@ -0,0 +1,668 @@ +/* + * 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.ComponentModel; +using System.Security.Claims; +using Microsoft.Extensions.Logging; + +namespace OpenIddict.Client; + +public static partial class OpenIddictClientEvents +{ + /// + /// Represents an abstract base class used for certain event contexts. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class BaseContext + { + /// + /// Creates a new instance of the class. + /// + protected BaseContext(OpenIddictClientTransaction transaction) + => Transaction = transaction ?? throw new ArgumentNullException(nameof(transaction)); + + /// + /// Gets the environment associated with the current request being processed. + /// + public OpenIddictClientTransaction Transaction { get; } + + /// + /// Gets or sets the endpoint type that handled the request, if applicable. + /// + public OpenIddictClientEndpointType EndpointType + { + get => Transaction.EndpointType; + set => Transaction.EndpointType = value; + } + + /// + /// Gets the logger responsible of logging processed operations. + /// + public ILogger Logger => Transaction.Logger; + + /// + /// Gets the OpenIddict client options. + /// + public OpenIddictClientOptions Options => Transaction.Options; + + /// + /// Gets or sets the issuer used for the current request. + /// + public Uri? Issuer + { + get => Transaction.Issuer; + set => Transaction.Issuer = value; + } + + /// + /// Gets or sets the client registration used for the current request. + /// + public OpenIddictClientRegistration Registration + { + get => Transaction.Registration; + set => Transaction.Registration = value; + } + } + + /// + /// Represents an abstract base class used for certain event contexts. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class BaseRequestContext : BaseContext + { + /// + /// Creates a new instance of the class. + /// + protected BaseRequestContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets a boolean indicating whether the request was fully handled. + /// + public bool IsRequestHandled { get; private set; } + + /// + /// Gets a boolean indicating whether the request processing was skipped. + /// + public bool IsRequestSkipped { get; private set; } + + /// + /// Marks the request as fully handled. Once declared handled, + /// a request shouldn't be processed further by the underlying host. + /// + public void HandleRequest() => IsRequestHandled = true; + + /// + /// 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). + /// + public void SkipRequest() => IsRequestSkipped = true; + } + + /// + /// Represents an abstract base class used for certain event contexts. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class BaseExternalContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + protected BaseExternalContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the address of the external endpoint to communicate with. + /// + public Uri? Address { get; set; } + } + + /// + /// Represents an abstract base class used for certain event contexts. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class BaseValidatingContext : BaseRequestContext + { + /// + /// Creates a new instance of the class. + /// + protected BaseValidatingContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets a boolean indicating whether the request will be rejected. + /// + public bool IsRejected { get; protected set; } + + /// + /// Gets or sets the "error" parameter returned to the client application. + /// + public string? Error { get; private set; } + + /// + /// Gets or sets the "error_description" parameter returned to the client application. + /// + public string? ErrorDescription { get; private set; } + + /// + /// Gets or sets the "error_uri" parameter returned to the client application. + /// + public string? ErrorUri { get; private set; } + + /// + /// Rejects the request. + /// + /// 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 = null, string? description = null, string? uri = null) + { + Error = error; + ErrorDescription = description; + ErrorUri = uri; + + IsRejected = true; + } + } + + /// + /// Represents an abstract base class used for certain event contexts. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class BaseValidatingTicketContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + protected BaseValidatingTicketContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the security principal. + /// + public ClaimsPrincipal? Principal { get; set; } + } + + /// + /// Represents an event called when processing an incoming request. + /// + public class ProcessRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ProcessRequestContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called when processing an errored response. + /// + public class ProcessErrorContext : BaseRequestContext + { + /// + /// Creates a new instance of the class. + /// + public ProcessErrorContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request, or null if it couldn't be extracted. + /// + public OpenIddictRequest? Request + { + get => Transaction.Request; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the response. + /// + public OpenIddictResponse Response + { + get => Transaction.Response!; + set => Transaction.Response = value; + } + + /// + /// Gets or sets the error returned to the caller. + /// + public string? Error { get; set; } + + /// + /// Gets or sets the error description returned to the caller. + /// + public string? ErrorDescription { get; set; } + + /// + /// Gets or sets the error URL returned to the caller. + /// + public string? ErrorUri { get; set; } + + /// + /// Gets the additional parameters returned to the caller. + /// + public Dictionary Parameters { get; } = new(StringComparer.Ordinal); + } + + /// + /// Represents an event called when processing an authentication operation. + /// + public class ProcessAuthenticationContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ProcessAuthenticationContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets or sets a boolean indicating whether a backchannel + /// access token should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractBackchannelAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a backchannel + /// identity token should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractBackchannelIdentityToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a backchannel + /// refresh token should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractBackchannelRefreshToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a frontchannel + /// access token should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractFrontchannelAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a frontchannel + /// authorization code should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractFrontchannelAuthorizationCode { get; set; } + + /// + /// Gets or sets a boolean indicating whether a frontchannel + /// identity token should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractFrontchannelIdentityToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a frontchannel + /// state token should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractFrontchannelStateToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a backchannel access + /// token must be resolved for the authentication to be considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RequireBackchannelAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a backchannel identity + /// token must be resolved for the authentication to be considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RequireBackchannelIdentityToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a backchannel refresh + /// token must be resolved for the authentication to be considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RequireBackchannelRefreshToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a frontchannel identity + /// token must be resolved for the authentication to be considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RequireFrontchannelAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a backchannel authorization + /// code must be resolved for the authentication to be considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RequireFrontchannelAuthorizationCode { get; set; } + + /// + /// Gets or sets a boolean indicating whether a frontchannel identity + /// token must be resolved for the authentication to be considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RequireFrontchannelIdentityToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a frontchannel state token + /// must be resolved for the authentication to be considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RequireFrontchannelStateToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether the backchannel access + /// token extracted from the current context should be validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateBackchannelAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether the backchannel identity + /// token extracted from the current context should be validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateBackchannelIdentityToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether the backchannel refresh token + /// extracted from the current context should be validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateBackchannelRefreshToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether the frontchannel access + /// token extracted from the current context should be validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateFrontchannelAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether the frontchannel authorization + /// code extracted from the current context should be validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateFrontchannelAuthorizationCode { get; set; } + + /// + /// Gets or sets a boolean indicating whether the frontchannel identity + /// token extracted from the current context should be validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateFrontchannelIdentityToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether the frontchannel state token + /// extracted from the current context should be validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateFrontchannelStateToken { get; set; } + + /// + /// Gets or sets the backchannel access token to validate, if applicable. + /// + public string? BackchannelAccessToken { get; set; } + + /// + /// Gets or sets the backchannel identity token to validate, if applicable. + /// + public string? BackchannelIdentityToken { get; set; } + + /// + /// Gets or sets the backchannel refresh token to validate, if applicable. + /// + public string? BackchannelRefreshToken { get; set; } + + /// + /// Gets or sets the frontchannel access token to validate, if applicable. + /// + public string? FrontchannelAccessToken { get; set; } + + /// + /// Gets or sets the frontchannel authorization code to validate, if applicable. + /// + public string? FrontchannelAuthorizationCode { get; set; } + + /// + /// Gets or sets the frontchannel identity token to validate, if applicable. + /// + public string? FrontchannelIdentityToken { get; set; } + + /// + /// Gets or sets the frontchannel state token to validate, if applicable. + /// + public string? FrontchannelStateToken { get; set; } + + /// + /// Gets or sets the principal extracted from the backchannel access token, if applicable. + /// + public ClaimsPrincipal? BackchannelAccessTokenPrincipal { get; set; } + + /// + /// Gets or sets the principal extracted from the backchannel identity token, if applicable. + /// + public ClaimsPrincipal? BackchannelIdentityTokenPrincipal { get; set; } + + /// + /// Gets or sets the principal extracted from the backchannel refresh token, if applicable. + /// + public ClaimsPrincipal? BackchannelRefreshTokenPrincipal { get; set; } + + /// + /// Gets or sets the principal extracted from the frontchannel access token, if applicable. + /// + public ClaimsPrincipal? FrontchannelAccessTokenPrincipal { get; set; } + + /// + /// Gets or sets the principal extracted from the frontchannel identity token, if applicable. + /// + public ClaimsPrincipal? FrontchannelIdentityTokenPrincipal { get; set; } + + /// + /// Gets or sets the principal extracted from the frontchannel authorization code, if applicable. + /// + public ClaimsPrincipal? FrontchannelAuthorizationCodePrincipal { get; set; } + + /// + /// Gets or sets the principal extracted from the frontchannel state token, if applicable. + /// + public ClaimsPrincipal? FrontchannelStateTokenPrincipal { get; set; } + + /// + /// Gets or sets the request sent to the token endpoint, if applicable. + /// + public OpenIddictRequest? TokenRequest { get; set; } + + /// + /// Gets or sets the response returned by the token endpoint, if applicable. + /// + public OpenIddictResponse? TokenResponse { get; set; } + } + + /// + /// Represents an event called when processing a challenge response. + /// + public class ProcessChallengeContext : BaseValidatingTicketContext + { + /// + /// Creates a new instance of the class. + /// + public ProcessChallengeContext(OpenIddictClientTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the response. + /// + public OpenIddictResponse Response + { + get => Transaction.Response!; + set => Transaction.Response = value; + } + + /// + /// Gets the additional parameters returned to caller. + /// + public Dictionary Parameters { get; } = new(StringComparer.Ordinal); + + /// + /// Gets or sets the client identifier that will be used for the challenge demand. + /// + public string? ClientId { get; set; } + + /// + /// Gets or sets the grant type that will be used for the challenge demand. + /// + public string? GrantType { get; set; } + + /// + /// Gets or sets the response mode that will be + /// used for the challenge demand, if applicable. + /// + public string? ResponseMode { get; set; } + + /// + /// Gets or sets the response type that will be + /// used for the challenge demand, if applicable. + /// + public string? ResponseType { get; set; } + + /// + /// Gets or sets the redirection endpoint that will + /// be used for the challenge demand, if applicable. + /// + public string? RedirectUri { get; set; } + + /// + /// Gets or sets the code challenge that will + /// be used for the challenge demand, if applicable. + /// + public string? CodeChallenge { get; set; } + + /// + /// Gets or sets the code challenge method that will + /// be used for the challenge demand, if applicable. + /// + public string? CodeChallengeMethod { get; set; } + + /// + /// Gets or sets the code verifier that will be stored in the state token, if applicable. + /// + public string? CodeVerifier { get; set; } + + /// + /// Gets or sets the nonce that will be used for the challenge demand, if applicable. + /// + public string? Nonce { get; set; } + + /// + /// Gets or sets the request forgery protection that will be stored in the state token, if applicable. + /// Note: this value MUST NOT be user-defined or extracted from any request and MUST be random + /// (generated by a random number generator suitable for cryptographic operations). + /// + public string? RequestForgeryProtection { get; set; } + + /// + /// Gets or sets the optional return URL that will be stored in the state token, if applicable. + /// + public string? TargetLinkUri { get; set; } + + /// + /// Gets the set of scopes that will be requested to the authorization server. + /// + public HashSet Scopes { get; } = new(StringComparer.Ordinal); + + /// + /// Gets or sets a boolean indicating whether a state token + /// should be generated (and optionally included in the request). + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool GenerateStateToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether the generated + /// state token should be included as part of the request. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool IncludeStateToken { get; set; } + + /// + /// Gets or sets the generated state token, if applicable. + /// The access token will only be returned if + /// is set to true. + /// + public string? StateToken { get; set; } + + /// + /// Gets or sets the principal containing the claims that + /// will be used to create the state token, if applicable. + /// + public ClaimsPrincipal? StateTokenPrincipal { get; set; } + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientExtensions.cs b/src/OpenIddict.Client/OpenIddictClientExtensions.cs new file mode 100644 index 00000000..a24cbdd5 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientExtensions.cs @@ -0,0 +1,87 @@ +/* + * 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.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using OpenIddict.Client; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Exposes extensions allowing to register the OpenIddict client services. +/// +public static class OpenIddictClientExtensions +{ + /// + /// Registers the OpenIddict client 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 OpenIddictClientBuilder AddClient(this OpenIddictBuilder builder) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Services.AddLogging(); + builder.Services.AddOptions(); + + builder.Services.TryAddScoped(); + builder.Services.TryAddScoped(); + builder.Services.TryAddSingleton(); + + // Register the built-in filters used by the default OpenIddict client 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(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + + // Register the built-in client event handlers used by the OpenIddict client 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)); + + // Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< + IPostConfigureOptions, OpenIddictClientConfiguration>()); + + return new OpenIddictClientBuilder(builder.Services); + } + + /// + /// Registers the OpenIddict client services in the DI container. + /// + /// The services builder used by OpenIddict to register new services. + /// The configuration delegate used to configure the client services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictBuilder AddClient(this OpenIddictBuilder builder, Action configuration) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + configuration(builder.AddClient()); + + return builder; + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientFactory.cs b/src/OpenIddict.Client/OpenIddictClientFactory.cs new file mode 100644 index 00000000..44d3bef9 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientFactory.cs @@ -0,0 +1,34 @@ +/* + * 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.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace OpenIddict.Client; + +public class OpenIddictClientFactory : IOpenIddictClientFactory +{ + private readonly ILogger _logger; + private readonly IOptionsMonitor _options; + + /// + /// Creates a new instance of the class. + /// + public OpenIddictClientFactory( + ILogger logger, + IOptionsMonitor options) + { + _logger = logger; + _options = options; + } + + public ValueTask CreateTransactionAsync() + => new ValueTask(new OpenIddictClientTransaction + { + Logger = _logger, + Options = _options.CurrentValue + }); +} diff --git a/src/OpenIddict.Client/OpenIddictClientHandler.cs b/src/OpenIddict.Client/OpenIddictClientHandler.cs new file mode 100644 index 00000000..9abeaaf9 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientHandler.cs @@ -0,0 +1,33 @@ +/* + * 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.Client; + +/// +/// Represents a handler able to process events. +/// +/// The type of the events handled by this instance. +public class OpenIddictClientHandler : IOpenIddictClientHandler where TContext : BaseContext +{ + private readonly Func _handler; + + /// + /// Creates a new event using the specified handler delegate. + /// + /// The event handler delegate. + public OpenIddictClientHandler(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. + /// + public ValueTask HandleAsync(TContext context) + => _handler(context ?? throw new ArgumentNullException(nameof(context))); +} diff --git a/src/OpenIddict.Client/OpenIddictClientHandlerDescriptor.cs b/src/OpenIddict.Client/OpenIddictClientHandlerDescriptor.cs new file mode 100644 index 00000000..6c4abd8b --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientHandlerDescriptor.cs @@ -0,0 +1,281 @@ +/* + * 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.ComponentModel; +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; + +namespace OpenIddict.Client; + +/// +/// Represents an immutable descriptor of an OpenIddict client event handler. +/// +[DebuggerDisplay("{ServiceDescriptor?.ServiceType}")] +public class OpenIddictClientHandlerDescriptor +{ + /// + /// Creates a new instance of the class. + /// + private OpenIddictClientHandlerDescriptor() { } + + /// + /// Gets the context type associated with the event. + /// + public Type ContextType { get; private set; } = default!; + + /// + /// 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; } = default!; + + /// + /// Gets the type associated with the handler. + /// + public OpenIddictClientHandlerType Type { 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 _filters = new(); + private int _order; + private OpenIddictClientHandlerType _type; + + /// + /// 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(Type type) + { + if (type is null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (!typeof(IOpenIddictClientHandlerFilter<>).MakeGenericType(typeof(TContext)).IsAssignableFrom(type)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0104)); + } + + _filters.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 : IOpenIddictClientHandlerFilter + => AddFilter(typeof(TFilter)); + + /// + /// Imports the properties set on the specified descriptor. + /// + /// The existing descriptor properties are copied from. + /// All the properties previously set on this instance are automatically replaced. + /// The builder instance, so that calls can be easily chained. + public Builder Import(OpenIddictClientHandlerDescriptor descriptor) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + if (descriptor.ContextType != typeof(TContext)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0284)); + } + + _descriptor = descriptor.ServiceDescriptor; + _filters.Clear(); + _filters.AddRange(descriptor.FilterTypes); + _order = descriptor.Order; + _type = descriptor.Type; + + return this; + } + + /// + /// Sets the service descriptor. + /// + /// The service descriptor. + /// The builder instance, so that calls can be easily chained. + public Builder SetServiceDescriptor(ServiceDescriptor descriptor) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + var type = descriptor.ServiceType; + if (!typeof(IOpenIddictClientHandler<>).MakeGenericType(typeof(TContext)).IsAssignableFrom(type)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0104)); + } + + _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; + } + + /// + /// Sets the type associated to the handler. + /// + /// The handler type. + /// The builder instance, so that calls can be easily chained. + public Builder SetType(OpenIddictClientHandlerType type) + { + if (!Enum.IsDefined(typeof(OpenIddictClientHandlerType), type)) + { + throw new InvalidEnumArgumentException(nameof(type), (int) type, typeof(OpenIddictClientHandlerType)); + } + + _type = type; + + 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(Func handler) + { + if (handler is null) + { + throw new ArgumentNullException(nameof(handler)); + } + + return UseSingletonHandler(new OpenIddictClientHandler(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 : IOpenIddictClientHandler + => SetServiceDescriptor(new ServiceDescriptor( + typeof(THandler), typeof(THandler), ServiceLifetime.Scoped)); + + /// + /// Configures the descriptor to use the specified scoped handler. + /// + /// The handler type. + /// The factory used to create the handler. + /// The builder instance, so that calls can be easily chained. + public Builder UseScopedHandler(Func factory) + where THandler : IOpenIddictClientHandler + { + if (factory is null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return SetServiceDescriptor(new ServiceDescriptor( + typeof(THandler), factory, 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 : IOpenIddictClientHandler + => SetServiceDescriptor(new ServiceDescriptor( + typeof(THandler), typeof(THandler), ServiceLifetime.Singleton)); + + /// + /// Configures the descriptor to use the specified singleton handler. + /// + /// The handler type. + /// The factory used to create the handler. + /// The builder instance, so that calls can be easily chained. + public Builder UseSingletonHandler(Func factory) + where THandler : IOpenIddictClientHandler + { + if (factory is null) + { + throw new ArgumentNullException(nameof(factory)); + } + + return SetServiceDescriptor(new ServiceDescriptor( + typeof(THandler), factory, 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(THandler handler) + where THandler : IOpenIddictClientHandler + { + if (handler is 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 OpenIddictClientHandlerDescriptor Build() => new OpenIddictClientHandlerDescriptor + { + ContextType = typeof(TContext), + FilterTypes = _filters.ToImmutableArray(), + Order = _order, + ServiceDescriptor = _descriptor ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0105)), + Type = _type + }; + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs new file mode 100644 index 00000000..8cd751a5 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs @@ -0,0 +1,222 @@ +/* + * 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.ComponentModel; + +namespace OpenIddict.Client; + +[EditorBrowsable(EditorBrowsableState.Advanced)] +public static class OpenIddictClientHandlerFilters +{ + /// + /// Represents a filter that excludes the associated handlers if the challenge + /// doesn't correspond to an authorization code or implicit grant operation. + /// + public class RequireAuthorizationCodeOrImplicitGrantType : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.GrantType is GrantTypes.AuthorizationCode or GrantTypes.Implicit); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no backchannel access token is validated. + /// + public class RequireBackchannelAccessTokenValidated : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateBackchannelAccessToken); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no backchannel identity token is validated. + /// + public class RequireBackchannelIdentityTokenValidated : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateBackchannelIdentityToken); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no backchannel refresh token is validated. + /// + public class RequireBackchannelRefreshTokenValidated : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateBackchannelRefreshToken); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no backchannel request is expected to be sent. + /// + public class RequireBackchannelRequest : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.TokenRequest is not null); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no backchannel response was received. + /// + public class RequireBackchannelResponse : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.TokenResponse is not null); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no frontchannel access token is validated. + /// + public class RequireFrontchannelAccessTokenValidated : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateFrontchannelAccessToken); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no frontchannel authorization code is extracted. + /// + public class RequireFrontchannelAuthorizationCodeExtracted : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ExtractFrontchannelAuthorizationCode); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no frontchannel authorization code is validated. + /// + public class RequireFrontchannelAuthorizationCodeValidated : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateFrontchannelAuthorizationCode); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no frontchannel identity token is validated. + /// + public class RequireFrontchannelIdentityTokenValidated : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateFrontchannelIdentityToken); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no frontchannel state token is validated. + /// + public class RequireFrontchannelStateTokenValidated : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateFrontchannelStateToken); + } + } + + /// + /// Represents a filter that excludes the associated handlers if the request is not a redirection request. + /// + public class RequireRedirectionRequest : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.EndpointType == OpenIddictClientEndpointType.Redirection); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no state token is generated. + /// + public class RequireStateTokenGenerated : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.GenerateStateToken); + } + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientHandlerType.cs b/src/OpenIddict.Client/OpenIddictClientHandlerType.cs new file mode 100644 index 00000000..3aff9a13 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientHandlerType.cs @@ -0,0 +1,28 @@ +/* + * 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.Client; + +/// +/// Represents the type of an OpenIddict client handler. +/// +public enum OpenIddictClientHandlerType +{ + /// + /// The handler is of an unspecified type. + /// + Unknown = 0, + + /// + /// The handler is a built-in handler, provided as part of the official OpenIddict packages. + /// + BuiltIn = 1, + + /// + /// The handler is a custom handler, registered by the end user or a third-party package. + /// + Custom = 2 +} diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs new file mode 100644 index 00000000..12639c0e --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs @@ -0,0 +1,509 @@ +/* + * 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 Microsoft.Extensions.Logging; + +namespace OpenIddict.Client; + +public static partial class OpenIddictClientHandlers +{ + public static class Authentication + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Authorization request top-level processing: + */ + PrepareAuthorizationRequest.Descriptor, + ApplyAuthorizationRequest.Descriptor, + + /* + * Authorization request preparation: + */ + NormalizeResponseModeParameter.Descriptor, + + /* + * Authorization request processing: + */ + AttachAuthorizationEndpoint.Descriptor, + + /* + * Redirection request top-level processing: + */ + ExtractRedirectionRequest.Descriptor, + ValidateRedirectionRequest.Descriptor, + HandleRedirectionRequest.Descriptor, + ApplyRedirectionResponse.Descriptor, + ApplyRedirectionResponse.Descriptor, + + /* + * Redirection request validation: + */ + ValidateTokens.Descriptor); + + /// + /// Contains the logic responsible of preparing authorization requests and invoking the corresponding event handlers. + /// + public class PrepareAuthorizationRequest : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public PrepareAuthorizationRequest(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var notification = new PrepareAuthorizationRequestContext(context.Transaction); + await _dispatcher.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + } + + /// + /// Contains the logic responsible of applying authorization requests and invoking the corresponding event handlers. + /// + public class ApplyAuthorizationRequest : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public ApplyAuthorizationRequest(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(PrepareAuthorizationRequest.Descriptor.Order + 1_000) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var notification = new ApplyAuthorizationRequestContext(context.Transaction); + await _dispatcher.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + } + + /// + /// Contains the logic responsible of attaching the address of the authorization request to the request. + /// + public class AttachAuthorizationEndpoint : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + public async ValueTask HandleAsync(ApplyAuthorizationRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + // Ensure the issuer resolved from the configuration matches the expected value. + if (configuration.Issuer != context.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + } + + // Ensure the authorization endpoint is present and is a valid absolute URL. + if (configuration.AuthorizationEndpoint is not { IsAbsoluteUri: true } || + !configuration.AuthorizationEndpoint.IsWellFormedOriginalString()) + { + throw new InvalidOperationException(SR.FormatID0301(Metadata.AuthorizationEndpoint)); + } + + context.AuthorizationEndpoint = configuration.AuthorizationEndpoint.AbsoluteUri; + } + } + + /// + /// Contains the logic responsible of extracting redirection requests and invoking the corresponding event handlers. + /// + public class ExtractRedirectionRequest : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public ExtractRedirectionRequest(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(100_000) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var notification = new ExtractRedirectionRequestContext(context.Transaction); + await _dispatcher.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 is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0302)); + } + + context.Logger.LogInformation(SR.GetResourceString(SR.ID6178), notification.Request); + } + } + + /// + /// Contains the logic responsible of validating redirection requests and invoking the corresponding event handlers. + /// + public class ValidateRedirectionRequest : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public ValidateRedirectionRequest(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ExtractRedirectionRequest.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var notification = new ValidateRedirectionRequestContext(context.Transaction); + await _dispatcher.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(SR.GetResourceString(SR.ID6179)); + } + } + + /// + /// Contains the logic responsible of handling redirection requests and invoking the corresponding event handlers. + /// + public class HandleRedirectionRequest : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public HandleRedirectionRequest(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateRedirectionRequest.Descriptor.Order + 1_000) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var notification = new HandleRedirectionRequestContext(context.Transaction); + await _dispatcher.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(SR.GetResourceString(SR.ID6180)); + } + } + + /// + /// Contains the logic responsible of processing redirection responses and invoking the corresponding event handlers. + /// + public class ApplyRedirectionResponse : IOpenIddictClientHandler where TContext : BaseRequestContext + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public ApplyRedirectionResponse(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler>() + .SetOrder(int.MaxValue - 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var notification = new ApplyRedirectionResponseContext(context.Transaction); + await _dispatcher.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + throw new InvalidOperationException(SR.GetResourceString(SR.ID0303)); + } + } + + /// + /// Contains the logic responsible of removing the response mode parameter from the + /// request if it corresponds to the default mode for the selected response type. + /// + public class NormalizeResponseModeParameter : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + public ValueTask HandleAsync(PrepareAuthorizationRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // When the response mode corresponds to the default mode assigned to the selected + // response type, the specification explicitly recommends omitting the response mode. + // As such, this handler is expected to remove the mode parameter in the following cases: + // - Authorization code flow: response_mode=query. + // - Hybrid flow: response_mode=fragment. + // - Implicit flow: response_mode=fragment. + // + // For more information, read + // https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes. + // + if (!string.IsNullOrEmpty(context.Request.ResponseMode) && + (context.Request.IsAuthorizationCodeFlow() && context.Request.IsQueryResponseMode()) || + (context.Request.IsHybridFlow() && context.Request.IsFragmentResponseMode()) || + (context.Request.IsImplicitFlow() && context.Request.IsFragmentResponseMode())) + { + context.Request.ResponseMode = null; + } + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting redirection requests that don't + /// specify a valid access token, authorization code, identity token or state token. + /// + public class ValidateTokens : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public ValidateTokens(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ValidateRedirectionRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var notification = new ProcessAuthenticationContext(context.Transaction); + await _dispatcher.DispatchAsync(notification); + + // Store the context object in the transaction so it can be later retrieved by handlers + // that want to access the authentication result without triggering a new authentication flow. + context.Transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName!, 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; + } + + // Attach the security principal extracted from the token to the validation context. + context.Principal = notification.FrontchannelIdentityTokenPrincipal; + context.StateTokenPrincipal = notification.FrontchannelStateTokenPrincipal; + } + } + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs new file mode 100644 index 00000000..fd75f692 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs @@ -0,0 +1,651 @@ +/* + * 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 Microsoft.IdentityModel.Tokens; + +namespace OpenIddict.Client; + +public static partial class OpenIddictClientHandlers +{ + public static class Discovery + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Configuration response handling: + */ + HandleErrorResponse.Descriptor, + ValidateIssuer.Descriptor, + ExtractAuthorizationEndpoint.Descriptor, + ExtractCryptographyEndpoint.Descriptor, + ExtractTokenEndpoint.Descriptor, + ExtractTokenEndpointClientAuthenticationMethods.Descriptor, + ExtractGrantTypes.Descriptor, + ExtractResponseModes.Descriptor, + ExtractResponseTypes.Descriptor, + ExtractCodeChallengeMethods.Descriptor, + ExtractScopes.Descriptor, + ExtractIssuerParameterRequirement.Descriptor, + + /* + * Cryptography response handling: + */ + HandleErrorResponse.Descriptor, + ExtractSigningKeys.Descriptor); + + /// + /// Contains the logic responsible of extracting the issuer from the discovery document. + /// + public class ValidateIssuer : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // The issuer returned in the discovery document must exactly match the URL used to access it. + // See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationClient. + var issuer = (string?) context.Response[Metadata.Issuer]; + if (string.IsNullOrEmpty(issuer)) + { + context.Reject( + error: Errors.ServerError, + description: SR.GetResourceString(SR.ID2096), + uri: SR.FormatID8000(SR.ID2096)); + + return default; + } + + if (!Uri.TryCreate(issuer, UriKind.Absolute, out Uri? address)) + { + context.Reject( + error: Errors.ServerError, + description: SR.GetResourceString(SR.ID2097), + uri: SR.FormatID8000(SR.ID2097)); + + return default; + } + + context.Configuration.Issuer = address; + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the JWKS endpoint address from the discovery document. + /// + public class ExtractCryptographyEndpoint : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateIssuer.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the jwks_uri node is required by the OpenID Connect discovery specification. + // See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationClient. + var address = (string?) context.Response[Metadata.JwksUri]; + if (string.IsNullOrEmpty(address)) + { + context.Reject( + error: Errors.ServerError, + description: SR.GetResourceString(SR.ID2099), + uri: SR.FormatID8000(SR.ID2099)); + + return default; + } + + if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString()) + { + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2100(Metadata.JwksUri), + uri: SR.FormatID8000(SR.ID2100)); + + return default; + } + + context.Configuration.JwksUri = uri; + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the token endpoint address from the discovery document. + /// + public class ExtractTokenEndpoint : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ExtractCryptographyEndpoint.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var address = (string?) context.Response[Metadata.TokenEndpoint]; + if (!string.IsNullOrEmpty(address)) + { + if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString()) + { + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2100(Metadata.TokenEndpoint), + uri: SR.FormatID8000(SR.ID2100)); + + return default; + } + + context.Configuration.TokenEndpoint = uri; + } + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the authentication methods + /// supported by the token endpoint from the discovery document. + /// + public class ExtractTokenEndpointClientAuthenticationMethods : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ExtractTokenEndpoint.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Resolve the client authentication methods supported by the token endpoint, if available. + var methods = context.Response[Metadata.TokenEndpointAuthMethodsSupported]?.GetUnnamedParameters(); + if (methods is { Count: > 0 }) + { + for (var index = 0; index < methods.Count; index++) + { + // Note: custom values are allowed in this case. + var method = (string?) methods[index]; + if (!string.IsNullOrEmpty(method)) + { + context.Configuration.TokenEndpointAuthMethodsSupported.Add(method); + } + } + } + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the authorization endpoint address from the discovery document. + /// + public class ExtractAuthorizationEndpoint : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ExtractTokenEndpointClientAuthenticationMethods.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the authorization_endpoint node is required by the OpenID Connect discovery specification + // but is optional in the OAuth 2.0 authorization server metadata specification. To make OpenIddict + // compatible with the newer OAuth 2.0 specification, null/empty and missing values are allowed here. + // + // Handlers that require a non-null authorization endpoint URL are expected to return an error + // if the authorization endpoint URL couldn't be resolved from the authorization server metadata. + // See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationClient + // and https://datatracker.ietf.org/doc/html/rfc8414#section-2 for more information. + // + var address = (string?) context.Response[Metadata.AuthorizationEndpoint]; + if (!string.IsNullOrEmpty(address)) + { + if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString()) + { + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2100(Metadata.AuthorizationEndpoint), + uri: SR.FormatID8000(SR.ID2100)); + + return default; + } + + context.Configuration.AuthorizationEndpoint = uri; + } + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the supported grant types from the discovery document. + /// + public class ExtractGrantTypes : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ExtractAuthorizationEndpoint.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Resolve the grant types supported by the authorization endpoint, if available. + var types = context.Response[Metadata.GrantTypesSupported]?.GetUnnamedParameters(); + if (types is { Count: > 0 }) + { + for (var index = 0; index < types.Count; index++) + { + // Note: custom values are allowed in this case. + var type = (string?) types[index]; + if (!string.IsNullOrEmpty(type)) + { + context.Configuration.GrantTypesSupported.Add(type); + } + } + } + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the supported response types from the discovery document. + /// + public class ExtractResponseModes : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ExtractAuthorizationEndpoint.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Resolve the response modes supported by the authorization endpoint, if available. + var modes = context.Response[Metadata.ResponseModesSupported]?.GetUnnamedParameters(); + if (modes is { Count: > 0 }) + { + for (var index = 0; index < modes.Count; index++) + { + // Note: custom values are allowed in this case. + var mode = (string?) modes[index]; + if (!string.IsNullOrEmpty(mode)) + { + context.Configuration.ResponseModesSupported.Add(mode); + } + } + } + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the supported response types from the discovery document. + /// + public class ExtractResponseTypes : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ExtractResponseModes.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Resolve the response types supported by the authorization endpoint, if available. + var types = context.Response[Metadata.ResponseTypesSupported]?.GetUnnamedParameters(); + if (types is { Count: > 0 }) + { + for (var index = 0; index < types.Count; index++) + { + // Note: custom values are allowed in this case. + var type = (string?) types[index]; + if (!string.IsNullOrEmpty(type)) + { + context.Configuration.ResponseTypesSupported.Add(type); + } + } + } + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the supported code challenge methods from the discovery document. + /// + public class ExtractCodeChallengeMethods : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ExtractResponseTypes.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Resolve the code challenge methods supported by the authorization endpoint, if available. + var methods = context.Response[Metadata.CodeChallengeMethodsSupported]?.GetUnnamedParameters(); + if (methods is { Count: > 0 }) + { + for (var index = 0; index < methods.Count; index++) + { + // Note: custom values are allowed in this case. + var method = (string?) methods[index]; + if (!string.IsNullOrEmpty(method)) + { + context.Configuration.CodeChallengeMethodsSupported.Add(method); + } + } + } + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the supported scopes from the discovery document. + /// + public class ExtractScopes : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ExtractCodeChallengeMethods.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Resolve the scopes supported by the remote server, if available. + var scopes = context.Response[Metadata.ScopesSupported]?.GetUnnamedParameters(); + if (scopes is { Count: > 0 }) + { + for (var index = 0; index < scopes.Count; index++) + { + // Note: custom values are allowed in this case. + var scope = (string?) scopes[index]; + if (!string.IsNullOrEmpty(scope)) + { + context.Configuration.ScopesSupported.Add(scope); + } + } + } + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the flag indicating + /// whether the "iss" parameter is supported from the discovery document. + /// + public class ExtractIssuerParameterRequirement : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ExtractScopes.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.Configuration.AuthorizationResponseIssParameterSupported = (bool?) + context.Response[Metadata.AuthorizationResponseIssParameterSupported]; + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the signing keys from the JWKS document. + /// + public class ExtractSigningKeys : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleCryptographyResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var keys = context.Response[JsonWebKeySetParameterNames.Keys]?.GetUnnamedParameters(); + if (keys is not { Count: > 0 }) + { + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2102(JsonWebKeySetParameterNames.Keys), + uri: SR.FormatID8000(SR.ID2102)); + + return default; + } + + for (var index = 0; index < keys.Count; index++) + { + // Note: the "use" parameter is defined as optional by the specification. + // To prevent key swapping attacks, OpenIddict requires that this parameter + // be present and will ignore keys that don't include a "use" parameter. + var use = (string?) keys[index][JsonWebKeyParameterNames.Use]; + if (string.IsNullOrEmpty(use)) + { + continue; + } + + // Ignore security keys that are not used for signing. + if (!string.Equals(use, JsonWebKeyUseNames.Sig, StringComparison.Ordinal)) + { + continue; + } + + var key = (string?) keys[index][JsonWebKeyParameterNames.Kty] switch + { + JsonWebAlgorithmsKeyTypes.RSA => new JsonWebKey + { + Kty = JsonWebAlgorithmsKeyTypes.RSA, + E = (string?) keys[index][JsonWebKeyParameterNames.E], + N = (string?) keys[index][JsonWebKeyParameterNames.N] + }, + + JsonWebAlgorithmsKeyTypes.EllipticCurve => new JsonWebKey + { + Kty = JsonWebAlgorithmsKeyTypes.EllipticCurve, + Crv = (string?) keys[index][JsonWebKeyParameterNames.Crv], + X = (string?) keys[index][JsonWebKeyParameterNames.X], + Y = (string?) keys[index][JsonWebKeyParameterNames.Y] + }, + + _ => null + }; + + if (key is null) + { + context.Reject( + error: Errors.ServerError, + description: SR.GetResourceString(SR.ID2103), + uri: SR.FormatID8000(SR.ID2103)); + + return default; + } + + // If the key is a RSA key, ensure the mandatory parameters are all present. + if (string.Equals(key.Kty, JsonWebAlgorithmsKeyTypes.RSA, StringComparison.Ordinal) && + (string.IsNullOrEmpty(key.E) || string.IsNullOrEmpty(key.N))) + { + context.Reject( + error: Errors.ServerError, + description: SR.GetResourceString(SR.ID2104), + uri: SR.FormatID8000(SR.ID2104)); + + return default; + } + + // If the key is an EC key, ensure the mandatory parameters are all present. + if (string.Equals(key.Kty, JsonWebAlgorithmsKeyTypes.EllipticCurve, StringComparison.Ordinal) && + (string.IsNullOrEmpty(key.Crv) || string.IsNullOrEmpty(key.X) || string.IsNullOrEmpty(key.Y))) + { + context.Reject( + error: Errors.ServerError, + description: SR.GetResourceString(SR.ID2104), + uri: SR.FormatID8000(SR.ID2104)); + + return default; + } + + key.KeyId = (string?) keys[index][JsonWebKeyParameterNames.Kid]; + key.X5t = (string?) keys[index][JsonWebKeyParameterNames.X5t]; + key.X5tS256 = (string?) keys[index][JsonWebKeyParameterNames.X5tS256]; + + if (keys[index].TryGetNamedParameter(JsonWebKeyParameterNames.X5c, out var chain)) + { + foreach (string? certificate in chain.GetUnnamedParameters()) + { + if (string.IsNullOrEmpty(certificate)) + { + continue; + } + + key.X5c.Add(certificate); + } + } + + context.SecurityKeys.Keys.Add(key); + } + + return default; + } + } + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Exchange.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Exchange.cs new file mode 100644 index 00000000..efc73eb1 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Exchange.cs @@ -0,0 +1,78 @@ +/* + * 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.Text.Json; + +namespace OpenIddict.Client; + +public static partial class OpenIddictClientHandlers +{ + public static class Exchange + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Token response handling: + */ + HandleErrorResponse.Descriptor, + ValidateWellKnownParameters.Descriptor); + + /// + /// Contains the logic responsible of validating the well-known parameters contained in the token response. + /// + public class ValidateWellKnownParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleTokenResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + foreach (var parameter in context.Response.GetParameters()) + { + if (ValidateParameterType(parameter.Key, parameter.Value.Value)) + { + continue; + } + + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2107(parameter.Key), + uri: SR.FormatID8000(SR.ID2107)); + + return default; + } + + return default; + + static bool ValidateParameterType(string name, object? value) => name switch + { + // The 'expires_in' parameter MUST be formatted as a numeric date value. + Parameters.ExpiresIn => value is long or JsonElement { ValueKind: JsonValueKind.Number }, + + // The 'access_token', 'id_token' and 'refresh_token' parameters MUST be formatted as unique strings. + Parameters.AccessToken or Parameters.IdToken or Parameters.RefreshToken + => value is string or JsonElement { ValueKind: JsonValueKind.String }, + + // Parameters that are not in the well-known list can be of any type. + _ => true + }; + } + } + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs new file mode 100644 index 00000000..fa041b79 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs @@ -0,0 +1,507 @@ +/* + * 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.Diagnostics; +using System.Globalization; +using System.Security.Claims; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; + +namespace OpenIddict.Client; + +public static partial class OpenIddictClientHandlers +{ + public static class Protection + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Token validation: + */ + ResolveTokenValidationParameters.Descriptor, + ValidateIdentityModelToken.Descriptor, + MapInternalClaims.Descriptor, + ValidatePrincipal.Descriptor, + ValidateExpirationDate.Descriptor, + + /* + * Token generation: + */ + AttachSecurityCredentials.Descriptor, + GenerateIdentityModelToken.Descriptor); + + /// + /// Contains the logic responsible of resolving the validation parameters used to validate tokens. + /// + public class ResolveTokenValidationParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // The OpenIddict client is expected to validate tokens it creates (e.g state tokens) and + // tokens that are created by one or multiple authorization servers (e.g identity tokens). + // + // While state tokens could also be created by the authorization servers themselves, + // this scenario is currently not supported. To simplify the token validation parameters + // selection logic, an exception is thrown if multiple token types are considered valid + // and contain tokens issued by the authorization server and tokens issued by the client. + // + // See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09#section-4.3 + // for more information. + if (context.ValidTokenTypes.Count > 1 && context.ValidTokenTypes.Contains(TokenTypeHints.StateToken)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0308)); + } + + var parameters = context.ValidTokenTypes.Count switch + { + // When only state tokens are considered valid, use the token validation parameters of the client. + 1 when context.ValidTokenTypes.Contains(TokenTypeHints.StateToken) + => GetClientTokenValidationParameters(context.Options), + + // Otherwise, use the token validation parameters of the authorization server. + _ => await GetServerTokenValidationParametersAsync(context.Registration) + }; + + context.SecurityTokenHandler = context.Options.JsonWebTokenHandler; + context.TokenValidationParameters = parameters; + + static TokenValidationParameters GetClientTokenValidationParameters(OpenIddictClientOptions options) + { + var parameters = options.TokenValidationParameters.Clone(); + parameters.ValidateIssuer = false; + + // For state tokens, only the short "oi_stet+jwt" form is valid. + parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.StateToken }; + + return parameters; + } + + static async Task GetServerTokenValidationParametersAsync( + OpenIddictClientRegistration registration) + { + var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + // Ensure the issuer resolved from the configuration matches the expected value. + if (configuration.Issuer != registration!.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + } + + var parameters = registration!.TokenValidationParameters.Clone(); + + parameters.ValidIssuer ??= configuration.Issuer?.AbsoluteUri ?? registration.Issuer?.AbsoluteUri; + parameters.ValidateIssuer = !string.IsNullOrEmpty(parameters.ValidIssuer); + + // Combine the signing keys registered statically in the token validation parameters + // with the signing keys resolved from the OpenID Connect server configuration. + parameters.IssuerSigningKeys = + parameters.IssuerSigningKeys?.Concat(configuration.SigningKeys) ?? configuration.SigningKeys; + + // For maximum compatibility, all "typ" values are accepted for all types of JSON Web Tokens, + // which typically includes identity tokens but can also include access tokens, authorization + // codes or refresh tokens for non-standard implementations that need to read these tokens. + // + // To prevent token mix-up/confused deputy attacks, additional checks (e.g audience validation) + // are expected to be made by specialized handlers later in the token validation processing. + parameters.ValidTypes = null; + + return parameters; + } + } + } + + /// + /// Contains the logic responsible of validating tokens generated using IdentityModel. + /// + public class ValidateIdentityModelToken : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ResolveTokenValidationParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a principal was already attached, don't overwrite it. + if (context.Principal is not null) + { + return; + } + + // If the token cannot be read, don't return an error to allow another handler to validate it. + if (!context.SecurityTokenHandler.CanReadToken(context.Token)) + { + return; + } + + var result = await context.SecurityTokenHandler.ValidateTokenAsync(context.Token, context.TokenValidationParameters); + if (!result.IsValid) + { + context.Logger.LogTrace(result.Exception, SR.GetResourceString(SR.ID6000), context.Token); + + context.Reject( + error: Errors.InvalidToken, + description: result.Exception switch + { + SecurityTokenInvalidTypeException => SR.GetResourceString(SR.ID2089), + SecurityTokenInvalidIssuerException => SR.GetResourceString(SR.ID2088), + SecurityTokenSignatureKeyNotFoundException => SR.GetResourceString(SR.ID2090), + SecurityTokenInvalidSignatureException => SR.GetResourceString(SR.ID2091), + + _ => SR.GetResourceString(SR.ID2004) + }, + uri: result.Exception switch + { + SecurityTokenInvalidTypeException => SR.FormatID8000(SR.ID2089), + SecurityTokenInvalidIssuerException => SR.FormatID8000(SR.ID2088), + SecurityTokenSignatureKeyNotFoundException => SR.FormatID8000(SR.ID2090), + SecurityTokenInvalidSignatureException => SR.FormatID8000(SR.ID2091), + + _ => SR.FormatID8000(SR.ID2004) + }); + + return; + } + + // Get the JWT token. If the token is encrypted using JWE, retrieve the inner token. + var token = (JsonWebToken) result.SecurityToken; + if (token.InnerToken is not null) + { + token = token.InnerToken; + } + + // Clone the identity and remove OpenIddict-specific claims from tokens that are not fully trusted. + var identity = result.ClaimsIdentity.Clone(claim => claim switch + { + // Exclude claims starting with "oi_", unless the token is a state token. + { Type: string type } when type.StartsWith(Claims.Prefixes.Private) && + result.TokenType is not JsonWebTokenTypes.Private.StateToken => false, + + _ => true // Allow any other claim. + }); + + // Attach the principal extracted from the token to the parent event context and store + // the token type (resolved from "typ" or "token_usage") as a special private claim. + context.Principal = new ClaimsPrincipal(identity).SetTokenType(result.TokenType switch + { + null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)), + + // Both JWT and application/JWT are supported for identity tokens. + JsonWebTokenTypes.IdentityToken or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.IdentityToken + => TokenTypeHints.IdToken, + + JsonWebTokenTypes.Private.StateToken => TokenTypeHints.StateToken, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + }); + + // Store the resolved signing algorithm from the token and attach it to the principal. + context.Principal.SetClaim(Claims.Private.SigningAlgorithm, token.Alg); + + context.Logger.LogTrace(SR.GetResourceString(SR.ID6001), context.Token, context.Principal.Claims); + } + } + + /// + /// Contains the logic responsible of mapping internal claims used by OpenIddict. + /// + public class MapInternalClaims : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: only map the private claims from fully trusted tokens. + if (context.Principal is null || !context.Principal.HasTokenType(TokenTypeHints.StateToken)) + { + return default; + } + + // In OpenIddict 3.0, the creation date of a token is stored in "oi_crt_dt". + // If the claim doesn't exist, try to infer it from the standard "iat" JWT claim. + if (!context.Principal.HasClaim(Claims.Private.CreationDate)) + { + var date = context.Principal.GetClaim(Claims.IssuedAt); + if (!string.IsNullOrEmpty(date) && + long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + context.Principal.SetCreationDate(DateTimeOffset.FromUnixTimeSeconds(value)); + } + } + + // In OpenIddict 3.0, the expiration date of a token is stored in "oi_exp_dt". + // If the claim doesn't exist, try to infer it from the standard "exp" JWT claim. + if (!context.Principal.HasClaim(Claims.Private.ExpirationDate)) + { + var date = context.Principal.GetClaim(Claims.ExpiresAt); + if (!string.IsNullOrEmpty(date) && + long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + context.Principal.SetExpirationDate(DateTimeOffset.FromUnixTimeSeconds(value)); + } + } + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting authentication demands for which no valid principal was resolved. + /// + public class ValidatePrincipal : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Principal is null) + { + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2004), + uri: SR.FormatID8000(SR.ID2004)); + + return default; + } + + // When using JWT or Data Protection tokens, the correct token type is always enforced by IdentityModel + // (using the "typ" header) or by ASP.NET Core Data Protection (using per-token-type purposes strings). + // To ensure tokens deserialized using a custom routine are of the expected type, a manual check is used, + // which requires that a special claim containing the token type be present in the security principal. + var type = context.Principal.GetTokenType(); + if (string.IsNullOrEmpty(type)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0004)); + } + + if (context.ValidTokenTypes.Count > 0 && !context.ValidTokenTypes.Contains(type)) + { + throw new InvalidOperationException(SR.FormatID0005(type, string.Join(", ", context.ValidTokenTypes))); + } + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting authentication demands that use an expired token. + /// + public class ValidateExpirationDate : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidatePrincipal.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + var date = context.Principal.GetExpirationDate(); + if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + { + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2019), + uri: SR.FormatID8000(SR.ID2019)); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of resolving the signing and encryption credentials used to protect tokens. + /// + public class AttachSecurityCredentials : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.SecurityTokenHandler = context.Options.JsonWebTokenHandler; + + context.EncryptionCredentials = context.Options.EncryptionCredentials.First(); + context.SigningCredentials = context.Options.SigningCredentials.First(); + + return default; + } + } + + /// + /// Contains the logic responsible of generating a token using IdentityModel. + /// + public class GenerateIdentityModelToken : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachSecurityCredentials.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a token was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Token)) + { + return default; + } + + if (context.Principal is not { Identity: ClaimsIdentity }) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); + } + + // Clone the principal and exclude the private claims mapped to standard JWT claims. + var principal = context.Principal.Clone(claim => claim.Type switch + { + Claims.Private.CreationDate or Claims.Private.ExpirationDate or Claims.Private.TokenType => false, + + Claims.Private.Audience when context.TokenType is TokenTypeHints.StateToken => false, + + _ => true + }); + + Debug.Assert(principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + var claims = new Dictionary(StringComparer.Ordinal); + + // For state tokens, set the public audience claims using + // the private audience claims from the security principal. + if (context.TokenType is TokenTypeHints.StateToken) + { + var audiences = context.Principal.GetAudiences(); + if (audiences.Any()) + { + claims.Add(Claims.Audience, audiences.Length switch + { + 1 => audiences.ElementAt(0), + _ => audiences + }); + } + } + + var descriptor = new SecurityTokenDescriptor + { + Claims = claims, + EncryptingCredentials = context.EncryptionCredentials, + Expires = context.Principal.GetExpirationDate()?.UtcDateTime, + IssuedAt = context.Principal.GetCreationDate()?.UtcDateTime, + SigningCredentials = context.SigningCredentials, + Subject = (ClaimsIdentity) principal.Identity, + TokenType = context.TokenType switch + { + null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)), + + TokenTypeHints.StateToken => JsonWebTokenTypes.Private.StateToken, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + } + }; + + context.Token = context.SecurityTokenHandler.CreateToken(descriptor); + + context.Logger.LogTrace(SR.GetResourceString(SR.ID6013), context.TokenType, context.Token, principal.Claims); + + return default; + } + } + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs new file mode 100644 index 00000000..4cbed4b2 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -0,0 +1,3467 @@ +/* + * 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.ComponentModel; +using System.Diagnostics; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +#if !SUPPORTS_TIME_CONSTANT_COMPARISONS +using Org.BouncyCastle.Utilities; +#endif + +namespace OpenIddict.Client; + +[EditorBrowsable(EditorBrowsableState.Never)] +public static partial class OpenIddictClientHandlers +{ + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Authentication processing: + */ + ValidateAuthenticationDemand.Descriptor, + EvaluateValidatedUpfrontTokens.Descriptor, + ResolveUpfrontTokens.Descriptor, + ValidateRequiredUpfrontTokens.Descriptor, + ValidateFrontchannelStateToken.Descriptor, + ResolveClientRegistrationFromStateToken.Descriptor, + ValidateIssuerParameter.Descriptor, + ValidateFrontchannelErrorParameters.Descriptor, + ValidateGrantType.Descriptor, + + EvaluateValidatedFrontchannelTokens.Descriptor, + ResolveValidatedFrontchannelTokens.Descriptor, + ValidateRequiredFrontchannelTokens.Descriptor, + + ValidateFrontchannelIdentityToken.Descriptor, + ValidateFrontchannelIdentityTokenWellknownClaims.Descriptor, + ValidateFrontchannelIdentityTokenAudience.Descriptor, + ValidateFrontchannelIdentityTokenPresenter.Descriptor, + ValidateFrontchannelIdentityTokenNonce.Descriptor, + ValidateFrontchannelTokenDigests.Descriptor, + + ValidateFrontchannelAccessToken.Descriptor, + ValidateFrontchannelAuthorizationCode.Descriptor, + + EvaluateValidatedBackchannelTokens.Descriptor, + + AttachBackchannelRequestParameters.Descriptor, + SendBackchannelRequest.Descriptor, + ValidateBackchannelErrorParameters.Descriptor, + + ResolveValidatedBackchannelTokens.Descriptor, + ValidateRequiredBackchannelTokens.Descriptor, + + ValidateBackchannelIdentityToken.Descriptor, + ValidateBackchannelIdentityTokenWellknownClaims.Descriptor, + ValidateBackchannelIdentityTokenAudience.Descriptor, + ValidateBackchannelIdentityTokenPresenter.Descriptor, + ValidateBackchannelIdentityTokenNonce.Descriptor, + ValidateBackchannelTokenDigests.Descriptor, + + ValidateBackchannelAccessToken.Descriptor, + ValidateBackchannelRefreshToken.Descriptor, + + /* + * Challenge processing: + */ + ValidateChallengeDemand.Descriptor, + ResolveClientRegistration.Descriptor, + AttachGrantType.Descriptor, + EvaluateGeneratedTokens.Descriptor, + AttachResponseType.Descriptor, + AttachResponseMode.Descriptor, + AttachClientId.Descriptor, + AttachRedirectUri.Descriptor, + AttachRequestForgeryProtection.Descriptor, + AttachScopes.Descriptor, + AttachNonce.Descriptor, + AttachCodeChallengeParameters.Descriptor, + PrepareStateTokenPrincipal.Descriptor, + ValidateRedirectUriParameter.Descriptor, + GenerateStateToken.Descriptor, + AttachChallengeParameters.Descriptor, + + /* + * Error processing: + */ + AttachErrorParameters.Descriptor) + + .AddRange(Authentication.DefaultHandlers) + .AddRange(Discovery.DefaultHandlers) + .AddRange(Protection.DefaultHandlers); + + /// + /// Contains the logic responsible of rejecting invalid authentication demands. + /// + public class ValidateAuthenticationDemand : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + switch (context.EndpointType) + { + case OpenIddictClientEndpointType.Redirection: + break; + + default: throw new InvalidOperationException(SR.GetResourceString(SR.ID0290)); + } + + return default; + } + } + + /// + /// Contains the logic responsible of determining the types of tokens to validate upfront. + /// + public class EvaluateValidatedUpfrontTokens : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateAuthenticationDemand.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + (context.ExtractFrontchannelStateToken, + context.RequireFrontchannelStateToken, + context.ValidateFrontchannelStateToken) = context.EndpointType switch + { + // While the OAuth 2.0/2.1 and OpenID Connect specifications don't require sending a + // state as part of authorization requests, the identity provider MUST return the state + // if one was initially specified. Since OpenIddict always sends a state (used as a way + // to mitigate CSRF attacks and store per-authorization values like the identity of the + // chosen authorization server), the state is always considered required at this point. + OpenIddictClientEndpointType.Redirection => (true, true, true), + + _ => (false, false, false) + }; + + return default; + } + } + + /// + /// Contains the logic responsible of resolving the tokens to validate upfront from the incoming request. + /// + public class ResolveUpfrontTokens : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(EvaluateValidatedUpfrontTokens.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.FrontchannelStateToken = context.EndpointType switch + { + OpenIddictClientEndpointType.Redirection when context.ExtractFrontchannelStateToken + => context.Request.State, + + _ => null + }; + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting authentication demands that lack required upfront tokens. + /// + public class ValidateRequiredUpfrontTokens : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ResolveUpfrontTokens.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.RequireFrontchannelStateToken && string.IsNullOrEmpty(context.FrontchannelStateToken)) + { + context.Reject( + error: Errors.MissingToken, + description: SR.GetResourceString(SR.ID2000), + uri: SR.FormatID8000(SR.ID2000)); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of validating the state token resolved from the context. + /// + public class ValidateFrontchannelStateToken : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public ValidateFrontchannelStateToken(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateRequiredUpfrontTokens.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.FrontchannelStateTokenPrincipal is not null || + string.IsNullOrEmpty(context.FrontchannelStateToken)) + { + return; + } + + var notification = new ValidateTokenContext(context.Transaction) + { + Token = context.FrontchannelStateToken, + ValidTokenTypes = { TokenTypeHints.StateToken } + }; + + await _dispatcher.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.FrontchannelStateTokenPrincipal = notification.Principal; + } + } + + /// + /// Contains the logic responsible of resolving the client registration + /// based on the authorization server identity stored in the state token. + /// + public class ResolveClientRegistrationFromStateToken : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateFrontchannelStateToken.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Retrieve the client definition using the authorization server stored in the state token. + // + // Note: there's no guarantee that the state token was not replaced by a malicious actor + // by a state token meant to be used with a different authorization server as part of a + // mix-up attack where the state token and the authorization code or access/identity tokens + // wouldn't match. To mitigate this, additional defenses are added later by other handlers. + + // Restore the identity of the authorization server from the special "as" claim. + // See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09#section-2 + // for more information. + var value = context.FrontchannelStateTokenPrincipal.GetClaim(Claims.AuthorizationServer); + if (string.IsNullOrEmpty(value) || !Uri.TryCreate(value, UriKind.Absolute, out Uri? issuer) || + !issuer.IsWellFormedOriginalString()) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0291)); + } + + // Note: if the static registration cannot be found in the options, this may indicate + // the client was removed after the authorization dance started and thus, can no longer + // be used to authenticate users. In this case, throw an exception to abort the flow. + var registration = context.Options.Registrations.Find(registration => registration.Issuer == issuer); + if (registration is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0292)); + } + + context.Issuer = issuer; + context.Registration = registration; + + return default; + } + } + + /// + /// Contains the logic responsible of ensuring the issuer parameter, if available, matches the expected issuer. + /// + public class ValidateIssuerParameter : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ResolveClientRegistrationFromStateToken.Descriptor.Order + 1_000) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); + + var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + // Ensure the issuer resolved from the configuration matches the expected value. + if (configuration.Issuer != context.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + } + + // To help mitigate mix-up attacks, the identity of the issuer can be returned by + // authorization servers that support it as a part of the "iss" parameter, which + // allows comparing it to the issuer in the state token. Depending on the selected + // response_type, the same information could be retrieved from the identity token + // that is expected to contain an "iss" claim containing the issuer identity. + // + // This handler eagerly validates the "iss" parameter if the authorization server + // is known to support it (and automatically rejects the request if it doesn't). + // Validation based on the identity token is performed later in the pipeline. + // + // See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-4.4 + // for more information. + var issuer = (string?) context.Request[Parameters.Iss]; + + if (configuration.AuthorizationResponseIssParameterSupported is true) + { + // Reject authorization responses that don't contain the "iss" parameter + // if the server configuration indicates this parameter should be present. + if (string.IsNullOrEmpty(issuer)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2029), + uri: SR.FormatID8000(SR.ID2029)); + + return; + } + + // If the two values don't match, this may indicate a mix-up attack attempt. + if (!string.Equals(issuer, context.Issuer.AbsoluteUri, StringComparison.Ordinal)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2119(Parameters.Iss), + uri: SR.FormatID8000(SR.ID2119)); + + return; + } + } + + // Reject authorization responses containing an "iss" parameter if the configuration + // doesn't indicate this parameter is supported, as recommended by the specification. + // See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-iss-auth-resp-05#section-2.4 + // for more information. + else if (!string.IsNullOrEmpty(issuer)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2120(Parameters.Iss, Metadata.AuthorizationResponseIssParameterSupported), + uri: SR.FormatID8000(SR.ID2120)); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting errored authorization responses. + /// + public class ValidateFrontchannelErrorParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateIssuerParameter.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var (error, description, uri) = ( + (string?) context.Request[Parameters.Error], + (string?) context.Request[Parameters.ErrorDescription], + (string?) context.Request[Parameters.ErrorUri]); + + if (!string.IsNullOrEmpty(error)) + { + context.Reject( + error: error ?? Errors.InvalidRequest, + description: description, + uri: uri); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of ensuring the authentication demand is valid + /// based on the grant type initially negotiated and stored in the state token. + /// + public class ValidateGrantType : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateIssuerParameter.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Resolve the negotiated grant type from the state token. + var type = context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.GrantType); + + // Note: OpenIddict currently only supports the implicit and authorization code + // grants but additional grants (like CIBA) may be supported in future versions. + switch (context.EndpointType) + { + // Authentication demands triggered from the redirection endpoint are only valid for + // the authorization code and implicit grants (which includes the hybrid flow, that + // can be represented using either the authorization code or implicit grant types). + case OpenIddictClientEndpointType.Redirection when type is not + (GrantTypes.AuthorizationCode or GrantTypes.Implicit): + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2130), + uri: SR.FormatID8000(SR.ID2130)); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of determining the set of frontchannel tokens to validate. + /// + public class EvaluateValidatedFrontchannelTokens : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateGrantType.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Resolve the grant grant and the response type stored in the state token and extract its individual elements. + var types = ( + GrantType: context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.GrantType), + ResponseTypes: context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.ResponseType) + !.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries) + .ToImmutableHashSet()); + + (context.ExtractFrontchannelAccessToken, + context.RequireFrontchannelAccessToken, + context.ValidateFrontchannelAccessToken) = types switch + { + // An access token is returned for the authorization code and implicit grants when + // the response type contains the "token" value, which includes some variations of + // the implicit and hybrid flows, but not the authorization code flow. As such, + // a frontchannel access token is only considered required if a token was requested. + // + // Note: since access tokens are supposed to be opaque to the clients, they are never + // validated by default. Clients that need to deal with non-standard implementations + // can use custom handlers to validate access tokens that use a readable format (e.g JWT). + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet set) + when set.Contains(ResponseTypes.Token) => (true, true, false), + + _ => (false, false, false) + }; + + (context.ExtractFrontchannelAuthorizationCode, + context.RequireFrontchannelAuthorizationCode, + context.ValidateFrontchannelAuthorizationCode) = types switch + { + // An authorization code is returned for the authorization code and implicit grants when + // the response type contains the "code" value, which includes the authorization code + // flow and some variations of the hybrid flow. As such, an authorization code is only + // considered required if the negotiated response_type includes "code". + // + // Note: since authorization codes are supposed to be opaque to the clients, they are never + // validated by default. Clients that need to deal with non-standard implementations + // can use custom handlers to validate access tokens that use a readable format (e.g JWT). + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet set) + when set.Contains(ResponseTypes.Code) => (true, true, false), + + _ => (false, false, false) + }; + + (context.ExtractFrontchannelIdentityToken, + context.RequireFrontchannelIdentityToken, + context.ValidateFrontchannelIdentityToken) = types switch + { + // An identity token is returned for the authorization code and implicit grants when + // the response type contains the "id_token" value, which includes some variations + // of the implicit and hybrid flows, but not the authorization code flow. As such, + // a frontchannel identity token is only considered required if an id_token was requested. + // + // Note: the granted scopes list (returned as a "scope" parameter in authorization + // responses) is not used in this case as it's not protected against tampering. + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet set) + when set.Contains(ResponseTypes.IdToken) => (true, true, true), + + _ => (false, false, false) + }; + + return default; + } + } + + /// + /// Contains the logic responsible of resolving the token from the incoming request. + /// + public class ResolveValidatedFrontchannelTokens : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(EvaluateValidatedFrontchannelTokens.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.FrontchannelAccessToken = context.EndpointType switch + { + OpenIddictClientEndpointType.Redirection when context.ExtractFrontchannelAccessToken + => context.Request.AccessToken, + + _ => null + }; + + context.FrontchannelAuthorizationCode = context.EndpointType switch + { + OpenIddictClientEndpointType.Redirection when context.ExtractFrontchannelAuthorizationCode + => context.Request.Code, + + _ => null + }; + + context.FrontchannelIdentityToken = context.EndpointType switch + { + OpenIddictClientEndpointType.Redirection when context.ExtractFrontchannelIdentityToken + => context.Request.IdToken, + + _ => null + }; + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting authentication demands that lack required tokens. + /// + public class ValidateRequiredFrontchannelTokens : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ResolveValidatedFrontchannelTokens.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if ((context.RequireFrontchannelAccessToken && string.IsNullOrEmpty(context.FrontchannelAccessToken)) || + (context.RequireFrontchannelAuthorizationCode && string.IsNullOrEmpty(context.FrontchannelAuthorizationCode)) || + (context.RequireFrontchannelIdentityToken && string.IsNullOrEmpty(context.FrontchannelIdentityToken))) + { + context.Reject( + error: Errors.MissingToken, + description: SR.GetResourceString(SR.ID2000), + uri: SR.FormatID8000(SR.ID2000)); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of validating the frontchannel identity token resolved from the context. + /// + public class ValidateFrontchannelIdentityToken : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public ValidateFrontchannelIdentityToken(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateRequiredFrontchannelTokens.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.FrontchannelIdentityTokenPrincipal is not null || + string.IsNullOrEmpty(context.FrontchannelIdentityToken)) + { + return; + } + + var notification = new ValidateTokenContext(context.Transaction) + { + Token = context.FrontchannelIdentityToken, + ValidTokenTypes = { TokenTypeHints.IdToken } + }; + + await _dispatcher.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.FrontchannelIdentityTokenPrincipal = notification.Principal; + } + } + + /// + /// Contains the logic responsible of validating the well-known claims contained in the frontchannel identity token. + /// + public class ValidateFrontchannelIdentityTokenWellknownClaims : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateFrontchannelIdentityToken.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.FrontchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + foreach (var group in context.FrontchannelIdentityTokenPrincipal.Claims + .GroupBy(claim => claim.Type) + .ToDictionary(group => group.Key, group => group.ToList())) + { + if (ValidateClaimGroup(group)) + { + continue; + } + + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2121(group.Key), + uri: SR.FormatID8000(SR.ID2121)); + + return default; + } + + // Identity tokens MUST contain an "iss" claim. For more information, + // see https://openid.net/specs/openid-connect-core-1_0.html#IDToken. + if (!context.FrontchannelIdentityTokenPrincipal.HasClaim(Claims.Issuer)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2122(Claims.Issuer), + uri: SR.FormatID8000(SR.ID2122)); + + return default; + } + + // Identity tokens MUST contain a "sub" claim. For more information, + // see https://openid.net/specs/openid-connect-core-1_0.html#IDToken. + if (!context.FrontchannelIdentityTokenPrincipal.HasClaim(Claims.Subject)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2122(Claims.Subject), + uri: SR.FormatID8000(SR.ID2122)); + + return default; + } + + // Identity tokens MUST contain at least one "aud" claim. For more information, + // see https://openid.net/specs/openid-connect-core-1_0.html#IDToken. + if (!context.FrontchannelIdentityTokenPrincipal.HasClaim(Claims.Audience)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2122(Claims.Audience), + uri: SR.FormatID8000(SR.ID2122)); + + return default; + } + + // Identity tokens MUST contain contain a "exp" claim. For more information, + // see https://openid.net/specs/openid-connect-core-1_0.html#IDToken. + if (!context.FrontchannelIdentityTokenPrincipal.HasClaim(Claims.ExpiresAt)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2122(Claims.ExpiresAt), + uri: SR.FormatID8000(SR.ID2122)); + + return default; + } + + // Identity tokens MUST contain contain an "iat" claim. For more information, + // see https://openid.net/specs/openid-connect-core-1_0.html#IDToken. + if (!context.FrontchannelIdentityTokenPrincipal.HasClaim(Claims.IssuedAt)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2122(Claims.IssuedAt), + uri: SR.FormatID8000(SR.ID2122)); + + return default; + } + + return default; + + static bool ValidateClaimGroup(KeyValuePair> claims) => claims switch + { + // The following JWT claims MUST be represented as unique strings. + { + Key: Claims.AuthenticationContextReference or Claims.AuthorizedParty or Claims.Issuer or Claims.Nonce, + Value: List values + } => values.Count is 1 && values[0].ValueType is ClaimValueTypes.String, + + // The following JWT claims MUST be represented as unique strings or array of strings. + { + Key: Claims.Audience or Claims.AuthenticationMethodReference, + Value: List values + } => values.Count switch + { + 1 => values[0].ValueType is ClaimValueTypes.String, + _ => values.All(value => value.ValueType is ClaimValueTypes.String) + }, + + // The following JWT claims MUST be represented as unique numeric dates. + { + Key: Claims.AuthenticationTime or Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore, + Value: List values + } => values.Count is 1 && values[0].ValueType is ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or + ClaimValueTypes.Integer64 or ClaimValueTypes.Double or + ClaimValueTypes.UInteger32 or ClaimValueTypes.UInteger64, + + // Claims that are not in the well-known list can be of any type. + _ => true + }; + } + } + + /// + /// Contains the logic responsible of validating the audience returned in the frontchannel identity token, if applicable. + /// + public class ValidateFrontchannelIdentityTokenAudience : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateFrontchannelIdentityTokenWellknownClaims.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.FrontchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Note: while an identity token typically contains a single audience represented + // as a JSON string, multiple values can be returned represented a a JSON array. + // + // In any case, the client identifier of the application MUST be included in the audiences. + // See https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information. + var audiences = context.FrontchannelIdentityTokenPrincipal.GetClaims(Claims.Audience); + if (!audiences.Contains(context.Registration.ClientId!)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2123), + uri: SR.FormatID8000(SR.ID2123)); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of validating the presenter returned in the frontchannel identity token, if applicable. + /// + public class ValidateFrontchannelIdentityTokenPresenter : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateFrontchannelIdentityTokenAudience.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.FrontchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Note: the "azp" claim is optional, but if it's present, it MUST match the client identifier of the application. + // See https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information. + var presenter = context.FrontchannelIdentityTokenPrincipal.GetClaim(Claims.AuthorizedParty); + if (!string.IsNullOrEmpty(presenter) && + !string.Equals(presenter, context.Registration.ClientId, StringComparison.Ordinal)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2123), + uri: SR.FormatID8000(SR.ID2123)); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of validating the nonce returned in the frontchannel identity token, if applicable. + /// + public class ValidateFrontchannelIdentityTokenNonce : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateFrontchannelIdentityTokenPresenter.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.FrontchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + switch (( + FrontchannelIdentityTokenNonce: context.FrontchannelIdentityTokenPrincipal.GetClaim(Claims.Nonce), + StateTokenNonce: context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.Nonce))) + { + // If no nonce if no present in the state token (e.g because the authorization server doesn't + // support OpenID Connect and response_type=code was negotiated), bypass the validation logic. + case { StateTokenNonce: null or { Length: not > 0 } }: + return default; + + // If a nonce was found in the state token but is not present in the identity token, return an error. + case { FrontchannelIdentityTokenNonce: null or { Length: not > 0 } }: + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2122(Claims.Nonce), + uri: SR.FormatID8000(SR.ID2122)); + + return default; + + // If the two nonces don't match, return an error. + case { FrontchannelIdentityTokenNonce: { } left, StateTokenNonce: { } right } when +#if SUPPORTS_TIME_CONSTANT_COMPARISONS + !CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(left), Encoding.ASCII.GetBytes(right)): +#else + !Arrays.ConstantTimeAreEqual(Encoding.ASCII.GetBytes(left), Encoding.ASCII.GetBytes(right)): +#endif + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2124(Claims.Nonce), + uri: SR.FormatID8000(SR.ID2124)); + + return default; + + default: return default; + } + } + } + + /// + /// Contains the logic responsible of validating the digests of the frontchannel tokens, if applicable. + /// + public class ValidateFrontchannelTokenDigests : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateFrontchannelIdentityTokenNonce.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.FrontchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Resolve the signing algorithm used to sign the identity token. If the private + // claim cannot be found, it means the "alg" header of the identity token was + // malformed but the token was still considered valid. While highly unlikly, + // an exception is thrown in this case to abort the authentication demand. + var name = context.FrontchannelIdentityTokenPrincipal.GetClaim(Claims.Private.SigningAlgorithm); + if (string.IsNullOrEmpty(name)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0293)); + } + + // Resolve the hash algorithm corresponding to the signing algorithm. If an + // instance of the BCL hash algorithm cannot be resolved, throw an exception. + var algorithm = GetHashAlgorithm(name); + if (algorithm is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0293)); + } + + // If a frontchannel access token was returned in the authorization response, + // ensure the at_hash claim matches the hash of the actual access token. + if (!string.IsNullOrEmpty(context.FrontchannelAccessToken)) + { + var hash = context.FrontchannelIdentityTokenPrincipal.GetClaim(Claims.AccessTokenHash); + if (string.IsNullOrEmpty(hash)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2122(Claims.AccessTokenHash), + uri: SR.FormatID8000(SR.ID2122)); + + return default; + } + + if (!ValidateTokenHash(algorithm, context.FrontchannelAccessToken, hash)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2124(Claims.AccessTokenHash), + uri: SR.FormatID8000(SR.ID2124)); + + return default; + } + } + + // If an authorization code was returned in the authorization response, + // ensure the c_hash claim matches the hash of the actual authorization code. + if (!string.IsNullOrEmpty(context.FrontchannelAuthorizationCode)) + { + var hash = context.FrontchannelIdentityTokenPrincipal.GetClaim(Claims.CodeHash); + if (string.IsNullOrEmpty(hash)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2122(Claims.CodeHash), + uri: SR.FormatID8000(SR.ID2122)); + + return default; + } + + if (!ValidateTokenHash(algorithm, context.FrontchannelAuthorizationCode, hash)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2124(Claims.CodeHash), + uri: SR.FormatID8000(SR.ID2124)); + + return default; + } + } + + static byte[] ComputeTokenHash(HashAlgorithm algorithm, string token) + { + // Note: only the left-most half of the access token and authorization code digest is used. + // See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken for more information. + var digest = algorithm.ComputeHash(Encoding.ASCII.GetBytes(token)); + + return Encoding.ASCII.GetBytes(Base64UrlEncoder.Encode(digest, 0, digest.Length / 2)); + } + + static bool ValidateTokenHash(HashAlgorithm algorithm, string token, string hash) => +#if SUPPORTS_TIME_CONSTANT_COMPARISONS + CryptographicOperations.FixedTimeEquals( + left: Encoding.ASCII.GetBytes(hash), + right: ComputeTokenHash(algorithm, token)); +#else + Arrays.ConstantTimeAreEqual( + a: Encoding.ASCII.GetBytes(hash), + b: ComputeTokenHash(algorithm, token)); +#endif + + return default; + + static HashAlgorithm? GetHashAlgorithm(string algorithm) => algorithm switch + { + SecurityAlgorithms.EcdsaSha256 or SecurityAlgorithms.HmacSha256 or + SecurityAlgorithms.RsaSha256 or SecurityAlgorithms.RsaSsaPssSha256 + => CryptoConfig.CreateFromName(SecurityAlgorithms.Sha256) as HashAlgorithm, + + SecurityAlgorithms.EcdsaSha384 or SecurityAlgorithms.HmacSha384 or + SecurityAlgorithms.RsaSha384 or SecurityAlgorithms.RsaSsaPssSha384 + => CryptoConfig.CreateFromName(SecurityAlgorithms.Sha384) as HashAlgorithm, + + SecurityAlgorithms.EcdsaSha512 or SecurityAlgorithms.HmacSha384 or + SecurityAlgorithms.RsaSha512 or SecurityAlgorithms.RsaSsaPssSha512 + => CryptoConfig.CreateFromName(SecurityAlgorithms.Sha512) as HashAlgorithm, + + _ => null + }; + } + } + + /// + /// Contains the logic responsible of validating the frontchannel access token resolved from the context. + /// Note: this handler is typically not used for standard-compliant implementations as access tokens + /// are supposed to be opaque to clients. + /// + public class ValidateFrontchannelAccessToken : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public ValidateFrontchannelAccessToken(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateFrontchannelTokenDigests.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.FrontchannelAccessTokenPrincipal is not null || + string.IsNullOrEmpty(context.FrontchannelAccessToken)) + { + return; + } + + var notification = new ValidateTokenContext(context.Transaction) + { + Token = context.FrontchannelAccessToken, + ValidTokenTypes = { TokenTypeHints.AccessToken } + }; + + await _dispatcher.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.FrontchannelAccessTokenPrincipal = notification.Principal; + } + } + + /// + /// Contains the logic responsible of validating the frontchannel authorization code resolved from the context. + /// Note: this handler is typically not used for standard-compliant implementations as authorization codes + /// are supposed to be opaque to clients. + /// + public class ValidateFrontchannelAuthorizationCode : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public ValidateFrontchannelAuthorizationCode(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateFrontchannelAccessToken.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.FrontchannelAuthorizationCodePrincipal is not null || + string.IsNullOrEmpty(context.FrontchannelAuthorizationCode)) + { + return; + } + + var notification = new ValidateTokenContext(context.Transaction) + { + Token = context.FrontchannelAuthorizationCode, + ValidTokenTypes = { TokenTypeHints.AuthorizationCode } + }; + + await _dispatcher.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.FrontchannelAuthorizationCodePrincipal = notification.Principal; + } + } + + /// + /// Contains the logic responsible of determining the set of backchannel tokens to validate. + /// + public class EvaluateValidatedBackchannelTokens : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateFrontchannelAuthorizationCode.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Resolve the grant grant and the response type stored in the state token and extract its individual elements. + var types = ( + GrantType: context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.GrantType), + ResponseTypes: context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.ResponseType) + !.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries) + .ToImmutableHashSet()); + + (context.ExtractBackchannelAccessToken, + context.RequireBackchannelAccessToken, + context.ValidateBackchannelAccessToken) = types switch + { + // An access token is always returned as part of token responses, independently of + // the negotiated response types or whether the server supports OpenID Connect or not. + // As such, a backchannel access token is always considered required if a code was received. + // + // Note: since access tokens are supposed to be opaque to the clients, they are never + // validated by default. Clients that need to deal with non-standard implementations + // can use custom handlers to validate access tokens that use a readable format (e.g JWT). + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet set) + when set.Contains(ResponseTypes.Code) => (true, true, false), + + _ => (false, false, false) + }; + + (context.ExtractBackchannelIdentityToken, + context.RequireBackchannelIdentityToken, + context.ValidateBackchannelIdentityToken) = types switch + { + // An identity token is always returned as part of token responses for the code and + // hybrid flows when the authorization server supports OpenID Connect. As such, + // a backchannel identity token is only considered required if the negotiated scopes + // include "openid", which indicates the initial request was an OpenID Connect request. + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet set) + when set.Contains(ResponseTypes.Code) && + context.FrontchannelStateTokenPrincipal.HasScope(Scopes.OpenId) => (true, true, true), + + _ => (false, false, false) + }; + + (context.ExtractBackchannelRefreshToken, + context.RequireBackchannelRefreshToken, + context.ValidateBackchannelRefreshToken) = types switch + { + // A refresh token may be returned as part of token responses, depending on the + // policy enforced by the remote authorization server (e.g the "offline_access" + // scope may be used). Since the requirements will differ between authorization + // servers, a refresh token is never considered required by default. + // + // Note: since refresh tokens are supposed to be opaque to the clients, they are never + // validated by default. Clients that need to deal with non-standard implementations + // can use custom handlers to validate access tokens that use a readable format (e.g JWT). + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet set) + when set.Contains(ResponseTypes.Code) => (true, false, false), + + _ => (false, false, false) + }; + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the parameters to the backchannel token request, if applicable. + /// + public class AttachBackchannelRequestParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(EvaluateValidatedBackchannelTokens.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Attach a new request instance if none was created already. + context.TokenRequest ??= new OpenIddictRequest(); + + // Attach the grant type selected during the challenge phase. + context.TokenRequest.GrantType = context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.GrantType) switch + { + null => throw new InvalidOperationException(SR.GetResourceString(SR.ID0294)), + + GrantTypes.AuthorizationCode => GrantTypes.AuthorizationCode, + + // Note: in OpenID Connect, the hybrid flow doesn't have a dedicated grant_type and is + // typically treated as a combination of both the implicit and authorization code grants. + // If the implicit flow was selected during the challenge phase and an authorization code + // was returned, this very likely means that the hybrid flow was used. In this case, + // use grant_type=authorization_code when communicating with the remote token endpoint. + GrantTypes.Implicit => GrantTypes.AuthorizationCode, + + // If the grant_type is not natively supported or recognized, try to send it as-is. + string type => type + }; + + // Attach the client credentials to the request. Note: the client_secret may be null at this point + // (e.g for a public client or if a custom authentication method is used by the application). + context.TokenRequest.ClientId = context.Registration.ClientId; + context.TokenRequest.ClientSecret = context.Registration.ClientSecret; + + // If the token request uses an authorization code grant, retrieve the code_verifier and + // the redirect_uri from the state token principal and attach them to the request, if available. + if (context.TokenRequest.GrantType is GrantTypes.AuthorizationCode) + { + context.TokenRequest.Code = context.FrontchannelAuthorizationCode; + context.TokenRequest.CodeVerifier = context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.CodeVerifier); + context.TokenRequest.RedirectUri = context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.RedirectUri); + } + + return default; + } + } + + /// + /// Contains the logic responsible of sending the token request, if applicable. + /// + public class SendBackchannelRequest : IOpenIddictClientHandler + { + private readonly OpenIddictClientService _service; + + public SendBackchannelRequest(OpenIddictClientService service) + => _service = service; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachBackchannelRequestParameters.Descriptor.Order + 1_000) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.TokenRequest is not null, SR.GetResourceString(SR.ID4008)); + + context.TokenResponse = await _service.SendTokenRequestAsync(context.Registration, context.TokenRequest); + } + } + + /// + /// Contains the logic responsible of rejecting errored token responses. + /// + public class ValidateBackchannelErrorParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(SendBackchannelRequest.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007)); + + if (!string.IsNullOrEmpty(context.TokenResponse.Error)) + { + context.Reject( + error: context.TokenResponse.Error, + description: context.TokenResponse.ErrorDescription, + uri: context.TokenResponse.ErrorUri); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of resolving the backchannel tokens by sending a token request, if applicable. + /// + public class ResolveValidatedBackchannelTokens : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateBackchannelErrorParameters.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007)); + + context.BackchannelAccessToken = context.ExtractBackchannelAccessToken switch + { + true => context.TokenResponse.AccessToken, + false => null + }; + + context.BackchannelIdentityToken = context.ExtractBackchannelIdentityToken switch + { + true => context.TokenResponse.IdToken, + false => null + }; + + context.BackchannelRefreshToken = context.ExtractBackchannelRefreshToken switch + { + true => context.TokenResponse.RefreshToken, + false => null + }; + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting authentication demands that lack required tokens. + /// + public class ValidateRequiredBackchannelTokens : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ResolveValidatedBackchannelTokens.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if ((context.RequireBackchannelAccessToken && string.IsNullOrEmpty(context.BackchannelAccessToken)) || + (context.RequireBackchannelIdentityToken && string.IsNullOrEmpty(context.BackchannelIdentityToken)) || + (context.RequireBackchannelRefreshToken && string.IsNullOrEmpty(context.BackchannelRefreshToken))) + { + context.Reject( + error: Errors.MissingToken, + description: SR.GetResourceString(SR.ID2000), + uri: SR.FormatID8000(SR.ID2000)); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of validating the backchannel identity token resolved from the context. + /// + public class ValidateBackchannelIdentityToken : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public ValidateBackchannelIdentityToken(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateRequiredBackchannelTokens.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.BackchannelIdentityTokenPrincipal is not null || + string.IsNullOrEmpty(context.BackchannelIdentityToken)) + { + return; + } + + var notification = new ValidateTokenContext(context.Transaction) + { + Token = context.BackchannelIdentityToken, + ValidTokenTypes = { TokenTypeHints.IdToken } + }; + + await _dispatcher.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.BackchannelIdentityTokenPrincipal = notification.Principal; + } + } + + /// + /// Contains the logic responsible of validating the well-known claims contained in the backchannel identity token. + /// + public class ValidateBackchannelIdentityTokenWellknownClaims : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateBackchannelIdentityToken.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.BackchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + foreach (var group in context.BackchannelIdentityTokenPrincipal.Claims + .GroupBy(claim => claim.Type) + .ToDictionary(group => group.Key, group => group.ToList())) + { + if (ValidateClaimGroup(group)) + { + continue; + } + + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2125(group.Key), + uri: SR.FormatID8000(SR.ID2125)); + + return default; + } + + // Identity tokens MUST contain an "iss" claim. For more information, + // see https://openid.net/specs/openid-connect-core-1_0.html#IDToken. + if (!context.BackchannelIdentityTokenPrincipal.HasClaim(Claims.Issuer)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2126(Claims.Issuer), + uri: SR.FormatID8000(SR.ID2126)); + + return default; + } + + // Identity tokens MUST contain a "sub" claim. For more information, + // see https://openid.net/specs/openid-connect-core-1_0.html#IDToken. + if (!context.BackchannelIdentityTokenPrincipal.HasClaim(Claims.Subject)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2126(Claims.Subject), + uri: SR.FormatID8000(SR.ID2126)); + + return default; + } + + // Identity tokens MUST contain at least one "aud" claim. For more information, + // see https://openid.net/specs/openid-connect-core-1_0.html#IDToken. + if (!context.BackchannelIdentityTokenPrincipal.HasClaim(Claims.Audience)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2126(Claims.Audience), + uri: SR.FormatID8000(SR.ID2126)); + + return default; + } + + // Identity tokens MUST contain contain a "exp" claim. For more information, + // see https://openid.net/specs/openid-connect-core-1_0.html#IDToken. + if (!context.BackchannelIdentityTokenPrincipal.HasClaim(Claims.ExpiresAt)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2126(Claims.ExpiresAt), + uri: SR.FormatID8000(SR.ID2126)); + + return default; + } + + // Identity tokens MUST contain contain an "iat" claim. For more information, + // see https://openid.net/specs/openid-connect-core-1_0.html#IDToken. + if (!context.BackchannelIdentityTokenPrincipal.HasClaim(Claims.IssuedAt)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2126(Claims.IssuedAt), + uri: SR.FormatID8000(SR.ID2126)); + + return default; + } + + return default; + + static bool ValidateClaimGroup(KeyValuePair> claims) => claims switch + { + // The following JWT claims MUST be represented as unique strings. + { + Key: Claims.AuthenticationContextReference or Claims.AuthorizedParty or Claims.Issuer or Claims.Nonce, + Value: List values + } => values.Count is 1 && values[0].ValueType is ClaimValueTypes.String, + + // The following JWT claims MUST be represented as unique strings or array of strings. + { + Key: Claims.Audience or Claims.AuthenticationMethodReference, + Value: List values + } => values.Count switch + { + 1 => values[0].ValueType is ClaimValueTypes.String, + _ => values.All(value => value.ValueType is ClaimValueTypes.String) + }, + + // The following JWT claims MUST be represented as unique numeric dates. + { + Key: Claims.AuthenticationTime or Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore, + Value: List values + } => values.Count is 1 && values[0].ValueType is ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or + ClaimValueTypes.Integer64 or ClaimValueTypes.Double or + ClaimValueTypes.UInteger32 or ClaimValueTypes.UInteger64, + + // Claims that are not in the well-known list can be of any type. + _ => true + }; + } + } + + /// + /// Contains the logic responsible of validating the audience returned in the backchannel identity token, if applicable. + /// + public class ValidateBackchannelIdentityTokenAudience : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateBackchannelIdentityTokenWellknownClaims.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.BackchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Note: while an identity token typically contains a single audience represented + // as a JSON string, multiple values can be returned represented a a JSON array. + // + // In any case, the client identifier of the application MUST be included in the audiences. + // See https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information. + var audiences = context.BackchannelIdentityTokenPrincipal.GetClaims(Claims.Audience); + if (!audiences.Contains(context.Registration.ClientId!)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2127), + uri: SR.FormatID8000(SR.ID2127)); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of validating the presenter returned in the backchannel identity token, if applicable. + /// + public class ValidateBackchannelIdentityTokenPresenter : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateBackchannelIdentityTokenAudience.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.BackchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Note: the "azp" claim is optional, but if it's present, it MUST match the client identifier of the application. + // See https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information. + var presenter = context.BackchannelIdentityTokenPrincipal.GetClaim(Claims.AuthorizedParty); + if (!string.IsNullOrEmpty(presenter) && + !string.Equals(presenter, context.Registration.ClientId, StringComparison.Ordinal)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2127), + uri: SR.FormatID8000(SR.ID2127)); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of validating the nonce returned in the backchannel identity token, if applicable. + /// + public class ValidateBackchannelIdentityTokenNonce : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateBackchannelIdentityTokenPresenter.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.BackchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + switch (( + BackchannelIdentityTokenNonce: context.BackchannelIdentityTokenPrincipal.GetClaim(Claims.Nonce), + StateTokenNonce: context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.Nonce))) + { + // If no nonce if no present in the state token (e.g because the authorization server doesn't + // support OpenID Connect and response_type=code was negotiated), bypass the validation logic. + case { StateTokenNonce: null or { Length: not > 0 } }: + return default; + + // If a nonce was found in the state token but is not present in the identity token, return an error. + case { BackchannelIdentityTokenNonce: null or { Length: not > 0 } }: + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2126(Claims.Nonce), + uri: SR.FormatID8000(SR.ID2126)); + + return default; + + // If the two nonces don't match, return an error. + case { BackchannelIdentityTokenNonce: { } left, StateTokenNonce: { } right } when +#if SUPPORTS_TIME_CONSTANT_COMPARISONS + !CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(left), Encoding.ASCII.GetBytes(right)): +#else + !Arrays.ConstantTimeAreEqual(Encoding.ASCII.GetBytes(left), Encoding.ASCII.GetBytes(right)): +#endif + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2128(Claims.Nonce), + uri: SR.FormatID8000(SR.ID2128)); + + return default; + + default: return default; + } + } + } + + /// + /// Contains the logic responsible of validating the digests of the backchannel access token. + /// + public class ValidateBackchannelTokenDigests : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateBackchannelIdentityTokenNonce.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.BackchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + Debug.Assert(!string.IsNullOrEmpty(context.BackchannelAccessToken), SR.GetResourceString(SR.ID4010)); + + // Resolve the signing algorithm used to sign the identity token. If the private + // claim cannot be found, it means the "alg" header of the identity token was + // malformed but the token was still considered valid. While highly unlikly, + // an exception is thrown in this case to abort the authentication demand. + var name = context.BackchannelIdentityTokenPrincipal.GetClaim(Claims.Private.SigningAlgorithm); + if (string.IsNullOrEmpty(name)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0295)); + } + + // Resolve the hash algorithm corresponding to the signing algorithm. If an + // instance of the BCL hash algorithm cannot be resolved, throw an exception. + var algorithm = GetHashAlgorithm(name); + if (algorithm is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0295)); + } + + var hash = context.BackchannelIdentityTokenPrincipal.GetClaim(Claims.AccessTokenHash); + if (string.IsNullOrEmpty(hash)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2126(Claims.AccessTokenHash), + uri: SR.FormatID8000(SR.ID2126)); + + return default; + } + + if (!ValidateTokenHash(algorithm, context.BackchannelAccessToken, hash)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2128(Claims.AccessTokenHash), + uri: SR.FormatID8000(SR.ID2128)); + + return default; + } + + // Note: unlike frontchannel identity tokens, backchannel identity tokens are not expected to include + // an authorization code hash as no authorization code is normally returned from the token endpoint. + + static byte[] ComputeTokenHash(HashAlgorithm algorithm, string token) + { + // Note: only the left-most half of the access token and authorization code digest is used. + // See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken for more information. + var digest = algorithm.ComputeHash(Encoding.ASCII.GetBytes(token)); + + return Encoding.ASCII.GetBytes(Base64UrlEncoder.Encode(digest, 0, digest.Length / 2)); + } + + static bool ValidateTokenHash(HashAlgorithm algorithm, string token, string hash) => +#if SUPPORTS_TIME_CONSTANT_COMPARISONS + CryptographicOperations.FixedTimeEquals( + left: Encoding.ASCII.GetBytes(hash), + right: ComputeTokenHash(algorithm, token)); +#else + Arrays.ConstantTimeAreEqual( + a: Encoding.ASCII.GetBytes(hash), + b: ComputeTokenHash(algorithm, token)); +#endif + + return default; + + static HashAlgorithm? GetHashAlgorithm(string algorithm) => algorithm switch + { + SecurityAlgorithms.EcdsaSha256 or SecurityAlgorithms.HmacSha256 or + SecurityAlgorithms.RsaSha256 or SecurityAlgorithms.RsaSsaPssSha256 + => CryptoConfig.CreateFromName(SecurityAlgorithms.Sha256) as HashAlgorithm, + + SecurityAlgorithms.EcdsaSha384 or SecurityAlgorithms.HmacSha384 or + SecurityAlgorithms.RsaSha384 or SecurityAlgorithms.RsaSsaPssSha384 + => CryptoConfig.CreateFromName(SecurityAlgorithms.Sha384) as HashAlgorithm, + + SecurityAlgorithms.EcdsaSha512 or SecurityAlgorithms.HmacSha384 or + SecurityAlgorithms.RsaSha512 or SecurityAlgorithms.RsaSsaPssSha512 + => CryptoConfig.CreateFromName(SecurityAlgorithms.Sha512) as HashAlgorithm, + + _ => null + }; + } + } + + /// + /// Contains the logic responsible of validating the backchannel access token resolved from the context. + /// Note: this handler is typically not used for standard-compliant implementations as access tokens + /// are supposed to be opaque to clients. + /// + public class ValidateBackchannelAccessToken : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public ValidateBackchannelAccessToken(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateBackchannelTokenDigests.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.BackchannelAccessTokenPrincipal is not null || + string.IsNullOrEmpty(context.BackchannelAccessToken)) + { + return; + } + + var notification = new ValidateTokenContext(context.Transaction) + { + Token = context.BackchannelAccessToken, + ValidTokenTypes = { TokenTypeHints.AccessToken } + }; + + await _dispatcher.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.BackchannelAccessTokenPrincipal = notification.Principal; + } + } + + /// + /// Contains the logic responsible of validating the backchannel refresh token resolved from the context. + /// Note: this handler is typically not used for standard-compliant implementations as refresh tokens + /// are supposed to be opaque to clients. + /// + public class ValidateBackchannelRefreshToken : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public ValidateBackchannelRefreshToken(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateBackchannelAccessToken.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.BackchannelRefreshTokenPrincipal is not null || + string.IsNullOrEmpty(context.BackchannelRefreshToken)) + { + return; + } + + var notification = new ValidateTokenContext(context.Transaction) + { + Token = context.BackchannelRefreshToken, + ValidTokenTypes = { TokenTypeHints.RefreshToken } + }; + + await _dispatcher.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.BackchannelRefreshTokenPrincipal = notification.Principal; + } + } + + /// + /// Contains the logic responsible of rejecting invalid challenge demands. + /// + public class ValidateChallengeDemand : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If an explicit grant type was specified, ensure it is supported by OpenIddict. + if (!string.IsNullOrEmpty(context.GrantType) && + context.GrantType is not (GrantTypes.AuthorizationCode or GrantTypes.Implicit)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0296)); + } + + // If no issuer was explicitly attached and a single client is registered, use it. + // Otherwise, throw an exception to indicate that setting an explicit issuer + // is required when multiple clients are registered. + context.Issuer ??= context.Options.Registrations.Count switch + { + 0 => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)), + 1 => context.Options.Registrations[0].Issuer, + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0305)) + }; + + return default; + } + } + + /// + /// Contains the logic responsible of resolving the client registration applicable to the challenge demand. + /// + public class ResolveClientRegistration : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateChallengeDemand.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: if the static registration cannot be found in the options, this may indicate + // the client was removed after the authorization dance started and thus, can no longer + // be used to authenticate users. In this case, throw an exception to abort the flow. + context.Registration = context.Options.Registrations.Find( + registration => registration.Issuer == context.Issuer) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0292)); + + return default; + } + } + + /// + /// Contains the logic responsible of resolving the best grant type + /// supported by both the client and the authorization server. + /// + public class AttachGrantType : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ResolveClientRegistration.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If an explicit grant type was specified, don't overwrite it. + if (!string.IsNullOrEmpty(context.GrantType)) + { + return; + } + + // Note: if no grant type was explicitly returned as part of the server configuration, + // the identity provider is assumed to at least support both the authorization code + // and the implicit grants, as defined by the discovery specifications. In this case, + // the authorization code grant is generally preferred as it offers the broadest + // support and the best level of security thanks to additional features like + // client authentication, code binding and access token injections mitigations. + // See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata + // and https://datatracker.ietf.org/doc/html/rfc8414#section-2 for more information. + + var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + // Ensure the issuer resolved from the configuration matches the expected value. + if (configuration.Issuer != context.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + } + + context.GrantType = (context.Registration.GrantTypes, configuration.GrantTypesSupported) switch + { + // If neither the client nor the server specify a list of grant types, + // use the authorization code grant, as it's always supported by default. + ({ Count: 0 }, { Count: 0 }) => GrantTypes.AuthorizationCode, + + // If the client supports the code grant and the server doesn't specify a list of + // grant types, use the authorization code grant, as it's always supported by default. + ({ Count: > 0 } client, { Count: 0 }) when client.Contains(GrantTypes.AuthorizationCode) + => GrantTypes.AuthorizationCode, + + // If the client supports the code grant and the server doesn't specify a list of + // grant types, use the authorization code grant, as it's always supported by default. + ({ Count: 0 }, { Count: > 0 } server) when server.Contains(GrantTypes.AuthorizationCode) + => GrantTypes.AuthorizationCode, + + // If both the client and the server support the code grant, prefer it over the implicit grant. + ({ Count: > 0 } client, { Count: > 0 } server) when + server.Contains(GrantTypes.AuthorizationCode) && client.Contains(GrantTypes.AuthorizationCode) + => GrantTypes.AuthorizationCode, + + // If the client supports the implicit grant and the server doesn't specify a list + // of grant types, use the implicit code grant, as it's always supported by default. + ({ Count: > 0 } client, { Count: 0 }) when client.Contains(GrantTypes.Implicit) + => GrantTypes.Implicit, + + // If the client supports the implicit grant and the server doesn't specify a list + // of grant types, use the implicit code grant, as it's always supported by default. + ({ Count: 0 }, { Count: > 0 } server) when server.Contains(GrantTypes.Implicit) + => GrantTypes.Implicit, + + // If both the client and the server support the implicit grant, use it as a last chance option. + ({ Count: > 0 } client, { Count: > 0 } server) when + server.Contains(GrantTypes.Implicit) && client.Contains(GrantTypes.Implicit) + => GrantTypes.Implicit, + + // If no common grant type can be negotiated, abort the challenge operation. + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0297)) + }; + } + } + + /// + /// Contains the logic responsible of selecting the token types that + /// should be generated and optionally returned in the response. + /// + public class EvaluateGeneratedTokens : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachGrantType.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // In OpenIddict, per-authorization demand values are stored in an encrypted and signed token + // called "state token", that allows flowing per-authorization demand data like the issuer + // targeted by the authorization demand or secret values like the code verifier used to + // derive the code challenge sent to the remote authorization server. While not strictly + // required by the OAuth 2.0/2.1 and OpenID Connect specifications, the state parameter is + // considered essential in OpenIddict and as such, is always included in challenge demands + // that use the authorization code or implicit grants (which includes the hybrid flow). + // + // See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09 + // for more information. + (context.GenerateStateToken, context.IncludeStateToken) = context.GrantType switch + { + GrantTypes.AuthorizationCode or GrantTypes.Implicit => (true, true), + + _ => (false, false) + }; + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the response type to the challenge request. + /// + public class AttachResponseType : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 1_000) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If an explicit response type was specified, don't overwrite it. + if (!string.IsNullOrEmpty(context.ResponseType)) + { + return; + } + + var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + // Ensure the issuer resolved from the configuration matches the expected value. + if (configuration.Issuer != context.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + } + + context.ResponseType = ( + context.GrantType, + context.Registration.ResponseTypes.Select(types => + types.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries) + .ToImmutableHashSet(StringComparer.Ordinal)).ToList(), + configuration.ResponseTypesSupported.Select(types => + types.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries) + .ToImmutableHashSet(StringComparer.Ordinal)).ToList()) switch + { + // Note: the OAuth 2.0 provider metadata and OpenID Connect discovery specification define + // the supported response types as a required property. Nevertheless, to ensure OpenIddict + // is compatible with most identity providers, a missing or empty list is not treated as an + // error. In this case, response_type=code (for the code grant) and response_type=id_token + // (for the implicit grant) are assumed to be the most commonly supported values. + // + // Note: response_type=code is always tested first as it doesn't require using + // response_mode=form_post or response_mode=fragment: fragment doesn't natively work with + // server-side clients and form_post is impacted by the same-site cookies restrictions + // that are now enforced by most browser vendors, which requires using SameSite=None for + // response_mode=form_post to work correctly. While it doesn't have native protection + // against mix-up attacks (due to the missing id_token in the authorization response), + // the code flow remains the best compromise and thus always comes first in the list. + // + // Note: response types combinations containing "token" are always tested last as some + // authorization servers - like OpenIddict - are known to block authorization requests + // asking for an access token if Proof Key for Code Exchange is used in the same request. + // Returning an identity token directly from the authorization endpoint also has privacy + // concerns that code-based flows - that require a backchannel request - typically don't + // have when the client application (confidential or public) is executed on a server. + + // If neither the client nor the server specify a list of response types, + // use "response_type=code", as it's the most commonly supported type. + (GrantTypes.AuthorizationCode, { Count: 0 }, { Count: 0 }) + => ResponseTypes.Code, + + // If the client supports "response_type=code" and the server doesn't + // specify a list of response types, use "response_type=code". + (GrantTypes.AuthorizationCode, { Count: > 0 } client, { Count: 0 }) when + client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code })) + => ResponseTypes.Code, + + // If the server supports "response_type=code" and the client doesn't + // specify a list of response types, use "response_type=code". + (GrantTypes.AuthorizationCode, { Count: 0 }, { Count: > 0 } server) when + server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code })) + => ResponseTypes.Code, + + // If both the client and the server support "response_type=code", use it. + (GrantTypes.AuthorizationCode, { Count: > 0 } client, { Count: > 0 } server) when + server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code })) && + client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code })) + => ResponseTypes.Code, + + // If the client supports "response_type=code id_token" and the server doesn't + // specify a list of response types, use "response_type=code id_token". + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: 0 }) when + client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken })) + => ResponseTypes.Code + ' ' + ResponseTypes.IdToken, + + // If the server supports "response_type=code id_token" and the client doesn't + // specify a list of response types, use "response_type=code id_token". + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: 0 }, { Count: > 0 } server) when + server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken })) + => ResponseTypes.Code + ' ' + ResponseTypes.IdToken, + + // If both the client and the server support "response_type=code id_token", use it. + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when + server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken })) && + client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken })) + => ResponseTypes.Code + ' ' + ResponseTypes.IdToken, + + // If neither the client nor the server specify a list of response types, use "response_type=id_token". + (GrantTypes.Implicit, { Count: 0 }, { Count: 0 }) + => ResponseTypes.IdToken, + + // If the client supports "response_type=id_token" and the server doesn't + // specify a list of response types, use "response_type=id_token". + (GrantTypes.Implicit, { Count: > 0 } client, { Count: 0 }) when + client.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken })) + => ResponseTypes.IdToken, + + // If the server supports "response_type=id_token" and the client doesn't + // specify a list of response types, use "response_type=id_token". + (GrantTypes.Implicit, { Count: 0 }, { Count: > 0 } server) when + server.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken })) + => ResponseTypes.IdToken, + + // If both the client and the server support "response_type=id_token", use it. + (GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when + server.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken })) && + client.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken })) + => ResponseTypes.IdToken, + + // If the client supports "response_type=code id_token token" and the server doesn't + // specify a list of response types, use "response_type=code id_token token". + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: 0 }) when + client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken, ResponseTypes.Token })) + => ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token, + + // If the server supports "response_type=code id_token token" and the client doesn't + // specify a list of response types, use "response_type=code id_token token". + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: 0 }, { Count: > 0 } server) when + server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken, ResponseTypes.Token })) + => ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token, + + // If both the client and the server support "response_type=code id_token token", use it. + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when + server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken, ResponseTypes.Token })) && + client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken, ResponseTypes.Token })) + => ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token, + + // If the client supports "response_type=code token" and the server doesn't + // specify a list of response types, use "response_type=code token". + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: 0 }) when + client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.Token })) + => ResponseTypes.Code + ' ' + ResponseTypes.Token, + + // If the server supports "response_type=code token" and the client doesn't + // specify a list of response types, use "response_type=code token". + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: 0 }, { Count: > 0 } server) when + server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.Token })) + => ResponseTypes.Code + ' ' + ResponseTypes.Token, + + // If both the client and the server support "response_type=code token", use it. + (GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when + server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.Token })) && + client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.Token })) + => ResponseTypes.Code + ' ' + ResponseTypes.Token, + + // If the client supports "response_type=id_token token" and the server doesn't + // specify a list of response types, use "response_type=id_token token". + (GrantTypes.Implicit, { Count: > 0 } client, { Count: 0 }) when + client.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken, ResponseTypes.Token })) + => ResponseTypes.IdToken + ' ' + ResponseTypes.Token, + + // If the server supports "response_type=id_token token" and the client doesn't + // specify a list of response types, use "response_type=id_token token". + (GrantTypes.Implicit, { Count: 0 }, { Count: > 0 } server) when + server.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken, ResponseTypes.Token })) + => ResponseTypes.IdToken + ' ' + ResponseTypes.Token, + + // If both the client and the server support "response_type=id_token token", use it. + (GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when + server.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken, ResponseTypes.Token })) && + client.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken, ResponseTypes.Token })) + => ResponseTypes.IdToken + ' ' + ResponseTypes.Token, + + // Note: response_type=token is not considered considered secure enough as it allows + // malicious actors to inject access tokens that were issued to a different client. + // As such, while OpenIddict-based servers allow using response_type=token for backward + // compatibility with legacy clients, OpenIddict-based clients are deliberately not + // allowed to negotiate the unsafe and OAuth 2.0-only response_type=token flow. + // + // For more information, see https://datatracker.ietf.org/doc/html/rfc6749#section-10.16 and + // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-2.1.2. + + // If no common response type can be negotiated, abort the challenge operation. + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0298)) + }; + } + } + + /// + /// Contains the logic responsible of attaching the response mode to the challenge request. + /// + public class AttachResponseMode : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachResponseType.Descriptor.Order + 1_000) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If an explicit response type was specified, don't overwrite it. + if (!string.IsNullOrEmpty(context.ResponseMode)) + { + return; + } + + // Note: in most cases, the query response mode will be used as it offers the + // best compatibility and, unlike the form_post response mode, is compatible + // with SameSite=Lax cookies (as it uses GET requests for the callback stage). + // + // However, browser-based hosts like Blazor may typically want to use the fragment + // response mode as it offers a better protection for SPA applications. + // Unfortunately, server-side clients like ASP.NET Core applications cannot + // natively use response_mode=fragment as URL fragments are never sent to servers. + // + // As such, this handler will not choose response_mode=fragment by default and it is + // expected that specialized hosts like Blazor implement custom event handlers to + // opt for fragment by default, if it is supported by the authorization server. + + var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + // Ensure the issuer resolved from the configuration matches the expected value. + if (configuration.Issuer != context.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + } + + // Some specific response_type/response_mode combinations are not allowed (e.g response_mode=query + // can never be used with a response type containing id_token or token, as required by the OAuth 2.0 + // multiple response types specification. To prevent invalid combinations from being sent to the + // remote server, the response types are taken into account when selecting the best response mode. + var types = context.ResponseType!.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries) + .ToImmutableHashSet(StringComparer.Ordinal); + + context.ResponseMode = (context.Registration.ResponseModes, configuration.ResponseModesSupported) switch + { + // If neither the client nor the server specify a list of response modes, + // use "response_mode=form_post" if the response types contain a value + // that prevents response_mode=query from being used (token/id_token). + ({ Count: 0 }, { Count: 0 }) when + types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token) + => ResponseModes.FormPost, + + // If the client support response_mode=form_post and the server doesn't + // specify a list of response modes, use it if the response types contain + // a value that prevents response_mode=query from being used (token/id_token). + ({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.FormPost) && + (types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token)) + => ResponseModes.FormPost, + + // If the server support response_mode=form_post and the server doesn't + // specify a list of response modes, use it if the response types contain + // a value that prevents response_mode=query from being used (token/id_token). + ({ Count: 0 }, { Count: > 0 } server) when server.Contains(ResponseModes.FormPost) && + (types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token)) + => ResponseModes.FormPost, + + // If both the client and the server support response_mode=form_post, use it if the response + // types contain a value that prevents response_mode=query from being used (token/id_token). + ({ Count: > 0 } client, { Count: > 0 } server) when + client.Contains(ResponseModes.FormPost) && server.Contains(ResponseModes.FormPost) && + (types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token)) + => ResponseModes.FormPost, + + // If neither the client nor the server specify a list of response modes, + // use "response_mode=query" as a fallback as it's universally supported: + ({ Count: 0 }, { Count: 0 }) => ResponseModes.Query, + + // If the client support response_mode=query and the server + // doesn't specify a list of response modes, use it: + ({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.Query) + => ResponseModes.Query, + + // If the server support response_mode=query and the client + // doesn't specify a list of response modes, use it: + ({ Count: 0 }, { Count: > 0 } server) when server.Contains(ResponseModes.Query) + => ResponseModes.Query, + + // If both the client and the server support response_mode=query, use it: + ({ Count: > 0 } client, { Count: > 0 } server) when + client.Contains(ResponseModes.Query) && server.Contains(ResponseModes.Query) + => ResponseModes.Query, + + // If no common response mode can be negotiated, abort the challenge operation. + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0299)) + }; + } + } + + /// + /// Contains the logic responsible of attaching the client identifier to the challenge request. + /// + public class AttachClientId : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachResponseMode.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.ClientId ??= context.Registration.ClientId; + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the redirect_uri to the challenge request. + /// + public class AttachRedirectUri : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachClientId.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Unlike OpenID Connect, OAuth 2.0 and 2.1 don't require specifying a redirect_uri. + // To keep OpenIddict compatible with OAuth 2.0/2.1 deployments, the redirect_uri + // is not required for OAuth 2.0 requests but an exception will be thrown later + // if the request that is being prepared is an OpenID Connect request. + context.RedirectUri ??= context.Registration.RedirectUri?.AbsoluteUri; + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the scopes to the challenge request. + /// + public class AttachScopes : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachRedirectUri.Descriptor.Order + 1_000) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If an explicit set of scopes was specified, don't overwrite it. + if (context.Scopes.Count > 0) + { + return; + } + + var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + // Ensure the issuer resolved from the configuration matches the expected value. + if (configuration.Issuer != context.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + } + + // If the server configuration indicates the identity provider supports OpenID Connect, + // always request the "openid" scope to identify the request as an OpenID Connect request. + // + // Developers who prefer sending OAuth 2.0/2.1 requests to an OpenID Connect server can + // implement a custom event handler that manually replaces the set of requested scopes. + if (configuration.ScopesSupported.Contains(Scopes.OpenId)) + { + context.Scopes.Add(Scopes.OpenId); + } + + context.Scopes.UnionWith(context.Registration.Scopes); + } + } + + /// + /// Contains the logic responsible of attaching a request forgery protection to the authorization request. + /// + public class AttachRequestForgeryProtection : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachScopes.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Generate a new crypto-secure random identifier that will + // be used as the non-guessable part of the state token. + var data = new byte[256 / 8]; +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + context.RequestForgeryProtection = Base64UrlEncoder.Encode(data); + + return default; + } + } + + /// + /// Contains the logic responsible of attaching a nonce to the authorization request. + /// + public class AttachNonce : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachRequestForgeryProtection.Descriptor.Order + 1_000) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + // Ensure the issuer resolved from the configuration matches the expected value. + if (configuration.Issuer != context.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + } + + // If the identity provider doesn't support OpenID Connect, don't attach a nonce. + if (!configuration.ScopesSupported.Contains(Scopes.OpenId)) + { + return; + } + + // Generate a new crypto-secure random identifier that will be used as the nonce. + var data = new byte[256 / 8]; +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + context.Nonce = Base64UrlEncoder.Encode(data); + } + } + + /// + /// Contains the logic responsible of attaching the code challenge parameters to the authorization request. + /// + public class AttachCodeChallengeParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachNonce.Descriptor.Order + 1_000) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Some specific response_type/response_mode combinations are not allowed (e.g response_mode=query + // can never be used with a response type containing id_token or token, as required by the OAuth 2.0 + // multiple response types specification. To prevent invalid combinations from being sent to the + // remote server, the response types are taken into account when selecting the best response mode. + var types = context.ResponseType!.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries) + .ToImmutableHashSet(StringComparer.Ordinal); + + // Don't attach a code challenge method if no authorization code is requested as some implementations + // (like OpenIddict server) are known to eagerly block authorization requests that specify an invalid + // code_challenge/code_challenge_method/response_type combination (e.g response_type=id_token). + if (!types.Contains(ResponseTypes.Code)) + { + return; + } + + var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + // Ensure the issuer resolved from the configuration matches the expected value. + if (configuration.Issuer != context.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + } + + context.CodeChallengeMethod ??= ( + context.Registration.CodeChallengeMethods, + configuration.CodeChallengeMethodsSupported) switch + { + // If neither the client nor the server specify a list of code challenge methods, don't use PKCE. + ({ Count: 0 }, { Count: 0 }) => null, + + // If the server doesn't specify a list of code challenge methods, don't use PKCE. + ({ Count: > 0 }, { Count: 0 }) => null, + + // If the client doesn't specify a list of code challenge methods but the server support S256, use it. + ({ Count: 0 }, { Count: > 0 } server) when server.Contains(CodeChallengeMethods.Sha256) + => CodeChallengeMethods.Sha256, + + // If both the client and the server support S256, use it. + ({ Count: > 0 } client, { Count: > 0 } server) when + client.Contains(CodeChallengeMethods.Sha256) && server.Contains(CodeChallengeMethods.Sha256) + => CodeChallengeMethods.Sha256, + + // If the client doesn't specify a list of code challenge methods but the server support plain, use it. + ({ Count: 0 }, { Count: > 0 } server) when server.Contains(CodeChallengeMethods.Plain) + => CodeChallengeMethods.Plain, + + // If both the client and the server support plain, use it. + ({ Count: > 0 } client, { Count: > 0 } server) when + client.Contains(CodeChallengeMethods.Plain) && server.Contains(CodeChallengeMethods.Plain) + => CodeChallengeMethods.Plain, + + _ => null + }; + + // Note: while enforced by OAuth 2.1 under certain circumstances, PKCE is not a required feature for + // OAuth 2.0 and OpenID Connect (where features like nonce validation can serve similar purposes). + // As such, no error is returned at this stage is no common code challenge method could be inferred. + if (string.IsNullOrEmpty(context.CodeChallengeMethod)) + { + return; + } + + // Generate a new crypto-secure random identifier that will be used as the code challenge. + var data = new byte[256 / 8]; +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + context.CodeVerifier = Base64UrlEncoder.Encode(data); + + if (context.CodeChallengeMethod is CodeChallengeMethods.Plain) + { + // Use the code verifier as the code challenge. + context.CodeChallenge = context.CodeVerifier; + } + + else if (context.CodeChallengeMethod is CodeChallengeMethods.Sha256) + { + // Compute of the SHA-256 hash of the code verifier and use it as the code challenge. + using var algorithm = SHA256.Create(); + context.CodeChallenge = Base64UrlEncoder.Encode(algorithm.ComputeHash( + Encoding.ASCII.GetBytes(context.CodeVerifier))); + } + } + } + + /// + /// Contains the logic responsible of preparing and attaching the claims principal + /// used to generate the state token, if one is going to be returned. + /// + public class PrepareStateTokenPrincipal : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachCodeChallengeParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Create a new principal containing only the filtered claims. + // Actors identities are also filtered (delegation scenarios). + var principal = context.Principal.Clone(claim => + { + // Never include the public or internal token identifiers to ensure the identifiers + // that are automatically inherited from the parent token are not reused for the new token. + if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Never include the creation and expiration dates that are automatically + // inherited from the parent token are not reused for the new token. + if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Other claims are always included in the state token, even private claims. + return true; + }); + + principal.SetCreationDate(DateTimeOffset.UtcNow); + + var lifetime = context.Principal.GetStateTokenLifetime() ?? context.Options.StateTokenLifetime; + if (lifetime.HasValue) + { + principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); + } + + // Store the identity of the authorization server in the state token principal to allow + // resolving it when handling the authorization callback. Note: additional security checks + // are generally required to ensure the state token was not replaced by a state token + // meant to be used with a different authorization server (e.g using the "iss" parameter). + // + // See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09 + // for more information about this special claim. + principal.SetClaim(Claims.AuthorizationServer, context.Issuer.AbsoluteUri); + + // Store the request forgery protection in the state token so it can be later used to + // ensure the authorization response sent to the redirection endpoint is not forged. + principal.SetClaim(Claims.RequestForgeryProtection, context.RequestForgeryProtection); + + // Store the optional return URL in the state token. + principal.SetClaim(Claims.TargetLinkUri, context.TargetLinkUri); + + // Attach the negotiated grant type to the state token. + principal.SetClaim(Claims.Private.GrantType, context.GrantType); + + // Attach the response type to the state token to allow the redirection endpoint + // to ensure the returned set of tokens matches the specified response type and + // help mitigate downgrade attacks (e.g authorization code flow -> implicit flow). + principal.SetClaim(Claims.Private.ResponseType, context.ResponseType); + + // Store the optional redirect_uri to allow sending it as part of the token request. + principal.SetClaim(Claims.Private.RedirectUri, context.RedirectUri); + + // Store the code verifier in the state token so it can be sent to + // the remote authorization server when preparing the token request. + // + // Note: the code challenge and challenge methods are not persisted as they are + // not needed to send a valid token request (that only requires the code verifier). + principal.SetClaim(Claims.Private.CodeVerifier, context.CodeVerifier); + + // Store the nonce in the state token so it can be later used to check whether + // the nonce extracted from the identity token matches the generated value. + principal.SetClaim(Claims.Private.Nonce, context.Nonce); + + // Store the requested scopes in the state token. + principal.SetClaims(Claims.Private.Scope, context.Scopes.ToImmutableArray()); + + context.StateTokenPrincipal = principal; + + return default; + } + } + + /// + /// Contains the logic responsible of generating a state token for the current challenge operation. + /// + public class GenerateStateToken : IOpenIddictClientHandler + { + private readonly IOpenIddictClientDispatcher _dispatcher; + + public GenerateStateToken(IOpenIddictClientDispatcher dispatcher) + => _dispatcher = dispatcher; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var notification = new GenerateTokenContext(context.Transaction) + { + Principal = context.StateTokenPrincipal!, + TokenType = TokenTypeHints.StateToken + }; + + await _dispatcher.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.StateToken = notification.Token; + } + } + + /// + /// Contains the logic responsible of ensuring the redirect_uri parameter is present + /// if the "openid" scope is requested (indicating the request is an OpenID Connect request). + /// + public class ValidateRedirectUriParameter : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(GenerateStateToken.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // While OAuth 2.0/2.1 allows sending an authorization request without a redirect_uri, + // doing so is illegal in OpenID Connect and such requests will always be rejected. + // To make that requirement explicit, an exception is proactively thrown here. + if (string.IsNullOrEmpty(context.RedirectUri) && context.Scopes.Contains(Scopes.OpenId)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0300)); + } + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the appropriate parameters to the challenge response. + /// + public class AttachChallengeParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateRedirectUriParameter.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: while the exact order of the parameters has typically no effect on how requests + // are handled by an authorization server, client_id and redirect_uri are deliberately + // set first so that they appear early in the URL (when GET requests are used), making + // mistyped values easier to spot when an error is returned by the identity provider. + context.Request.ClientId = context.ClientId; + context.Request.RedirectUri = context.RedirectUri; + context.Request.ResponseType = context.ResponseType; + context.Request.ResponseMode = context.ResponseMode; + + if (context.Scopes.Count > 0) + { + // Note: the final OAuth 2.0 specification requires using a space as the scope separator. + // Clients that need to deal with older or non-compliant implementations can register + // a custom handler to use a different separator (typically, a comma). + context.Request.Scope = string.Join(" ", context.Scopes); + } + + context.Request.Nonce = context.Nonce; + context.Request.CodeChallenge = context.CodeChallenge; + context.Request.CodeChallengeMethod = context.CodeChallengeMethod; + + if (context.IncludeStateToken) + { + context.Request.State = context.StateToken; + } + + if (context.Parameters.Count > 0) + { + foreach (var parameter in context.Parameters) + { + context.Request.SetParameter(parameter.Key, parameter.Value); + } + } + + return default; + } + } + + /// + /// Contains the logic responsible of extracting potential errors from the response. + /// + public class HandleErrorResponse : IOpenIddictClientHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler>() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!string.IsNullOrEmpty(context.Transaction.Response?.Error)) + { + context.Reject( + error: context.Transaction.Response.Error, + description: context.Transaction.Response.ErrorDescription, + uri: context.Transaction.Response.ErrorUri); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the appropriate parameters to the error response. + /// + public class AttachErrorParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessErrorContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.Response.Error = context.Error; + context.Response.ErrorDescription = context.ErrorDescription; + context.Response.ErrorUri = context.ErrorUri; + + if (context.Parameters.Count > 0) + { + foreach (var parameter in context.Parameters) + { + context.Response.SetParameter(parameter.Key, parameter.Value); + } + } + + return default; + } + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientHelpers.cs b/src/OpenIddict.Client/OpenIddictClientHelpers.cs new file mode 100644 index 00000000..28fa91ee --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientHelpers.cs @@ -0,0 +1,76 @@ +/* + * 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.Client; + +/// +/// Exposes extensions simplifying the integration with the OpenIddict client services. +/// +public static class OpenIddictClientHelpers +{ + /// + /// Retrieves a property value from the client transaction using the specified name. + /// + /// The type of the property. + /// The client transaction. + /// The property name. + /// The property value or null if it couldn't be found. + public static TProperty? GetProperty( + this OpenIddictClientTransaction transaction, string name) where TProperty : class + { + if (transaction is null) + { + throw new ArgumentNullException(nameof(transaction)); + } + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0106), nameof(name)); + } + + if (transaction.Properties.TryGetValue(name, out var property) && property is TProperty result) + { + return result; + } + + return null; + } + + /// + /// Sets a property in the client transaction using the specified name and value. + /// + /// The type of the property. + /// The client transaction. + /// The property name. + /// The property value. + /// The client transaction, so that calls can be easily chained. + public static OpenIddictClientTransaction SetProperty( + this OpenIddictClientTransaction transaction, + string name, TProperty? value) where TProperty : class + { + if (transaction is null) + { + throw new ArgumentNullException(nameof(transaction)); + } + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0106), nameof(name)); + } + + if (value is null) + { + transaction.Properties.Remove(name); + } + + else + { + transaction.Properties[name] = value; + } + + return transaction; + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientOptions.cs b/src/OpenIddict.Client/OpenIddictClientOptions.cs new file mode 100644 index 00000000..8398abbd --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientOptions.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 Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; + +namespace OpenIddict.Client; + +/// +/// Provides various settings needed to configure the OpenIddict client handler. +/// +public class OpenIddictClientOptions +{ + /// + /// Gets the list of the handlers responsible of processing the OpenIddict client operations. + /// Note: the list is automatically sorted based on the order assigned to each handler descriptor. + /// As such, it MUST NOT be mutated after options initialization to preserve the exact order. + /// + public List Handlers { get; } = new(OpenIddictClientHandlers.DefaultHandlers); + + /// + /// Gets the list of encryption credentials used by the OpenIddict client services. + /// Multiple credentials can be added to support key rollover, but if X.509 keys + /// are used, at least one of them must have a valid creation/expiration date. + /// Note: the encryption credentials are not used to protect/unprotect tokens issued + /// by ASP.NET Core Data Protection, that uses its own key ring, configured separately. + /// + /// + /// Note: OpenIddict automatically sorts the credentials based on the following algorithm: + /// + /// Symmetric keys are always preferred when they can be used for the operation (e.g token encryption). + /// X.509 keys are always preferred to non-X.509 asymmetric keys. + /// X.509 keys with the furthest expiration date are preferred. + /// X.509 keys whose backing certificate is not yet valid are never preferred. + /// + /// + public List EncryptionCredentials { get; } = new(); + + /// + /// Gets the list of signing credentials used by the OpenIddict client services. + /// Multiple credentials can be added to support key rollover, but if X.509 keys + /// are used, at least one of them must have a valid creation/expiration date. + /// Note: the signing credentials are not used to protect/unprotect tokens issued + /// by ASP.NET Core Data Protection, that uses its own key ring, configured separately. + /// + /// + /// Note: OpenIddict automatically sorts the credentials based on the following algorithm: + /// + /// Symmetric keys are always preferred when they can be used for the operation (e.g token signing). + /// X.509 keys are always preferred to non-X.509 asymmetric keys. + /// X.509 keys with the furthest expiration date are preferred. + /// X.509 keys whose backing certificate is not yet valid are never preferred. + /// + /// + public List SigningCredentials { get; } = new(); + + /// + /// Gets or sets the period of time state tokens remain valid after being issued. The default value is 15 minutes. + /// While not recommended, this property can be set to to issue state tokens that never expire. + /// + public TimeSpan? StateTokenLifetime { get; set; } = TimeSpan.FromMinutes(15); + + /// + /// Gets or sets the security token handler used to protect and unprotect tokens. + /// + public JsonWebTokenHandler JsonWebTokenHandler { get; set; } = new JsonWebTokenHandler + { + SetDefaultTimesOnTokenCreation = false + }; + + /// + /// Gets the absolute and relative URIs associated to the redirection endpoint. + /// + public List RedirectionEndpointUris { get; } = new(); + + /// + /// Gets the static client registrations used by the OpenIddict client services. + /// + public List Registrations { get; } = new(); + + /// + /// Gets the token validation parameters used by the OpenIddict client services. + /// + /// + /// This instance is not used to validate tokens issued by remote authorization servers + /// and is only used with tokens produced and validated by the client itself (e.g state tokens). + /// + public TokenValidationParameters TokenValidationParameters { get; } = new() + { + AuthenticationType = TokenValidationParameters.DefaultAuthenticationType, + ClockSkew = TimeSpan.Zero, + NameClaimType = Claims.Name, + RoleClaimType = Claims.Role, + // Note: audience and lifetime are manually validated by OpenIddict itself. + ValidateAudience = false, + ValidateLifetime = false + }; +} diff --git a/src/OpenIddict.Client/OpenIddictClientRegistration.cs b/src/OpenIddict.Client/OpenIddictClientRegistration.cs new file mode 100644 index 00000000..4dfa7325 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientRegistration.cs @@ -0,0 +1,120 @@ +/* + * 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.IdentityModel.Protocols; +using Microsoft.IdentityModel.Tokens; + +namespace OpenIddict.Client; + +/// +/// Contains the properties used to configure a client/server link. +/// +public class OpenIddictClientRegistration +{ + /// + /// Gets or sets the client identifier assigned by the authorization server. + /// + public string? ClientId { get; set; } + + /// + /// Gets or sets the client secret assigned by the authorization server, if applicable. + /// + public string? ClientSecret { get; set; } + + /// + /// Gets or sets the address of the redirection endpoint that will handle the callback. + /// + public Uri? RedirectUri { get; set; } + + /// + /// Gets the code challenge methods allowed by the client instance. + /// If no value is explicitly set, the default code challenge methods are automatically used. + /// + /// + /// The final code challenge method used in authorization requests is chosen by OpenIddict + /// based on the server configuration and the values registered in this property. + /// + public HashSet CodeChallengeMethods { get; } = new(StringComparer.Ordinal); + + /// + /// Gets the grant types allowed by the client instance. + /// If no value is explicitly set, the default grant types are automatically used. + /// + /// + /// The final grant type used in authorization requests is chosen by OpenIddict + /// based on the server configuration and the values registered in this property. + /// + public HashSet GrantTypes { get; } = new(StringComparer.Ordinal); + + /// + /// Gets the response type combinations allowed by the client instance. + /// If no value is explicitly set, the default response types are automatically used. + /// + /// + /// The final response type used in authorization requests is chosen by OpenIddict + /// based on the server configuration and the values registered in this property. + /// + public HashSet ResponseTypes { get; } = new(StringComparer.Ordinal); + + /// + /// Gets the response modes allowed by the client instance. + /// If no value is explicitly set, the default response modes are automatically used. + /// + /// + /// The final response method used in authorization requests is chosen by OpenIddict + /// based on the server configuration and the values registered in this property. + /// + public HashSet ResponseModes { get; } = new(StringComparer.Ordinal); + + /// + /// Gets or sets the address of the authorization server. + /// + public Uri? Issuer { get; set; } + + /// + /// Gets or sets the static server configuration, if applicable. + /// + public OpenIddictConfiguration? Configuration { get; set; } + + /// + /// Gets or sets the configuration manager used to retrieve and cache the server configuration. + /// + public IConfigurationManager ConfigurationManager { get; set; } = default!; + + /// + /// Gets or sets the address of the authorization endpoint exposed by the server. + /// + public Uri? AuthorizationEndpoint { get; set; } + + /// + /// Gets or sets the address of the token endpoint exposed by the server. + /// + public Uri? TokenEndpoint { get; set; } + + /// + /// Gets or sets the token validation parameters associated with the authorization server. + /// + public TokenValidationParameters TokenValidationParameters { get; } = new TokenValidationParameters + { + ClockSkew = TimeSpan.Zero, + NameClaimType = Claims.Name, + RoleClaimType = Claims.Role, + // Note: audience and lifetime are manually validated by OpenIddict itself. + ValidateAudience = false, + ValidateLifetime = false + }; + + /// + /// Gets or sets the URL of the OAuth 2.0/OpenID Connect server discovery endpoint. + /// When the URL is relative, must be set and absolute. + /// + public Uri? MetadataAddress { get; set; } + + /// + /// Gets the list of scopes sent by default as part of authorization requests. + /// + public HashSet Scopes { get; } = new(StringComparer.Ordinal); +} diff --git a/src/OpenIddict.Client/OpenIddictClientRetriever.cs b/src/OpenIddict.Client/OpenIddictClientRetriever.cs new file mode 100644 index 00000000..5b9d6925 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientRetriever.cs @@ -0,0 +1,62 @@ +/* + * 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.IdentityModel.Protocols; + +namespace OpenIddict.Client; + +public class OpenIddictClientRetriever : IConfigurationRetriever +{ + private readonly OpenIddictClientService _service; + + /// + /// Creates a new instance of the class. + /// + /// The validation service. + public OpenIddictClientRetriever(OpenIddictClientService service) + => _service = service; + + /// + /// Retrieves the OpenID Connect server configuration from the specified address. + /// + /// The address of the remote metadata endpoint. + /// The retriever used by IdentityModel. + /// The that can be used to abort the operation. + /// The OpenID Connect server configuration retrieved from the remote server. + async Task IConfigurationRetriever.GetConfigurationAsync(string address, IDocumentRetriever retriever, CancellationToken cancel) + { + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0143), nameof(address)); + } + + if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString()) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(address)); + } + + cancel.ThrowIfCancellationRequested(); + + var configuration = await _service.GetConfigurationAsync(uri, cancel) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0145)); + + if (configuration.JwksUri is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0146)); + } + + configuration.JsonWebKeySet = await _service.GetSecurityKeysAsync(configuration.JwksUri, cancel) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0147)); + + // Copy the signing keys found in the JSON Web Key Set to the SigningKeys collection. + foreach (var key in configuration.JsonWebKeySet.GetSigningKeys()) + { + configuration.SigningKeys.Add(key); + } + + return configuration; + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs new file mode 100644 index 00000000..0383dd44 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientService.cs @@ -0,0 +1,464 @@ +/* + * 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 Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; + +namespace OpenIddict.Client; + +public class OpenIddictClientService +{ + private readonly IServiceProvider _provider; + + /// + /// Creates a new instance of the class. + /// + /// The service provider. + public OpenIddictClientService(IServiceProvider provider) + => _provider = provider; + + /// + /// Retrieves the OpenID Connect server configuration from the specified address. + /// + /// The address of the remote metadata endpoint. + /// The that can be used to abort the operation. + /// The OpenID Connect server configuration retrieved from the remote server. + public async ValueTask GetConfigurationAsync(Uri address, CancellationToken cancellationToken = default) + { + if (address is null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!address.IsAbsoluteUri) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(address)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Note: this service is registered as a singleton service. As such, it cannot + // directly depend on scoped services like the validation provider. To work around + // this limitation, a scope is manually created for each method to this service. + var scope = _provider.CreateScope(); + + // Note: a try/finally block is deliberately used here to ensure the service scope + // can be disposed of asynchronously if it implements IAsyncDisposable. + try + { + var dispatcher = scope.ServiceProvider.GetRequiredService(); + var factory = scope.ServiceProvider.GetRequiredService(); + var transaction = await factory.CreateTransactionAsync(); + + var request = new OpenIddictRequest(); + request = await PrepareConfigurationRequestAsync(); + request = await ApplyConfigurationRequestAsync(); + var response = await ExtractConfigurationResponseAsync(); + + var configuration = await HandleConfigurationResponseAsync(); + if (configuration is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0145)); + } + + return configuration; + + async ValueTask PrepareConfigurationRequestAsync() + { + var context = new PrepareConfigurationRequestContext(transaction) + { + Address = address, + Request = request + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + throw new OpenIddictExceptions.GenericException( + SR.FormatID0148(context.Error, context.ErrorDescription, context.ErrorUri), + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Request; + } + + async ValueTask ApplyConfigurationRequestAsync() + { + var context = new ApplyConfigurationRequestContext(transaction) + { + Address = address, + Request = request + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + throw new OpenIddictExceptions.GenericException( + SR.FormatID0149(context.Error, context.ErrorDescription, context.ErrorUri), + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Request; + } + + async ValueTask ExtractConfigurationResponseAsync() + { + var context = new ExtractConfigurationResponseContext(transaction) + { + Address = address, + Request = request + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + throw new OpenIddictExceptions.GenericException( + SR.FormatID0150(context.Error, context.ErrorDescription, context.ErrorUri), + context.Error, context.ErrorDescription, context.ErrorUri); + } + + Debug.Assert(context.Response is not null, SR.GetResourceString(SR.ID4007)); + + return context.Response; + } + + async ValueTask HandleConfigurationResponseAsync() + { + var context = new HandleConfigurationResponseContext(transaction) + { + Address = address, + Request = request, + Response = response + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + throw new OpenIddictExceptions.GenericException( + SR.FormatID0151(context.Error, context.ErrorDescription, context.ErrorUri), + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Configuration; + } + } + + finally + { + if (scope is IAsyncDisposable disposable) + { + await disposable.DisposeAsync(); + } + + else + { + scope.Dispose(); + } + } + } + + /// + /// Retrieves the security keys exposed by the specified JWKS endpoint. + /// + /// The address of the remote metadata endpoint. + /// The that can be used to abort the operation. + /// The security keys retrieved from the remote server. + public async ValueTask GetSecurityKeysAsync(Uri address, CancellationToken cancellationToken = default) + { + if (address is null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!address.IsAbsoluteUri) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(address)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Note: this service is registered as a singleton service. As such, it cannot + // directly depend on scoped services like the validation provider. To work around + // this limitation, a scope is manually created for each method to this service. + var scope = _provider.CreateScope(); + + // Note: a try/finally block is deliberately used here to ensure the service scope + // can be disposed of asynchronously if it implements IAsyncDisposable. + try + { + var dispatcher = scope.ServiceProvider.GetRequiredService(); + var factory = scope.ServiceProvider.GetRequiredService(); + var transaction = await factory.CreateTransactionAsync(); + + var request = new OpenIddictRequest(); + request = await PrepareCryptographyRequestAsync(); + request = await ApplyCryptographyRequestAsync(); + + var response = await ExtractCryptographyResponseAsync(); + + var keys = await HandleCryptographyResponseAsync(); + if (keys is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0147)); + } + + return keys; + + async ValueTask PrepareCryptographyRequestAsync() + { + var context = new PrepareCryptographyRequestContext(transaction) + { + Address = address, + Request = request + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + throw new OpenIddictExceptions.GenericException( + SR.FormatID0152(context.Error, context.ErrorDescription, context.ErrorUri), + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Request; + } + + async ValueTask ApplyCryptographyRequestAsync() + { + var context = new ApplyCryptographyRequestContext(transaction) + { + Address = address, + Request = request + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + throw new OpenIddictExceptions.GenericException( + SR.FormatID0153(context.Error, context.ErrorDescription, context.ErrorUri), + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Request; + } + + async ValueTask ExtractCryptographyResponseAsync() + { + var context = new ExtractCryptographyResponseContext(transaction) + { + Address = address, + Request = request + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + throw new OpenIddictExceptions.GenericException( + SR.FormatID0154(context.Error, context.ErrorDescription, context.ErrorUri), + context.Error, context.ErrorDescription, context.ErrorUri); + } + + Debug.Assert(context.Response is not null, SR.GetResourceString(SR.ID4007)); + + return context.Response; + } + + async ValueTask HandleCryptographyResponseAsync() + { + var context = new HandleCryptographyResponseContext(transaction) + { + Address = address, + Request = request, + Response = response + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + throw new OpenIddictExceptions.GenericException( + SR.FormatID0155(context.Error, context.ErrorDescription, context.ErrorUri), + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.SecurityKeys; + } + } + + finally + { + if (scope is IAsyncDisposable disposable) + { + await disposable.DisposeAsync(); + } + + else + { + scope.Dispose(); + } + } + } + + /// + /// Sends the token request and retrieves the corresponding response. + /// + /// The client registration. + /// The token request. + /// The that can be used to abort the operation. + /// The token response. + public async ValueTask SendTokenRequestAsync( + OpenIddictClientRegistration registration, OpenIddictRequest request, CancellationToken cancellationToken = default) + { + if (registration is null) + { + throw new ArgumentNullException(nameof(registration)); + } + + var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } || + !configuration.TokenEndpoint.IsWellFormedOriginalString()) + { + throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Note: this service is registered as a singleton service. As such, it cannot + // directly depend on scoped services like the validation provider. To work around + // this limitation, a scope is manually created for each method to this service. + var scope = _provider.CreateScope(); + + // Note: a try/finally block is deliberately used here to ensure the service scope + // can be disposed of asynchronously if it implements IAsyncDisposable. + try + { + var dispatcher = scope.ServiceProvider.GetRequiredService(); + var factory = scope.ServiceProvider.GetRequiredService(); + var transaction = await factory.CreateTransactionAsync(); + + request = await PrepareTokenRequestAsync(); + request = await ApplyTokenRequestAsync(); + + var response = await ExtractTokenResponseAsync(); + + return await HandleTokenResponseAsync(); + + async ValueTask PrepareTokenRequestAsync() + { + var context = new PrepareTokenRequestContext(transaction) + { + Address = configuration.TokenEndpoint, + Issuer = registration.Issuer, + Registration = registration, + Request = request + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + throw new OpenIddictExceptions.GenericException( + SR.FormatID0152(context.Error, context.ErrorDescription, context.ErrorUri), + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Request; + } + + async ValueTask ApplyTokenRequestAsync() + { + var context = new ApplyTokenRequestContext(transaction) + { + Address = configuration.TokenEndpoint, + Issuer = registration.Issuer, + Registration = registration, + Request = request + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + throw new OpenIddictExceptions.GenericException( + SR.FormatID0153(context.Error, context.ErrorDescription, context.ErrorUri), + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Request; + } + + async ValueTask ExtractTokenResponseAsync() + { + var context = new ExtractTokenResponseContext(transaction) + { + Address = configuration.TokenEndpoint, + Issuer = registration.Issuer, + Registration = registration, + Request = request + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + throw new OpenIddictExceptions.GenericException( + SR.FormatID0154(context.Error, context.ErrorDescription, context.ErrorUri), + context.Error, context.ErrorDescription, context.ErrorUri); + } + + Debug.Assert(context.Response is not null, SR.GetResourceString(SR.ID4007)); + + return context.Response; + } + + async ValueTask HandleTokenResponseAsync() + { + var context = new HandleTokenResponseContext(transaction) + { + Address = configuration.TokenEndpoint, + Issuer = registration.Issuer, + Registration = registration, + Request = request, + Response = response + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + throw new OpenIddictExceptions.GenericException( + SR.FormatID0155(context.Error, context.ErrorDescription, context.ErrorUri), + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Response; + } + } + + finally + { + if (scope is IAsyncDisposable disposable) + { + await disposable.DisposeAsync(); + } + + else + { + scope.Dispose(); + } + } + } +} diff --git a/src/OpenIddict.Client/OpenIddictClientTransaction.cs b/src/OpenIddict.Client/OpenIddictClientTransaction.cs new file mode 100644 index 00000000..7e093585 --- /dev/null +++ b/src/OpenIddict.Client/OpenIddictClientTransaction.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 Microsoft.Extensions.Logging; + +namespace OpenIddict.Client; + +/// +/// Represents the context associated with an OpenID Connect client request. +/// +public class OpenIddictClientTransaction +{ + /// + /// Gets or sets the type of the endpoint processing the current request. + /// + public OpenIddictClientEndpointType EndpointType { get; set; } + + /// + /// Gets or sets the issuer address associated with the current transaction, if available. + /// + public Uri? Issuer { get; set; } + + /// + /// Gets or sets the logger associated with the current request. + /// + public ILogger Logger { get; set; } = default!; + + /// + /// Gets or sets the options associated with the current request. + /// + public OpenIddictClientOptions Options { get; set; } = default!; + + /// + /// Gets the additional properties associated with the current request. + /// + public Dictionary Properties { get; } = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets or sets the client registration used for the current request. + /// + public OpenIddictClientRegistration Registration { get; set; } = default!; + + /// + /// 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.AspNetCore/OpenIddictServerAspNetCoreConstants.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs index 376d79f1..3513360f 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs @@ -34,9 +34,19 @@ public static class OpenIddictServerAspNetCoreConstants public const string Error = ".error"; public const string ErrorDescription = ".error_description"; public const string ErrorUri = ".error_uri"; - public const string IdentityTokenPrincipal = ".identity_token_principal"; + public const string IdentityTokenPrincipal = ".id_token_principal"; public const string RefreshTokenPrincipal = ".refresh_token_principal"; public const string Scope = ".scope"; public const string UserCodePrincipal = ".user_code_principal"; } + + public static class Tokens + { + public const string AccessToken = "access_token"; + public const string AuthorizationCode = "authorization_code"; + public const string DeviceCode = "device_code"; + public const string IdentityToken = "id_token"; + public const string RefreshToken = "refresh_token"; + public const string UserCode = "user_code"; + } } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs index 74f5d031..10cc5620 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs @@ -8,6 +8,7 @@ using System.Security.Claims; using System.Text.Encodings.Web; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants; using Properties = OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants.Properties; namespace OpenIddict.Server.AspNetCore; @@ -191,72 +192,72 @@ public class OpenIddictServerAspNetCoreHandler : AuthenticationHandler Transaction.Request = value; } + /// + /// Gets or sets a boolean indicating whether an access + /// token should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an authorization + /// code should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractAuthorizationCode { get; set; } + + /// + /// Gets or sets a boolean indicating whether a device + /// code should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractDeviceCode { get; set; } + + /// + /// Gets or sets a boolean indicating whether a generic + /// token should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractGenericToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an identity + /// token should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractIdentityToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a refresh + /// token should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractRefreshToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a user + /// code should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractUserCode { get; set; } + /// /// Gets or sets a boolean indicating whether an access token - /// must be resolved for the authentication to considered valid. + /// must be resolved for the authentication to be considered valid. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// @@ -299,7 +355,7 @@ public static partial class OpenIddictServerEvents /// /// Gets or sets a boolean indicating whether an authorization code - /// must be resolved for the authentication to considered valid. + /// must be resolved for the authentication to be considered valid. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// @@ -307,7 +363,7 @@ public static partial class OpenIddictServerEvents /// /// Gets or sets a boolean indicating whether a device code - /// must be resolved for the authentication to considered valid. + /// must be resolved for the authentication to be considered valid. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// @@ -315,7 +371,7 @@ public static partial class OpenIddictServerEvents /// /// Gets or sets a boolean indicating whether a generic token - /// must be resolved for the authentication to considered valid. + /// must be resolved for the authentication to be considered valid. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// @@ -323,7 +379,7 @@ public static partial class OpenIddictServerEvents /// /// Gets or sets a boolean indicating whether an identity token - /// must be resolved for the authentication to considered valid. + /// must be resolved for the authentication to be considered valid. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// @@ -331,7 +387,7 @@ public static partial class OpenIddictServerEvents /// /// Gets or sets a boolean indicating whether a refresh token - /// must be resolved for the authentication to considered valid. + /// must be resolved for the authentication to be considered valid. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// @@ -339,63 +395,63 @@ public static partial class OpenIddictServerEvents /// /// Gets or sets a boolean indicating whether a user code - /// must be resolved for the authentication to considered valid. + /// must be resolved for the authentication to be considered valid. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// public bool RequireUserCode { get; set; } /// - /// Gets or sets a boolean indicating whether an access token - /// should be extracted from the current context and validated. + /// Gets or sets a boolean indicating whether the access + /// token extracted from the current request should be validated. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// public bool ValidateAccessToken { get; set; } /// - /// Gets or sets a boolean indicating whether an authorization code - /// should be extracted from the current context and validated. + /// Gets or sets a boolean indicating whether the authorization + /// code extracted from the current request should be validated. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// public bool ValidateAuthorizationCode { get; set; } /// - /// Gets or sets a boolean indicating whether a device code - /// should be extracted from the current context and validated. + /// Gets or sets a boolean indicating whether the device + /// code extracted from the current request should be validated. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// public bool ValidateDeviceCode { get; set; } /// - /// Gets or sets a boolean indicating whether a generic token - /// should be extracted from the current context and validated. + /// Gets or sets a boolean indicating whether the generic + /// token extracted from the current request should be validated. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// public bool ValidateGenericToken { get; set; } /// - /// Gets or sets a boolean indicating whether an identity token - /// should be extracted from the current context and validated. + /// Gets or sets a boolean indicating whether the identity + /// token extracted from the current request should be validated. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// public bool ValidateIdentityToken { get; set; } /// - /// Gets or sets a boolean indicating whether a refresh token - /// should be extracted from the current context and validated. + /// Gets or sets a boolean indicating whether the refresh + /// token extracted from the current request should be validated. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// public bool ValidateRefreshToken { get; set; } /// - /// Gets or sets a boolean indicating whether a user code - /// should be extracted from the current context and validated. + /// Gets or sets a boolean indicating whether the user + /// code extracted from the current request should be validated. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs index 0a2343c7..8c035be0 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs @@ -255,7 +255,7 @@ public static partial class OpenIddictServerHandlers .Build(); /// - public ValueTask HandleAsync(ValidateTokenContext context) + public async ValueTask HandleAsync(ValidateTokenContext context) { if (context is null) { @@ -265,13 +265,13 @@ public static partial class OpenIddictServerHandlers // If a principal was already attached, don't overwrite it. if (context.Principal is not null) { - return default; + return; } // If the token cannot be read, don't return an error to allow another handler to validate it. if (!context.SecurityTokenHandler.CanReadToken(context.Token)) { - return default; + return; } // Special endpoints like introspection or revocation use a single parameter to convey @@ -290,7 +290,7 @@ public static partial class OpenIddictServerHandlers // For more information, see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 // and https://datatracker.ietf.org/doc/html/rfc7662#section-2.1. - var result = context.SecurityTokenHandler.ValidateToken(context.Token, context.TokenValidationParameters); + var result = await context.SecurityTokenHandler.ValidateTokenAsync(context.Token, context.TokenValidationParameters); if (!result.IsValid) { context.Logger.LogTrace(result.Exception, SR.GetResourceString(SR.ID6000), context.Token); @@ -348,7 +348,7 @@ public static partial class OpenIddictServerHandlers _ => SR.FormatID8000(SR.ID2004) }); - return default; + return; } // Get the JWT token. If the token is encrypted using JWE, retrieve the inner token. @@ -387,8 +387,6 @@ public static partial class OpenIddictServerHandlers } context.Logger.LogTrace(SR.GetResourceString(SR.ID6001), context.Token, context.Principal.Claims); - - return default; } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 5cc8ae67..463d6250 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -25,6 +25,7 @@ public static partial class OpenIddictServerHandlers ValidateAuthenticationDemand.Descriptor, EvaluateValidatedTokens.Descriptor, ResolveValidatedTokens.Descriptor, + ValidateRequiredTokens.Descriptor, ValidateAccessToken.Descriptor, ValidateAuthorizationCode.Descriptor, ValidateDeviceCode.Descriptor, @@ -164,63 +165,77 @@ public static partial class OpenIddictServerHandlers throw new ArgumentNullException(nameof(context)); } - (context.ValidateAccessToken, context.RequireAccessToken) = context.EndpointType switch + (context.ExtractAccessToken, + context.RequireAccessToken, + context.ValidateAccessToken) = context.EndpointType switch { // The userinfo endpoint requires sending a valid access token. - OpenIddictServerEndpointType.Userinfo => (true, true), + OpenIddictServerEndpointType.Userinfo => (true, true, true), - _ => (false, false) + _ => (false, false, false) }; - (context.ValidateAuthorizationCode, context.RequireAuthorizationCode) = context.EndpointType switch + (context.ExtractAuthorizationCode, + context.RequireAuthorizationCode, + context.ValidateAuthorizationCode) = context.EndpointType switch { // The authorization code grant requires sending a valid authorization code. - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() => (true, true), + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() => (true, true, true), - _ => (false, false) + _ => (false, false, false) }; - (context.ValidateDeviceCode, context.RequireDeviceCode) = context.EndpointType switch + (context.ExtractDeviceCode, + context.RequireDeviceCode, + context.ValidateDeviceCode) = context.EndpointType switch { // The device code grant requires sending a valid device code. - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() => (true, true), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() => (true, true, true), - _ => (false, false) + _ => (false, false, false) }; - (context.ValidateGenericToken, context.RequireGenericToken) = context.EndpointType switch + (context.ExtractGenericToken, + context.RequireGenericToken, + context.ValidateGenericToken) = context.EndpointType switch { // Tokens received by the introspection and revocation endpoints can be of any type. // Additional token type filtering is made by the endpoint themselves, if needed. - OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation => (true, true), + OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation => (true, true, true), - _ => (false, false) + _ => (false, false, false) }; - (context.ValidateIdentityToken, context.RequireIdentityToken) = context.EndpointType switch + (context.ExtractIdentityToken, + context.RequireIdentityToken, + context.ValidateIdentityToken) = context.EndpointType switch { // The identity token received by the authorization and logout // endpoints are not required and serve as optional hints. - OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout => (true, false), + OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout => (true, false, true), - _ => (false, false) + _ => (false, false, true) }; - (context.ValidateRefreshToken, context.RequireRefreshToken) = context.EndpointType switch + (context.ExtractRefreshToken, + context.RequireRefreshToken, + context.ValidateRefreshToken) = context.EndpointType switch { // The refresh token grant requires sending a valid refresh token. - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() => (true, true), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() => (true, true, true), - _ => (false, false) + _ => (false, false, false) }; - (context.ValidateUserCode, context.RequireUserCode) = context.EndpointType switch + (context.ExtractUserCode, + context.RequireUserCode, + context.ValidateUserCode) = context.EndpointType switch { // Note: the verification endpoint can be accessed without specifying a // user code (that can be later set by the user using a form, for instance). - OpenIddictServerEndpointType.Verification => (true, false), + OpenIddictServerEndpointType.Verification => (true, false, true), - _ => (false, false) + _ => (false, false, false) }; return default; @@ -252,21 +267,24 @@ public static partial class OpenIddictServerHandlers context.AccessToken = context.EndpointType switch { - OpenIddictServerEndpointType.Userinfo when context.ValidateAccessToken => context.Request.AccessToken, + OpenIddictServerEndpointType.Userinfo when context.ExtractAccessToken + => context.Request.AccessToken, _ => null }; context.AuthorizationCode = context.EndpointType switch { - OpenIddictServerEndpointType.Token when context.ValidateAuthorizationCode => context.Request.Code, + OpenIddictServerEndpointType.Token when context.ExtractAuthorizationCode + => context.Request.Code, _ => null }; context.DeviceCode = context.EndpointType switch { - OpenIddictServerEndpointType.Token when context.ValidateDeviceCode => context.Request.DeviceCode, + OpenIddictServerEndpointType.Token when context.ExtractDeviceCode + => context.Request.DeviceCode, _ => null }; @@ -274,8 +292,8 @@ public static partial class OpenIddictServerHandlers (context.GenericToken, context.GenericTokenTypeHint) = context.EndpointType switch { OpenIddictServerEndpointType.Introspection or - OpenIddictServerEndpointType.Revocation - when context.ValidateGenericToken => (context.Request.Token, context.Request.TokenTypeHint), + OpenIddictServerEndpointType.Revocation when context.ExtractGenericToken + => (context.Request.Token, context.Request.TokenTypeHint), _ => (null, null) }; @@ -283,22 +301,24 @@ public static partial class OpenIddictServerHandlers context.IdentityToken = context.EndpointType switch { OpenIddictServerEndpointType.Authorization or - OpenIddictServerEndpointType.Logout - when context.ValidateIdentityToken => context.Request.IdTokenHint, + OpenIddictServerEndpointType.Logout when context.ExtractIdentityToken + => context.Request.IdTokenHint, _ => null }; context.RefreshToken = context.EndpointType switch { - OpenIddictServerEndpointType.Token when context.ValidateRefreshToken => context.Request.RefreshToken, + OpenIddictServerEndpointType.Token when context.ExtractRefreshToken + => context.Request.RefreshToken, _ => null }; context.UserCode = context.EndpointType switch { - OpenIddictServerEndpointType.Verification when context.ValidateUserCode => context.Request.UserCode, + OpenIddictServerEndpointType.Verification when context.ExtractUserCode + => context.Request.UserCode, _ => null }; @@ -307,6 +327,49 @@ public static partial class OpenIddictServerHandlers } } + /// + /// Contains the logic responsible of rejecting authentication demands that lack required tokens. + /// + public class ValidateRequiredTokens : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(EvaluateValidatedTokens.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if ((context.RequireAccessToken && string.IsNullOrEmpty(context.AccessToken)) || + (context.RequireAuthorizationCode && string.IsNullOrEmpty(context.AuthorizationCode)) || + (context.RequireDeviceCode && string.IsNullOrEmpty(context.DeviceCode)) || + (context.RequireGenericToken && string.IsNullOrEmpty(context.GenericToken)) || + (context.RequireIdentityToken && string.IsNullOrEmpty(context.IdentityToken)) || + (context.RequireRefreshToken && string.IsNullOrEmpty(context.RefreshToken)) || + (context.RequireUserCode && string.IsNullOrEmpty(context.UserCode))) + { + context.Reject( + error: Errors.MissingToken, + description: SR.GetResourceString(SR.ID2000), + uri: SR.FormatID8000(SR.ID2000)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible of validating the access token resolved from the context. /// @@ -324,7 +387,7 @@ public static partial class OpenIddictServerHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseScopedHandler() - .SetOrder(ResolveValidatedTokens.Descriptor.Order + 1_000) + .SetOrder(ValidateRequiredTokens.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -336,23 +399,8 @@ public static partial class OpenIddictServerHandlers throw new ArgumentNullException(nameof(context)); } - if (context.AccessTokenPrincipal is not null) - { - return; - } - - if (string.IsNullOrEmpty(context.AccessToken)) + if (context.AccessTokenPrincipal is not null || string.IsNullOrEmpty(context.AccessToken)) { - if (context.RequireAccessToken) - { - context.Reject( - error: Errors.MissingToken, - description: SR.GetResourceString(SR.ID2000), - uri: SR.FormatID8000(SR.ID2000)); - - return; - } - return; } @@ -418,26 +466,11 @@ public static partial class OpenIddictServerHandlers throw new ArgumentNullException(nameof(context)); } - if (context.AuthorizationCodePrincipal is not null) + if (context.AuthorizationCodePrincipal is not null || string.IsNullOrEmpty(context.AuthorizationCode)) { return; } - if (string.IsNullOrEmpty(context.AuthorizationCode)) - { - if (context.RequireAuthorizationCode) - { - context.Reject( - error: Errors.MissingToken, - description: SR.GetResourceString(SR.ID2000), - uri: SR.FormatID8000(SR.ID2000)); - - return; - } - - return; - } - var notification = new ValidateTokenContext(context.Transaction) { Token = context.AuthorizationCode, @@ -500,23 +533,8 @@ public static partial class OpenIddictServerHandlers throw new ArgumentNullException(nameof(context)); } - if (context.DeviceCodePrincipal is not null) - { - return; - } - - if (string.IsNullOrEmpty(context.DeviceCode)) + if (context.DeviceCodePrincipal is not null || string.IsNullOrEmpty(context.DeviceCode)) { - if (context.RequireDeviceCode) - { - context.Reject( - error: Errors.MissingToken, - description: SR.GetResourceString(SR.ID2000), - uri: SR.FormatID8000(SR.ID2000)); - - return; - } - return; } @@ -582,23 +600,8 @@ public static partial class OpenIddictServerHandlers throw new ArgumentNullException(nameof(context)); } - if (context.GenericTokenPrincipal is not null) - { - return; - } - - if (string.IsNullOrEmpty(context.GenericToken)) + if (context.GenericTokenPrincipal is not null || string.IsNullOrEmpty(context.GenericToken)) { - if (context.RequireGenericToken) - { - context.Reject( - error: Errors.MissingToken, - description: SR.GetResourceString(SR.ID2000), - uri: SR.FormatID8000(SR.ID2000)); - - return; - } - return; } @@ -671,26 +674,11 @@ public static partial class OpenIddictServerHandlers throw new ArgumentNullException(nameof(context)); } - if (context.IdentityTokenPrincipal is not null) + if (context.IdentityTokenPrincipal is not null || string.IsNullOrEmpty(context.IdentityToken)) { return; } - if (string.IsNullOrEmpty(context.IdentityToken)) - { - if (context.RequireIdentityToken) - { - context.Reject( - error: Errors.MissingToken, - description: SR.GetResourceString(SR.ID2000), - uri: SR.FormatID8000(SR.ID2000)); - - return; - } - - return; - } - var notification = new ValidateTokenContext(context.Transaction) { // Don't validate the lifetime of id_tokens used as id_token_hints. @@ -756,26 +744,11 @@ public static partial class OpenIddictServerHandlers throw new ArgumentNullException(nameof(context)); } - if (context.RefreshTokenPrincipal is not null) + if (context.RefreshTokenPrincipal is not null || string.IsNullOrEmpty(context.RefreshToken)) { return; } - if (string.IsNullOrEmpty(context.RefreshToken)) - { - if (context.RequireRefreshToken) - { - context.Reject( - error: Errors.MissingToken, - description: SR.GetResourceString(SR.ID2000), - uri: SR.FormatID8000(SR.ID2000)); - - return; - } - - return; - } - var notification = new ValidateTokenContext(context.Transaction) { Token = context.RefreshToken, @@ -838,26 +811,11 @@ public static partial class OpenIddictServerHandlers throw new ArgumentNullException(nameof(context)); } - if (context.UserCodePrincipal is not null) + if (context.UserCodePrincipal is not null || string.IsNullOrEmpty(context.UserCode)) { return; } - if (string.IsNullOrEmpty(context.UserCode)) - { - if (context.RequireUserCode) - { - context.Reject( - error: Errors.MissingToken, - description: SR.GetResourceString(SR.ID2000), - uri: SR.FormatID8000(SR.ID2000)); - - return; - } - - return; - } - var notification = new ValidateTokenContext(context.Transaction) { Token = context.UserCode, @@ -2649,22 +2607,22 @@ public static partial class OpenIddictServerHandlers return default; } - var credentials = context.Options.SigningCredentials.FirstOrDefault( + var credentials = context.Options.SigningCredentials.Find( credentials => credentials.Key is AsymmetricSecurityKey); if (credentials is null) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0266)); } - using var hash = GetHashAlgorithm(credentials); - if (hash is null || hash is KeyedHashAlgorithm) + using var algorithm = GetHashAlgorithm(credentials); + if (algorithm is null || algorithm is KeyedHashAlgorithm) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0267)); } if (!string.IsNullOrEmpty(context.AccessToken)) { - var digest = hash.ComputeHash(Encoding.ASCII.GetBytes(context.AccessToken)); + var digest = algorithm.ComputeHash(Encoding.ASCII.GetBytes(context.AccessToken)); // Note: only the left-most half of the hash is used. // See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken @@ -2673,7 +2631,7 @@ public static partial class OpenIddictServerHandlers if (!string.IsNullOrEmpty(context.AuthorizationCode)) { - var digest = hash.ComputeHash(Encoding.ASCII.GetBytes(context.AuthorizationCode)); + var digest = algorithm.ComputeHash(Encoding.ASCII.GetBytes(context.AuthorizationCode)); // Note: only the left-most half of the hash is used. // See http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs index a1a4a8ef..a5904bcd 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs @@ -25,4 +25,9 @@ public static class OpenIddictValidationAspNetCoreConstants public const string ErrorUri = ".error_uri"; public const string Scope = ".scope"; } + + public static class Tokens + { + public const string AccessToken = "access_token"; + } } diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs index 17fe35eb..2d73db10 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs @@ -7,6 +7,7 @@ using System.Text.Encodings.Web; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using static OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreConstants; using Properties = OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreConstants.Properties; namespace OpenIddict.Validation.AspNetCore; @@ -168,12 +169,12 @@ public class OpenIddictValidationAspNetCoreHandler : AuthenticationHandler() .AddFilter() - .AddFilter() + .AddFilter() .UseSingletonHandler() .SetOrder(EvaluateValidatedTokens.Descriptor.Order + 500) .SetType(OpenIddictValidationHandlerType.BuiltIn) @@ -188,7 +188,7 @@ public static partial class OpenIddictValidationAspNetCoreHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() - .AddFilter() + .AddFilter() .UseSingletonHandler() .SetOrder(ExtractAccessTokenFromAuthorizationHeader.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) @@ -246,7 +246,7 @@ public static partial class OpenIddictValidationAspNetCoreHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() - .AddFilter() + .AddFilter() .UseSingletonHandler() .SetOrder(ExtractAccessTokenFromBodyForm.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs index 9f897f72..b9044eac 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs @@ -175,7 +175,7 @@ public class OpenIddictValidationOwinHandler : AuthenticationHandler() .AddFilter() - .AddFilter() + .AddFilter() .UseSingletonHandler() .SetOrder(EvaluateValidatedTokens.Descriptor.Order + 500) .SetType(OpenIddictValidationHandlerType.BuiltIn) @@ -187,7 +187,7 @@ public static partial class OpenIddictValidationOwinHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() - .AddFilter() + .AddFilter() .UseSingletonHandler() .SetOrder(ExtractAccessTokenFromAuthorizationHeader.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) @@ -246,7 +246,7 @@ public static partial class OpenIddictValidationOwinHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() - .AddFilter() + .AddFilter() .UseSingletonHandler() .SetOrder(ExtractAccessTokenFromBodyForm.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) diff --git a/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs b/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs index 3aed153d..94c3f474 100644 --- a/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs +++ b/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs @@ -5,7 +5,6 @@ */ using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; using OpenIddict.Server; namespace OpenIddict.Validation.ServerIntegration; @@ -39,9 +38,9 @@ public class OpenIddictValidationServerIntegrationConfiguration : IConfigureOpti // Note: the issuer may be null. In this case, it will be usually provided by // a validation handler registered by the host (e.g ASP.NET Core or OWIN/Katana). - options.Configuration = new OpenIdConnectConfiguration + options.Configuration = new OpenIddictConfiguration { - Issuer = _options.CurrentValue.Issuer?.AbsoluteUri + Issuer = _options.CurrentValue.Issuer }; // Import the signing keys from the server configuration. diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs index 48555306..0a35dd27 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs @@ -14,9 +14,9 @@ public static class OpenIddictValidationSystemNetHttpHandlerFilters /// /// Represents a filter that excludes the associated handlers if the metadata address of the issuer is not available. /// - public class RequireHttpMetadataAddress : IOpenIddictValidationHandlerFilter + public class RequireHttpMetadataAddress : IOpenIddictValidationHandlerFilter { - public ValueTask IsActiveAsync(BaseContext context) + public ValueTask IsActiveAsync(BaseExternalContext context) { if (context is null) { @@ -24,8 +24,8 @@ public static class OpenIddictValidationSystemNetHttpHandlerFilters } return new ValueTask( - string.Equals(context.Options.MetadataAddress?.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || - string.Equals(context.Options.MetadataAddress?.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)); + string.Equals(context.Address?.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || + string.Equals(context.Address?.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)); } } } diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs index 38e889c7..e0cf1ef1 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs @@ -74,6 +74,12 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + // Ensure the issuer resolved from the configuration matches the expected value. + if (context.Options.Issuer is not null && configuration.Issuer != context.Options.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + } + // The OAuth 2.0 specification recommends sending the client credentials using basic authentication. // However, this authentication method is known to have compatibility issues with the way the // client credentials are encoded (they MUST be formURL-encoded before being base64-encoded). diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs index 548b1e4a..2e7c539a 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs @@ -117,7 +117,7 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() - .SetOrder(AttachFormParameters.Descriptor.Order - 100_000) + .SetOrder(AttachFormParameters.Descriptor.Order - 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index 6924c292..4935c3e3 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -10,7 +10,6 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using OpenIddict.Validation; @@ -404,7 +403,7 @@ public class OpenIddictValidationBuilder /// /// The server configuration. /// The . - public OpenIddictValidationBuilder SetConfiguration(OpenIdConnectConfiguration configuration) + public OpenIddictValidationBuilder SetConfiguration(OpenIddictConfiguration configuration) { if (configuration is null) { diff --git a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs index 31f2c9ab..741cbd21 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; namespace OpenIddict.Validation; @@ -90,7 +89,8 @@ public class OpenIddictValidationConfiguration : IPostConfigureOptions(options.Configuration); + options.Configuration.Issuer = options.Issuer; + options.ConfigurationManager = new StaticConfigurationManager(options.Configuration); } else @@ -133,11 +133,11 @@ public class OpenIddictValidationConfiguration : IPostConfigureOptions( + options.ConfigurationManager = new ConfigurationManager( options.MetadataAddress.AbsoluteUri, new OpenIddictValidationRetriever(_service)) { - AutomaticRefreshInterval = ConfigurationManager.DefaultAutomaticRefreshInterval, - RefreshInterval = ConfigurationManager.DefaultRefreshInterval + AutomaticRefreshInterval = ConfigurationManager.DefaultAutomaticRefreshInterval, + RefreshInterval = ConfigurationManager.DefaultRefreshInterval }; } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.Discovery.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.Discovery.cs index 36cd86b6..6aa7bb0d 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.Discovery.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.Discovery.cs @@ -4,7 +4,6 @@ * the license and the contributors participating to this project. */ -using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; namespace OpenIddict.Validation; @@ -126,7 +125,7 @@ public static partial class OpenIddictValidationEvents /// /// Gets the OpenID Connect configuration. /// - public OpenIdConnectConfiguration Configuration { get; } = new OpenIdConnectConfiguration(); + public OpenIddictConfiguration Configuration { get; } = new OpenIddictConfiguration(); } /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs index 457cedba..4f5c4ae8 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs @@ -265,17 +265,25 @@ public static partial class OpenIddictValidationEvents /// public string? AccessToken { get; set; } + /// + /// Gets or sets a boolean indicating whether an access + /// token should be extracted from the current context. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ExtractAccessToken { get; set; } + /// /// Gets or sets a boolean indicating whether an access token - /// must be resolved for the authentication to considered valid. + /// must be resolved for the authentication to be considered valid. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// public bool RequireAccessToken { get; set; } /// - /// Gets or sets a boolean indicating whether an access token - /// should be extracted from the current context and validated. + /// Gets or sets a boolean indicating whether the access + /// token extracted from the current context should be validated. /// Note: overriding the value of this property is generally not /// recommended, except when dealing with non-standard clients. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs index cb0e3a24..769380fe 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs @@ -41,6 +41,7 @@ public static class OpenIddictValidationExtensions builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); // Register the built-in filters used by the default OpenIddict validation event handlers. + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs index b6cd15f6..92c16843 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs @@ -11,6 +11,22 @@ namespace OpenIddict.Validation; [EditorBrowsable(EditorBrowsableState.Advanced)] public static class OpenIddictValidationHandlerFilters { + /// + /// Represents a filter that excludes the associated handlers if no access token is extracted. + /// + public class RequireAccessTokenExtracted : IOpenIddictValidationHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ExtractAccessToken); + } + } + /// /// Represents a filter that excludes the associated handlers if no access token is validated. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs index 3bf8ea3c..04bbee75 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs @@ -21,6 +21,7 @@ public static partial class OpenIddictValidationHandlers ValidateIssuer.Descriptor, ExtractCryptographyEndpoint.Descriptor, ExtractIntrospectionEndpoint.Descriptor, + ExtractIntrospectionEndpointClientAuthenticationMethods.Descriptor, /* * Cryptography response handling: @@ -74,17 +75,7 @@ public static partial class OpenIddictValidationHandlers return default; } - if (context.Issuer is not null && context.Issuer != address) - { - context.Reject( - error: Errors.ServerError, - description: SR.GetResourceString(SR.ID2098), - uri: SR.FormatID8000(SR.ID2098)); - - return default; - } - - context.Configuration.Issuer = issuer; + context.Configuration.Issuer = address; return default; } @@ -126,17 +117,17 @@ public static partial class OpenIddictValidationHandlers return default; } - if (!Uri.IsWellFormedUriString(address, UriKind.Absolute)) + if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString()) { context.Reject( error: Errors.ServerError, - description: SR.GetResourceString(SR.ID2100), + description: SR.FormatID2100(Metadata.JwksUri), uri: SR.FormatID8000(SR.ID2100)); return default; } - context.Configuration.JwksUri = address; + context.Configuration.JwksUri = uri; return default; } @@ -166,30 +157,61 @@ public static partial class OpenIddictValidationHandlers } var address = (string?) context.Response[Metadata.IntrospectionEndpoint]; - if (!string.IsNullOrEmpty(address) && !Uri.IsWellFormedUriString(address, UriKind.Absolute)) + if (!string.IsNullOrEmpty(address)) { - context.Reject( - error: Errors.ServerError, - description: SR.GetResourceString(SR.ID2101), - uri: SR.FormatID8000(SR.ID2101)); + if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString()) + { + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2100(Metadata.IntrospectionEndpoint), + uri: SR.FormatID8000(SR.ID2100)); - return default; + return default; + } + + context.Configuration.IntrospectionEndpoint = uri; } - context.Configuration.IntrospectionEndpoint = address; + return default; + } + } + + /// + /// Contains the logic responsible of extracting the authentication methods + /// supported by the introspection endpoint from the discovery document. + /// + public class ExtractIntrospectionEndpointClientAuthenticationMethods : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ExtractIntrospectionEndpoint.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } // Resolve the client authentication methods supported by the introspection endpoint, if available. - if (context.Response.TryGetParameter(Metadata.IntrospectionEndpointAuthMethodsSupported, out var methods)) + var methods = context.Response[Metadata.IntrospectionEndpointAuthMethodsSupported]?.GetUnnamedParameters(); + if (methods is { Count: > 0 }) { - foreach (var method in methods.GetUnnamedParameters()) + for (var index = 0; index < methods.Count; index++) { - var value = (string?) method; - if (string.IsNullOrEmpty(value)) + // Note: custom values are allowed in this case. + var method = (string?) methods[index]; + if (!string.IsNullOrEmpty(method)) { - continue; + context.Configuration.IntrospectionEndpointAuthMethodsSupported.Add(method); } - - context.Configuration.IntrospectionEndpointAuthMethodsSupported.Add(value); } } @@ -221,7 +243,7 @@ public static partial class OpenIddictValidationHandlers } var keys = context.Response[JsonWebKeySetParameterNames.Keys]?.GetUnnamedParameters(); - if (keys is null || keys.Count == 0) + if (keys is not { Count: > 0 }) { context.Reject( error: Errors.ServerError, diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs index 552bd5b6..115ced33 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs @@ -61,10 +61,16 @@ public static partial class OpenIddictValidationHandlers var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + // Ensure the issuer resolved from the configuration matches the expected value. + if (context.Options.Issuer is not null && configuration.Issuer != context.Options.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + } + // Clone the token validation parameters and set the issuer using the value found in the // OpenID Connect server configuration (that can be static or retrieved using discovery). var parameters = context.Options.TokenValidationParameters.Clone(); - parameters.ValidIssuer ??= configuration.Issuer ?? context.Issuer?.AbsoluteUri; + parameters.ValidIssuer ??= configuration.Issuer?.AbsoluteUri ?? context.Issuer?.AbsoluteUri; parameters.ValidateIssuer = !string.IsNullOrEmpty(parameters.ValidIssuer); // Combine the signing keys registered statically in the token validation parameters @@ -191,7 +197,7 @@ public static partial class OpenIddictValidationHandlers .Build(); /// - public ValueTask HandleAsync(ValidateTokenContext context) + public async ValueTask HandleAsync(ValidateTokenContext context) { if (context is null) { @@ -201,16 +207,16 @@ public static partial class OpenIddictValidationHandlers // If a principal was already attached, don't overwrite it. if (context.Principal is not null) { - return default; + return; } // If the token cannot be read, don't return an error to allow another handler to validate it. if (!context.SecurityTokenHandler.CanReadToken(context.Token)) { - return default; + return; } - var result = context.SecurityTokenHandler.ValidateToken(context.Token, context.TokenValidationParameters); + var result = await context.SecurityTokenHandler.ValidateTokenAsync(context.Token, context.TokenValidationParameters); if (!result.IsValid) { // If validation failed because of an unrecognized key identifier, inform the configuration manager @@ -243,7 +249,7 @@ public static partial class OpenIddictValidationHandlers _ => SR.FormatID8000(SR.ID2004) }); - return default; + return; } // Attach the principal extracted from the token to the parent event context and store @@ -260,8 +266,6 @@ public static partial class OpenIddictValidationHandlers }); context.Logger.LogTrace(SR.GetResourceString(SR.ID6001), context.Token, context.Principal.Claims); - - return default; } } @@ -305,23 +309,24 @@ public static partial class OpenIddictValidationHandlers var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - if (string.IsNullOrEmpty(configuration.IntrospectionEndpoint) || - !Uri.TryCreate(configuration.IntrospectionEndpoint, UriKind.Absolute, out Uri? address) || - !address.IsWellFormedOriginalString()) + // Ensure the issuer resolved from the configuration matches the expected value. + if (context.Options.Issuer is not null && configuration.Issuer != context.Options.Issuer) { - context.Reject( - error: Errors.ServerError, - description: SR.GetResourceString(SR.ID2092), - uri: SR.FormatID8000(SR.ID2092)); + throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + } - return; + // Ensure the introspection endpoint is present and is a valid absolute URL. + if (configuration.IntrospectionEndpoint is not { IsAbsoluteUri: true } || + !configuration.IntrospectionEndpoint.IsWellFormedOriginalString()) + { + throw new InvalidOperationException(SR.FormatID0301(Metadata.IntrospectionEndpoint)); } ClaimsPrincipal principal; try { - principal = await _service.IntrospectTokenAsync(address, context.Token, context.ValidTokenTypes.Count switch + principal = await _service.IntrospectTokenAsync(configuration.IntrospectionEndpoint, context.Token, context.ValidTokenTypes.Count switch { // Infer the token type hint sent to the authorization server to help speed up // the token resolution lookup. If multiple types are accepted, no hint is sent. diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index 550f5de2..b27abad6 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -17,6 +17,7 @@ public static partial class OpenIddictValidationHandlers * Authentication processing: */ EvaluateValidatedTokens.Descriptor, + ValidateRequiredTokens.Descriptor, ValidateAccessToken.Descriptor, /* @@ -51,7 +52,7 @@ public static partial class OpenIddictValidationHandlers throw new ArgumentNullException(nameof(context)); } - (context.ValidateAccessToken, context.RequireAccessToken) = context.EndpointType switch + (context.ExtractAccessToken, context.RequireAccessToken, context.ValidateAccessToken) = context.EndpointType switch { // The validation handler is responsible of validating access tokens for endpoints // it doesn't manage (typically, API endpoints using token authentication). @@ -59,9 +60,9 @@ public static partial class OpenIddictValidationHandlers // As such, sending an access token is not mandatory: API endpoints that require // authentication can set up an authorization policy to reject such requests later // in the request processing pipeline (typically, via the authorization middleware). - OpenIddictValidationEndpointType.Unknown => (true, false), + OpenIddictValidationEndpointType.Unknown => (true, false, true), - _ => (false, false) + _ => (false, false, false) }; // Note: unlike the equivalent event in the server stack, authentication can be triggered for @@ -73,6 +74,43 @@ public static partial class OpenIddictValidationHandlers } } + /// + /// Contains the logic responsible of rejecting authentication demands that lack required tokens. + /// + public class ValidateRequiredTokens : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(EvaluateValidatedTokens.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.RequireAccessToken && string.IsNullOrEmpty(context.AccessToken)) + { + context.Reject( + error: Errors.MissingToken, + description: SR.GetResourceString(SR.ID2000), + uri: SR.FormatID8000(SR.ID2000)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible of ensuring a token was correctly resolved from the context. /// @@ -90,7 +128,7 @@ public static partial class OpenIddictValidationHandlers = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseScopedHandler() - .SetOrder(EvaluateValidatedTokens.Descriptor.Order + 1_000) + .SetOrder(ValidateRequiredTokens.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -102,23 +140,8 @@ public static partial class OpenIddictValidationHandlers throw new ArgumentNullException(nameof(context)); } - if (context.AccessTokenPrincipal is not null) - { - return; - } - - if (string.IsNullOrEmpty(context.AccessToken)) + if (context.AccessTokenPrincipal is not null || string.IsNullOrEmpty(context.AccessToken)) { - if (context.RequireAccessToken) - { - context.Reject( - error: Errors.MissingToken, - description: SR.GetResourceString(SR.ID2000), - uri: SR.FormatID8000(SR.ID2000)); - - return; - } - return; } diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index 92e4a2ff..b86e1992 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -6,7 +6,6 @@ using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Protocols; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; namespace OpenIddict.Validation; @@ -93,13 +92,13 @@ public class OpenIddictValidationOptions /// /// Gets or sets the OAuth 2.0/OpenID Connect static server configuration, if applicable. /// - public OpenIdConnectConfiguration? Configuration { get; set; } + public OpenIddictConfiguration? Configuration { get; set; } /// /// Gets or sets the configuration manager used to retrieve /// and cache the OAuth 2.0/OpenID Connect server configuration. /// - public IConfigurationManager ConfigurationManager { get; set; } = default!; + public IConfigurationManager ConfigurationManager { get; set; } = default!; /// /// Gets the intended audiences of this resource server. diff --git a/src/OpenIddict.Validation/OpenIddictValidationRetriever.cs b/src/OpenIddict.Validation/OpenIddictValidationRetriever.cs index 5215e34b..4689b25d 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationRetriever.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationRetriever.cs @@ -5,11 +5,10 @@ */ using Microsoft.IdentityModel.Protocols; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace OpenIddict.Validation; -public class OpenIddictValidationRetriever : IConfigurationRetriever +public class OpenIddictValidationRetriever : IConfigurationRetriever { private readonly OpenIddictValidationService _service; @@ -27,7 +26,7 @@ public class OpenIddictValidationRetriever : IConfigurationRetrieverThe retriever used by IdentityModel. /// The that can be used to abort the operation. /// The OpenID Connect server configuration retrieved from the remote server. - async Task IConfigurationRetriever.GetConfigurationAsync(string address, IDocumentRetriever retriever, CancellationToken cancel) + async Task IConfigurationRetriever.GetConfigurationAsync(string address, IDocumentRetriever retriever, CancellationToken cancel) { if (string.IsNullOrEmpty(address)) { @@ -44,12 +43,12 @@ public class OpenIddictValidationRetriever : IConfigurationRetrieverThe address of the remote metadata endpoint. /// The that can be used to abort the operation. /// The OpenID Connect server configuration retrieved from the remote server. - public async ValueTask GetConfigurationAsync(Uri address, CancellationToken cancellationToken = default) + public async ValueTask GetConfigurationAsync(Uri address, CancellationToken cancellationToken = default) { if (address is null) { @@ -93,6 +92,7 @@ public class OpenIddictValidationService { var context = new ApplyConfigurationRequestContext(transaction) { + Address = address, Request = request }; @@ -112,6 +112,7 @@ public class OpenIddictValidationService { var context = new ExtractConfigurationResponseContext(transaction) { + Address = address, Request = request }; @@ -129,10 +130,11 @@ public class OpenIddictValidationService return context.Response; } - async ValueTask HandleConfigurationResponseAsync() + async ValueTask HandleConfigurationResponseAsync() { var context = new HandleConfigurationResponseContext(transaction) { + Address = address, Request = request, Response = response }; @@ -235,6 +237,7 @@ public class OpenIddictValidationService { var context = new ApplyCryptographyRequestContext(transaction) { + Address = address, Request = request }; @@ -254,6 +257,7 @@ public class OpenIddictValidationService { var context = new ExtractCryptographyResponseContext(transaction) { + Address = address, Request = request }; @@ -396,6 +400,7 @@ public class OpenIddictValidationService { var context = new ApplyIntrospectionRequestContext(transaction) { + Address = address, Request = request }; @@ -415,6 +420,7 @@ public class OpenIddictValidationService { var context = new ExtractIntrospectionResponseContext(transaction) { + Address = address, Request = request }; @@ -436,6 +442,7 @@ public class OpenIddictValidationService { var context = new HandleIntrospectionResponseContext(transaction) { + Address = address, Request = request, Response = response, Token = token diff --git a/src/OpenIddict/OpenIddict.csproj b/src/OpenIddict/OpenIddict.csproj index dcabf3b5..cdb9a40d 100644 --- a/src/OpenIddict/OpenIddict.csproj +++ b/src/OpenIddict/OpenIddict.csproj @@ -9,12 +9,14 @@ Versatile OpenID Connect stack for .NET. -Note: this metapackage only references the generic core, server and validation packages. +Note: this metapackage only references the generic core, client, server and validation packages. To use these features on ASP.NET Core or OWIN/Katana/ASP.NET 4.x, reference the OpenIddict.AspNetCore or OpenIddict.Owin package. + + diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictRequestTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictRequestTests.cs index efe4b426..5fa90643 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictRequestTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictRequestTests.cs @@ -142,6 +142,13 @@ public class OpenIddictRequestTests /* value: */ new OpenIddictParameter("802A3E3E-DCCA-4EFC-89FA-7D82FE8C27E4") }; + yield return new object[] + { + /* property: */ nameof(OpenIddictRequest.IdToken), + /* name: */ Parameters.IdToken, + /* value: */ new OpenIddictParameter("802A3E3E-DCCA-4EFC-89FA-7D82FE8C27E4") + }; + yield return new object[] { /* property: */ nameof(OpenIddictRequest.IdTokenHint), diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs index fc8c684f..7035bb86 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs @@ -290,7 +290,7 @@ public class OpenIddictServerBuilderTests var options = GetOptions(services); // Assert - Assert.Single(options.EncryptionCredentials); + Assert.NotEmpty(options.EncryptionCredentials); Assert.Equal(SecurityAlgorithms.RsaOAEP, options.EncryptionCredentials[0].Alg); Assert.Equal(SecurityAlgorithms.Aes256CbcHmacSha512, options.EncryptionCredentials[0].Enc); Assert.NotNull(options.EncryptionCredentials[0].Key.KeyId); @@ -341,7 +341,7 @@ public class OpenIddictServerBuilderTests var options = GetOptions(services); // Assert - Assert.Single(options.SigningCredentials); + Assert.NotEmpty(options.SigningCredentials); Assert.Equal(SecurityAlgorithms.RsaSha256, options.SigningCredentials[0].Algorithm); Assert.NotNull(options.SigningCredentials[0].Kid); }