diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 000000000..d45bd5a1f --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,17 @@ +init: +- git config --global core.autocrlf true +branches: + only: + - master + - /^release\/.*$/ + - /^(.*\/)?ci-.*$/ +build_script: +- ps: .\run.ps1 default-build +clone_depth: 1 +environment: + global: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: 1 +test: 'off' +deploy: 'off' +os: Visual Studio 2017 diff --git a/.gitignore b/.gitignore index 0f91ad120..3d7e16e84 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ project.lock.json .build/ .testPublish/ /.vs/ +.vscode/ +global.json +BenchmarkDotNet.Artifacts/ diff --git a/.travis.yml b/.travis.yml index df22f7a88..ab3980055 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,26 +1,27 @@ language: csharp -sudo: required +sudo: false dist: trusty +env: + global: + - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + - DOTNET_CLI_TELEMETRY_OPTOUT: 1 +mono: none +os: +- linux +- osx +osx_image: xcode8.2 addons: apt: packages: - - gettext - - libcurl4-openssl-dev - - libicu-dev - - libssl-dev - libunwind8 - - zlib1g -mono: - - 4.0.5 -os: - - linux - - osx -osx_image: xcode7.1 branches: only: - - master - - release - - dev - - /^(.*\/)?ci-.*$/ + - master + - /^release\/.*$/ + - /^(.*\/)?ci-.*$/ +before_install: +- if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s + /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib + /usr/local/lib/; fi script: - - ./build.sh verify \ No newline at end of file +- ./build.sh diff --git a/.vsts-pipelines/builds/ci-internal.yml b/.vsts-pipelines/builds/ci-internal.yml new file mode 100644 index 000000000..c2c5336fd --- /dev/null +++ b/.vsts-pipelines/builds/ci-internal.yml @@ -0,0 +1,13 @@ +trigger: +- master +- release/* + +resources: + repositories: + - repository: buildtools + type: git + name: aspnet-BuildTools + ref: refs/heads/master + +phases: +- template: .vsts-pipelines/templates/project-ci.yml@buildtools diff --git a/.vsts-pipelines/builds/ci-public.yml b/.vsts-pipelines/builds/ci-public.yml new file mode 100644 index 000000000..507c89b02 --- /dev/null +++ b/.vsts-pipelines/builds/ci-public.yml @@ -0,0 +1,15 @@ +trigger: +- master +- release/* + +# See https://github.com/aspnet/BuildTools +resources: + repositories: + - repository: buildtools + type: github + endpoint: DotNet-Bot GitHub Connection + name: aspnet/BuildTools + ref: refs/heads/master + +phases: +- template: .vsts-pipelines/templates/project-ci.yml@buildtools diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64ff041d5..eac4268e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ Contributing ====== -Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/dev/CONTRIBUTING.md) in the Home repo. +Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/master/CONTRIBUTING.md) in the Home repo. diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..eb9d9535e --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,19 @@ + + + + + + + + + Microsoft ASP.NET Core + https://github.com/aspnet/Security + git + $(MSBuildThisFileDirectory) + $(MSBuildThisFileDirectory)build\Key.snk + true + true + + diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 000000000..7e3f8df92 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,6 @@ + + + $(MicrosoftNETCoreAppPackageVersion) + $(NETStandardLibrary20PackageVersion) + + diff --git a/LICENSE.txt b/LICENSE.txt index 0bdc1962b..b3b180cd5 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,12 +1,201 @@ -Copyright (c) .NET Foundation. All rights reserved. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -these files except in compliance with the License. You may obtain a copy of the -License at + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -http://www.apache.org/licenses/LICENSE-2.0 + 1. Definitions. -Unless required by applicable law or agreed to in writing, software distributed -under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied. See the License for the -specific language governing permissions and limitations under the License. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) .NET Foundation and Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NuGet.config b/NuGet.config index 5500f6d50..e32bddfd5 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,7 +1,7 @@  - - + + diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index 9f3414fbc..974eb365c 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -1,26 +1,13 @@ { - "adx": { // Packages written by the ADX team and that ship on NuGet.org - "rules": [ - "AdxVerificationCompositeRule" - ], - "packages": { - "Microsoft.AspNetCore.Authentication": { }, - "Microsoft.AspNetCore.Authentication.Cookies": { }, - "Microsoft.AspNetCore.Authentication.Facebook": { }, - "Microsoft.AspNetCore.Authentication.Google": { }, - "Microsoft.AspNetCore.Authentication.JwtBearer": { }, - "Microsoft.AspNetCore.Authentication.MicrosoftAccount": { }, - "Microsoft.AspNetCore.Authentication.OAuth": { }, - "Microsoft.AspNetCore.Authentication.OpenIdConnect": { }, - "Microsoft.AspNetCore.Authentication.Twitter": { }, - "Microsoft.AspNetCore.Authorization": { }, - "Microsoft.AspNetCore.CookiePolicy": { }, - "Microsoft.Owin.Security.Interop": { } - } - }, - "Default": { // Rules to run for packages not listed in any other set. - "rules": [ - "DefaultCompositeRule" - ] + "adx-nonshipping": { + "rules": [], + "packages": { + "Microsoft.AspNetCore.ChunkingCookieManager.Sources": {} } + }, + "Default": { + "rules": [ + "DefaultCompositeRule" + ] + } } \ No newline at end of file diff --git a/README.md b/README.md index f32798072..66c2aa45e 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -ASP.NET Security -======== +ASP.NET Security [Archived] +=========================== -AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/fujhh8n956v5ohfd/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/Security/branch/dev) - -Travis: [![Travis](https://travis-ci.org/aspnet/Security.svg?branch=dev)](https://travis-ci.org/aspnet/Security) +**This GitHub project has been archived.** Ongoing development on this project can be found in . Contains the security and authorization middlewares for ASP.NET Core. +A list of community projects related to authentication and security for ASP.NET Core are listed in the [documentation](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/community). + ### Notes -ASP.NET Security will not include Basic Authentication middleware due to its potential insecurity and performance problems. If you host under IIS you can enable it via IIS configuration. If you require Basic Authentication middleware for testing purposes, as a shared secret authentication mechanism for server to server communication, or to use a database as a user source then please look at the samples from [leastprivilege](https://github.com/leastprivilege/BasicAuthentication.AspNet5) or [Kukkimonsuta](https://github.com/Kukkimonsuta/Odachi/tree/master/src/Odachi.AspNetCore.Authentication.Basic). +ASP.NET Security will not include Basic Authentication middleware due to its potential insecurity and performance problems. If you host under IIS you can enable it via IIS configuration. -This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo. +This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [AspNetCore](https://github.com/aspnet/AspNetCore) repo. diff --git a/Security.sln b/Security.sln index 2ed873270..240100e8c 100644 --- a/Security.sln +++ b/Security.sln @@ -1,69 +1,97 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 -MinimumVisualStudioVersion = 10.0.40219.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2027 +MinimumVisualStudioVersion = 15.0.26730.03 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4D2B6A51-2F9F-44F5-8131-EA5CAC053652}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CookieSample", "samples\CookieSample\CookieSample.xproj", "{558C2C2A-AED8-49DE-BB60-D5F8AE06C714}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookieSample", "samples\CookieSample\CookieSample.csproj", "{558C2C2A-AED8-49DE-BB60-D5F8AE06C714}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7BF11F3A-60B6-4796-B504-579C67FFBA34}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C40A5A3B-ABA3-4819-9C44-D821E6DA1BA1}" ProjectSection(SolutionItems) = preProject - global.json = global.json + test\Directory.Build.props = test\Directory.Build.props EndProjectSection EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SocialSample", "samples\SocialSample\SocialSample.xproj", "{8C73D216-332D-41D8-BFD0-45BC4BC36552}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SocialSample", "samples\SocialSample\SocialSample.csproj", "{8C73D216-332D-41D8-BFD0-45BC4BC36552}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookieSessionSample", "samples\CookieSessionSample\CookieSessionSample.csproj", "{19711880-46DA-4A26-9E0F-9B2E41D27651}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIdConnectSample", "samples\OpenIdConnectSample\OpenIdConnectSample.csproj", "{BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Cookies", "src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.csproj", "{FC152CC4-054B-457E-8D91-389C5DE3C561}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication", "src\Microsoft.AspNetCore.Authentication\Microsoft.AspNetCore.Authentication.csproj", "{BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CookieSessionSample", "samples\CookieSessionSample\CookieSessionSample.xproj", "{19711880-46DA-4A26-9E0F-9B2E41D27651}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Facebook", "src\Microsoft.AspNetCore.Authentication.Facebook\Microsoft.AspNetCore.Authentication.Facebook.csproj", "{EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "OpenIdConnectSample", "samples\OpenIdConnectSample\OpenIdConnectSample.xproj", "{BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Google", "src\Microsoft.AspNetCore.Authentication.Google\Microsoft.AspNetCore.Authentication.Google.csproj", "{76579C39-B829-490D-B8BE-1BD35FE8412E}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Authentication.Cookies", "src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.xproj", "{FC152CC4-054B-457E-8D91-389C5DE3C561}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.OpenIdConnect", "src\Microsoft.AspNetCore.Authentication.OpenIdConnect\Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj", "{35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Authentication", "src\Microsoft.AspNetCore.Authentication\Microsoft.AspNetCore.Authentication.xproj", "{2286250A-52C8-4126-9F93-B1E45F0AD078}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.MicrosoftAccount", "src\Microsoft.AspNetCore.Authentication.MicrosoftAccount\Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj", "{ACB45E19-F520-4D0C-8916-B0CEB9C017FE}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Authentication.Facebook", "src\Microsoft.AspNetCore.Authentication.Facebook\Microsoft.AspNetCore.Authentication.Facebook.xproj", "{EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Twitter", "src\Microsoft.AspNetCore.Authentication.Twitter\Microsoft.AspNetCore.Authentication.Twitter.csproj", "{0330FFF6-B4B5-42DD-8C99-26A789569000}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Authentication.Google", "src\Microsoft.AspNetCore.Authentication.Google\Microsoft.AspNetCore.Authentication.Google.xproj", "{76579C39-B829-490D-B8BE-1BD35FE8412E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.OAuth", "src\Microsoft.AspNetCore.Authentication.OAuth\Microsoft.AspNetCore.Authentication.OAuth.csproj", "{1657C79E-7755-4AEE-9D61-571295B69A30}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Authentication.OpenIdConnect", "src\Microsoft.AspNetCore.Authentication.OpenIdConnect\Microsoft.AspNetCore.Authentication.OpenIdConnect.xproj", "{35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Test", "test\Microsoft.AspNetCore.Authentication.Test\Microsoft.AspNetCore.Authentication.Test.csproj", "{8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Authentication.MicrosoftAccount", "src\Microsoft.AspNetCore.Authentication.MicrosoftAccount\Microsoft.AspNetCore.Authentication.MicrosoftAccount.xproj", "{ACB45E19-F520-4D0C-8916-B0CEB9C017FE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authorization.Test", "test\Microsoft.AspNetCore.Authorization.Test\Microsoft.AspNetCore.Authorization.Test.csproj", "{7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Authentication.Twitter", "src\Microsoft.AspNetCore.Authentication.Twitter\Microsoft.AspNetCore.Authentication.Twitter.xproj", "{0330FFF6-B4B5-42DD-8C99-26A789569000}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authorization", "src\Microsoft.AspNetCore.Authorization\Microsoft.AspNetCore.Authorization.csproj", "{6AB3E514-5894-4131-9399-DC7D5284ADDB}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Authentication.OAuth", "src\Microsoft.AspNetCore.Authentication.OAuth\Microsoft.AspNetCore.Authentication.OAuth.xproj", "{1657C79E-7755-4AEE-9D61-571295B69A30}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.CookiePolicy", "src\Microsoft.AspNetCore.CookiePolicy\Microsoft.AspNetCore.CookiePolicy.csproj", "{86183DC3-02A8-4A68-8B60-71ECEC066E79}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Authentication.Test", "test\Microsoft.AspNetCore.Authentication.Test\Microsoft.AspNetCore.Authentication.Test.xproj", "{8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.CookiePolicy.Test", "test\Microsoft.AspNetCore.CookiePolicy.Test\Microsoft.AspNetCore.CookiePolicy.Test.csproj", "{1790E052-646F-4529-B90E-6FEA95520D69}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Authorization.Test", "test\Microsoft.AspNetCore.Authorization.Test\Microsoft.AspNetCore.Authorization.Test.xproj", "{7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.JwtBearer", "src\Microsoft.AspNetCore.Authentication.JwtBearer\Microsoft.AspNetCore.Authentication.JwtBearer.csproj", "{2755BFE5-7421-4A31-A644-F817DF5CAA98}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Authorization", "src\Microsoft.AspNetCore.Authorization\Microsoft.AspNetCore.Authorization.xproj", "{6AB3E514-5894-4131-9399-DC7D5284ADDB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JwtBearerSample", "samples\JwtBearerSample\JwtBearerSample.csproj", "{D399B84F-591B-4E98-92BA-B0F63E7B6957}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.CookiePolicy", "src\Microsoft.AspNetCore.CookiePolicy\Microsoft.AspNetCore.CookiePolicy.xproj", "{86183DC3-02A8-4A68-8B60-71ECEC066E79}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Owin.Security.Interop", "src\Microsoft.Owin.Security.Interop\Microsoft.Owin.Security.Interop.csproj", "{A7922DD8-09F1-43E4-938B-CC523EA08898}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.CookiePolicy.Test", "test\Microsoft.AspNetCore.CookiePolicy.Test\Microsoft.AspNetCore.CookiePolicy.Test.xproj", "{1790E052-646F-4529-B90E-6FEA95520D69}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Owin.Security.Interop.Test", "test\Microsoft.Owin.Security.Interop.Test\Microsoft.Owin.Security.Interop.Test.csproj", "{A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Authentication.JwtBearer", "src\Microsoft.AspNetCore.Authentication.JwtBearer\Microsoft.AspNetCore.Authentication.JwtBearer.xproj", "{2755BFE5-7421-4A31-A644-F817DF5CAA98}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIdConnect.AzureAdSample", "samples\OpenIdConnect.AzureAdSample\OpenIdConnect.AzureAdSample.csproj", "{3A7AD414-EBDE-4F92-B307-4E8F19B6117E}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "JwtBearerSample", "samples\JwtBearerSample\JwtBearerSample.xproj", "{D399B84F-591B-4E98-92BA-B0F63E7B6957}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test", "test\Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test\Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj", "{51563775-C659-4907-9BAF-9995BAB87D01}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Owin.Security.Interop", "src\Microsoft.Owin.Security.Interop\Microsoft.Owin.Security.Interop.xproj", "{A7922DD8-09F1-43E4-938B-CC523EA08898}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{86BD08B1-F978-4F58-9982-2A017807F01C}" + ProjectSection(SolutionItems) = preProject + build\dependencies.props = build\dependencies.props + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + build\Key.snk = build\Key.snk + NuGet.config = NuGet.config + build\repo.props = build\repo.props + build\sources.props = build\sources.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authorization.Policy", "src\Microsoft.AspNetCore.Authorization.Policy\Microsoft.AspNetCore.Authorization.Policy.csproj", "{58194599-F07D-47A3-9DF2-E21A22C5EF9E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookiePolicySample", "samples\CookiePolicySample\CookiePolicySample.csproj", "{24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Owin.Security.Interop.Test", "test\Microsoft.Owin.Security.Interop.Test\Microsoft.Owin.Security.Interop.Test.xproj", "{A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.WsFederation", "src\Microsoft.AspNetCore.Authentication.WsFederation\Microsoft.AspNetCore.Authentication.WsFederation.csproj", "{B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "OpenIdConnect.AzureAdSample", "samples\OpenIdConnect.AzureAdSample\OpenIdConnect.AzureAdSample.xproj", "{3A7AD414-EBDE-4F92-B307-4E8F19B6117E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WsFedSample", "samples\WsFedSample\WsFedSample.csproj", "{5EC2E398-E46A-430D-8E4B-E91C8FC3E800}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{55052FE3-F8C2-4E6C-97B0-C02ED1DBEA62}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Security.Performance", "benchmarks\Microsoft.AspNetCore.Security.Performance\Microsoft.AspNetCore.Security.Performance.csproj", "{556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|Mixed Platforms = Release|Mixed Platforms + Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution @@ -71,260 +99,444 @@ Global {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Any CPU.Build.0 = Debug|Any CPU {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|x64.ActiveCfg = Debug|Any CPU + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|x64.Build.0 = Debug|Any CPU {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|x86.ActiveCfg = Debug|Any CPU {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Any CPU.ActiveCfg = Release|Any CPU {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Any CPU.Build.0 = Release|Any CPU {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|x64.ActiveCfg = Release|Any CPU + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|x64.Build.0 = Release|Any CPU {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|x86.ActiveCfg = Release|Any CPU {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|x64.ActiveCfg = Debug|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|x64.Build.0 = Debug|Any CPU {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|x86.ActiveCfg = Debug|Any CPU {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Any CPU.Build.0 = Release|Any CPU {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|x64.ActiveCfg = Release|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|x64.Build.0 = Release|Any CPU {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|x86.ActiveCfg = Release|Any CPU {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Any CPU.Build.0 = Debug|Any CPU {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|x64.ActiveCfg = Debug|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|x64.Build.0 = Debug|Any CPU {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|x86.ActiveCfg = Debug|Any CPU {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Any CPU.ActiveCfg = Release|Any CPU {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Any CPU.Build.0 = Release|Any CPU {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|x64.ActiveCfg = Release|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|x64.Build.0 = Release|Any CPU {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|x86.ActiveCfg = Release|Any CPU {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|Any CPU.Build.0 = Debug|Any CPU {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|x64.ActiveCfg = Debug|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|x64.Build.0 = Debug|Any CPU {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|x86.ActiveCfg = Debug|Any CPU {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|x86.Build.0 = Debug|Any CPU {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|Any CPU.ActiveCfg = Release|Any CPU {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|Any CPU.Build.0 = Release|Any CPU {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|x64.ActiveCfg = Release|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|x64.Build.0 = Release|Any CPU {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|x86.ActiveCfg = Release|Any CPU {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|x86.Build.0 = Release|Any CPU {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|x64.ActiveCfg = Debug|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|x64.Build.0 = Debug|Any CPU {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|x86.ActiveCfg = Debug|Any CPU {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|x86.Build.0 = Debug|Any CPU {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|Any CPU.Build.0 = Release|Any CPU {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|x64.ActiveCfg = Release|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|x64.Build.0 = Release|Any CPU {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|x86.ActiveCfg = Release|Any CPU {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|x86.Build.0 = Release|Any CPU - {2286250A-52C8-4126-9F93-B1E45F0AD078}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2286250A-52C8-4126-9F93-B1E45F0AD078}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2286250A-52C8-4126-9F93-B1E45F0AD078}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {2286250A-52C8-4126-9F93-B1E45F0AD078}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {2286250A-52C8-4126-9F93-B1E45F0AD078}.Debug|x86.ActiveCfg = Debug|Any CPU - {2286250A-52C8-4126-9F93-B1E45F0AD078}.Debug|x86.Build.0 = Debug|Any CPU - {2286250A-52C8-4126-9F93-B1E45F0AD078}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2286250A-52C8-4126-9F93-B1E45F0AD078}.Release|Any CPU.Build.0 = Release|Any CPU - {2286250A-52C8-4126-9F93-B1E45F0AD078}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {2286250A-52C8-4126-9F93-B1E45F0AD078}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {2286250A-52C8-4126-9F93-B1E45F0AD078}.Release|x86.ActiveCfg = Release|Any CPU - {2286250A-52C8-4126-9F93-B1E45F0AD078}.Release|x86.Build.0 = Release|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|x64.ActiveCfg = Debug|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|x64.Build.0 = Debug|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|x86.ActiveCfg = Debug|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Debug|x86.Build.0 = Debug|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|Any CPU.Build.0 = Release|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|x64.ActiveCfg = Release|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|x64.Build.0 = Release|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|x86.ActiveCfg = Release|Any CPU + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB}.Release|x86.Build.0 = Release|Any CPU {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|Any CPU.Build.0 = Debug|Any CPU {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|x64.ActiveCfg = Debug|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|x64.Build.0 = Debug|Any CPU {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|x86.ActiveCfg = Debug|Any CPU {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|x86.Build.0 = Debug|Any CPU {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|Any CPU.ActiveCfg = Release|Any CPU {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|Any CPU.Build.0 = Release|Any CPU {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|x64.ActiveCfg = Release|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|x64.Build.0 = Release|Any CPU {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|x86.ActiveCfg = Release|Any CPU {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|x86.Build.0 = Release|Any CPU {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|Any CPU.Build.0 = Debug|Any CPU {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|x64.ActiveCfg = Debug|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|x64.Build.0 = Debug|Any CPU {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|x86.ActiveCfg = Debug|Any CPU {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|x86.Build.0 = Debug|Any CPU {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|Any CPU.ActiveCfg = Release|Any CPU {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|Any CPU.Build.0 = Release|Any CPU {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|x64.ActiveCfg = Release|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|x64.Build.0 = Release|Any CPU {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|x86.ActiveCfg = Release|Any CPU {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|x86.Build.0 = Release|Any CPU {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|Any CPU.Build.0 = Debug|Any CPU {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|x64.ActiveCfg = Debug|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|x64.Build.0 = Debug|Any CPU {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|x86.ActiveCfg = Debug|Any CPU {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|x86.Build.0 = Debug|Any CPU {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|Any CPU.ActiveCfg = Release|Any CPU {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|Any CPU.Build.0 = Release|Any CPU {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|x64.ActiveCfg = Release|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|x64.Build.0 = Release|Any CPU {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|x86.ActiveCfg = Release|Any CPU {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|x86.Build.0 = Release|Any CPU {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|x64.ActiveCfg = Debug|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|x64.Build.0 = Debug|Any CPU {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|x86.ActiveCfg = Debug|Any CPU {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|x86.Build.0 = Debug|Any CPU {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|Any CPU.Build.0 = Release|Any CPU {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|x64.ActiveCfg = Release|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|x64.Build.0 = Release|Any CPU {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|x86.ActiveCfg = Release|Any CPU {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|x86.Build.0 = Release|Any CPU {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|Any CPU.Build.0 = Debug|Any CPU {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|x64.ActiveCfg = Debug|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|x64.Build.0 = Debug|Any CPU {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|x86.ActiveCfg = Debug|Any CPU {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|x86.Build.0 = Debug|Any CPU {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|Any CPU.ActiveCfg = Release|Any CPU {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|Any CPU.Build.0 = Release|Any CPU {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|x64.ActiveCfg = Release|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|x64.Build.0 = Release|Any CPU {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|x86.ActiveCfg = Release|Any CPU {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|x86.Build.0 = Release|Any CPU {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|Any CPU.Build.0 = Debug|Any CPU {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|x64.ActiveCfg = Debug|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|x64.Build.0 = Debug|Any CPU {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|x86.ActiveCfg = Debug|Any CPU {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|x86.Build.0 = Debug|Any CPU {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|Any CPU.ActiveCfg = Release|Any CPU {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|Any CPU.Build.0 = Release|Any CPU {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|x64.ActiveCfg = Release|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|x64.Build.0 = Release|Any CPU {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|x86.ActiveCfg = Release|Any CPU {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|x86.Build.0 = Release|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Any CPU.Build.0 = Debug|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x64.ActiveCfg = Debug|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x64.Build.0 = Debug|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x86.ActiveCfg = Debug|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x86.Build.0 = Debug|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Any CPU.ActiveCfg = Release|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Any CPU.Build.0 = Release|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x64.ActiveCfg = Release|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x64.Build.0 = Release|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x86.ActiveCfg = Release|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x86.Build.0 = Release|Any CPU {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|x64.ActiveCfg = Debug|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|x64.Build.0 = Debug|Any CPU {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|x86.ActiveCfg = Debug|Any CPU {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|x86.Build.0 = Debug|Any CPU {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|Any CPU.Build.0 = Release|Any CPU {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|x64.ActiveCfg = Release|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|x64.Build.0 = Release|Any CPU {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|x86.ActiveCfg = Release|Any CPU {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|x86.Build.0 = Release|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|Any CPU.Build.0 = Debug|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|x64.ActiveCfg = Debug|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|x64.Build.0 = Debug|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|x86.ActiveCfg = Debug|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|x86.Build.0 = Debug|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|Any CPU.ActiveCfg = Release|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|Any CPU.Build.0 = Release|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|x64.ActiveCfg = Release|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|x64.Build.0 = Release|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|x86.ActiveCfg = Release|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|x86.Build.0 = Release|Any CPU {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|Any CPU.Build.0 = Debug|Any CPU {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|x64.ActiveCfg = Debug|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|x64.Build.0 = Debug|Any CPU {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|x86.ActiveCfg = Debug|Any CPU {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|x86.Build.0 = Debug|Any CPU {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|Any CPU.ActiveCfg = Release|Any CPU {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|Any CPU.Build.0 = Release|Any CPU {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|x64.ActiveCfg = Release|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|x64.Build.0 = Release|Any CPU {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|x86.ActiveCfg = Release|Any CPU {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|x86.Build.0 = Release|Any CPU {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|Any CPU.Build.0 = Debug|Any CPU {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|x64.ActiveCfg = Debug|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|x64.Build.0 = Debug|Any CPU {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|x86.ActiveCfg = Debug|Any CPU {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|x86.Build.0 = Debug|Any CPU {1790E052-646F-4529-B90E-6FEA95520D69}.Release|Any CPU.ActiveCfg = Release|Any CPU {1790E052-646F-4529-B90E-6FEA95520D69}.Release|Any CPU.Build.0 = Release|Any CPU {1790E052-646F-4529-B90E-6FEA95520D69}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {1790E052-646F-4529-B90E-6FEA95520D69}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Release|x64.ActiveCfg = Release|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Release|x64.Build.0 = Release|Any CPU {1790E052-646F-4529-B90E-6FEA95520D69}.Release|x86.ActiveCfg = Release|Any CPU {1790E052-646F-4529-B90E-6FEA95520D69}.Release|x86.Build.0 = Release|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|Any CPU.Build.0 = Debug|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|x64.ActiveCfg = Debug|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|x64.Build.0 = Debug|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|x86.ActiveCfg = Debug|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|x86.Build.0 = Debug|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|Any CPU.ActiveCfg = Release|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|Any CPU.Build.0 = Release|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|x64.ActiveCfg = Release|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|x64.Build.0 = Release|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|x86.ActiveCfg = Release|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|x86.Build.0 = Release|Any CPU {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|Any CPU.Build.0 = Debug|Any CPU {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|x64.ActiveCfg = Debug|Any CPU + {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|x64.Build.0 = Debug|Any CPU {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|x86.ActiveCfg = Debug|Any CPU {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Debug|x86.Build.0 = Debug|Any CPU {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|Any CPU.ActiveCfg = Release|Any CPU {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|Any CPU.Build.0 = Release|Any CPU {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|x64.ActiveCfg = Release|Any CPU + {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|x64.Build.0 = Release|Any CPU {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|x86.ActiveCfg = Release|Any CPU {D399B84F-591B-4E98-92BA-B0F63E7B6957}.Release|x86.Build.0 = Release|Any CPU {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|Any CPU.Build.0 = Debug|Any CPU {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|x64.Build.0 = Debug|Any CPU {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|x86.ActiveCfg = Debug|Any CPU {A7922DD8-09F1-43E4-938B-CC523EA08898}.Debug|x86.Build.0 = Debug|Any CPU {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|Any CPU.ActiveCfg = Release|Any CPU {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|Any CPU.Build.0 = Release|Any CPU {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|x64.ActiveCfg = Release|Any CPU + {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|x64.Build.0 = Release|Any CPU {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|x86.ActiveCfg = Release|Any CPU {A7922DD8-09F1-43E4-938B-CC523EA08898}.Release|x86.Build.0 = Release|Any CPU {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|Any CPU.Build.0 = Debug|Any CPU {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|x64.Build.0 = Debug|Any CPU {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|x86.ActiveCfg = Debug|Any CPU {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Debug|x86.Build.0 = Debug|Any CPU {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|Any CPU.ActiveCfg = Release|Any CPU {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|Any CPU.Build.0 = Release|Any CPU {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|x64.ActiveCfg = Release|Any CPU + {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|x64.Build.0 = Release|Any CPU {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|x86.ActiveCfg = Release|Any CPU {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24}.Release|x86.Build.0 = Release|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|x64.ActiveCfg = Debug|Any CPU + {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|x64.Build.0 = Debug|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|x86.ActiveCfg = Debug|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Debug|x86.Build.0 = Debug|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|Any CPU.Build.0 = Release|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|x64.ActiveCfg = Release|Any CPU + {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|x64.Build.0 = Release|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|x86.ActiveCfg = Release|Any CPU {3A7AD414-EBDE-4F92-B307-4E8F19B6117E}.Release|x86.Build.0 = Release|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|x64.ActiveCfg = Debug|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|x64.Build.0 = Debug|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|x86.ActiveCfg = Debug|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Debug|x86.Build.0 = Debug|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Release|Any CPU.Build.0 = Release|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Release|x64.ActiveCfg = Release|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Release|x64.Build.0 = Release|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Release|x86.ActiveCfg = Release|Any CPU + {51563775-C659-4907-9BAF-9995BAB87D01}.Release|x86.Build.0 = Release|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|x64.ActiveCfg = Debug|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|x64.Build.0 = Debug|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|x86.ActiveCfg = Debug|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Debug|x86.Build.0 = Debug|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|Any CPU.Build.0 = Release|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|x64.ActiveCfg = Release|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|x64.Build.0 = Release|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|x86.ActiveCfg = Release|Any CPU + {58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|x86.Build.0 = Release|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|x64.ActiveCfg = Debug|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|x64.Build.0 = Debug|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|x86.ActiveCfg = Debug|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|x86.Build.0 = Debug|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|Any CPU.Build.0 = Release|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|x64.ActiveCfg = Release|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|x64.Build.0 = Release|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|x86.ActiveCfg = Release|Any CPU + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|x86.Build.0 = Release|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|x64.Build.0 = Debug|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Debug|x86.Build.0 = Debug|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|Any CPU.Build.0 = Release|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|x64.ActiveCfg = Release|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|x64.Build.0 = Release|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|x86.ActiveCfg = Release|Any CPU + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}.Release|x86.Build.0 = Release|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|x64.ActiveCfg = Debug|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|x64.Build.0 = Debug|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|x86.ActiveCfg = Debug|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Debug|x86.Build.0 = Debug|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|Any CPU.Build.0 = Release|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|x64.ActiveCfg = Release|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|x64.Build.0 = Release|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|x86.ActiveCfg = Release|Any CPU + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800}.Release|x86.Build.0 = Release|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Debug|x64.ActiveCfg = Debug|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Debug|x64.Build.0 = Debug|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Debug|x86.ActiveCfg = Debug|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Debug|x86.Build.0 = Debug|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Release|Any CPU.Build.0 = Release|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Release|x64.ActiveCfg = Release|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Release|x64.Build.0 = Release|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Release|x86.ActiveCfg = Release|Any CPU + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -335,7 +547,7 @@ Global {19711880-46DA-4A26-9E0F-9B2E41D27651} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} {FC152CC4-054B-457E-8D91-389C5DE3C561} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} - {2286250A-52C8-4126-9F93-B1E45F0AD078} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {BC0D4B56-1A5B-4D88-AFBF-68C0F2D545FB} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {76579C39-B829-490D-B8BE-1BD35FE8412E} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} @@ -352,5 +564,14 @@ Global {A7922DD8-09F1-43E4-938B-CC523EA08898} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {A2B5DC39-68D5-4145-A8CC-6AEAB7D33A24} = {7BF11F3A-60B6-4796-B504-579C67FFBA34} {3A7AD414-EBDE-4F92-B307-4E8F19B6117E} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} + {51563775-C659-4907-9BAF-9995BAB87D01} = {7BF11F3A-60B6-4796-B504-579C67FFBA34} + {58194599-F07D-47A3-9DF2-E21A22C5EF9E} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} + {B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {5EC2E398-E46A-430D-8E4B-E91C8FC3E800} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} + {556C4FAA-F4B1-4EA9-8921-CB1DF7D94C2A} = {55052FE3-F8C2-4E6C-97B0-C02ED1DBEA62} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {ABF8089E-43D0-4010-84A7-7A9DCFE49357} EndGlobalSection EndGlobal diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index b9a9bcd1e..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,13 +0,0 @@ -init: - - git config --global core.autocrlf true -branches: - only: - - master - - release - - dev - - /^(.*\/)?ci-.*$/ -build_script: - - build.cmd verify -clone_depth: 1 -test: off -deploy: off \ No newline at end of file diff --git a/benchmarks/Microsoft.AspNetCore.Security.Performance/AuthorizationMiddlewareBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Security.Performance/AuthorizationMiddlewareBenchmark.cs new file mode 100644 index 000000000..90cb135de --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Security.Performance/AuthorizationMiddlewareBenchmark.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Security +{ + public class AuthorizationMiddlewareBenchmark + { + private AuthorizationMiddleware _authorizationMiddleware; + private DefaultHttpContext _httpContextNoEndpoint; + private DefaultHttpContext _httpContextHasEndpoint; + + [GlobalSetup] + public void Setup() + { + var policyProvider = new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions())); + _authorizationMiddleware = new AuthorizationMiddleware((context) => Task.CompletedTask, policyProvider); + + _httpContextNoEndpoint = new DefaultHttpContext(); + + var feature = new EndpointFeature + { + Endpoint = new Endpoint((context) => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint") + }; + _httpContextHasEndpoint = new DefaultHttpContext(); + _httpContextHasEndpoint.Features.Set(feature); + } + + [Benchmark] + public Task Invoke_NoEndpoint_NoAuthorization() + { + return _authorizationMiddleware.Invoke(_httpContextNoEndpoint); + } + + [Benchmark] + public Task Invoke_HasEndpoint_NoAuthorization() + { + return _authorizationMiddleware.Invoke(_httpContextHasEndpoint); + } + + private class EndpointFeature : IEndpointFeature + { + public Endpoint Endpoint { get; set; } + } + } +} diff --git a/benchmarks/Microsoft.AspNetCore.Security.Performance/AuthorizationPolicyBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Security.Performance/AuthorizationPolicyBenchmark.cs new file mode 100644 index 000000000..86afdcd5a --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Security.Performance/AuthorizationPolicyBenchmark.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Security +{ + public class AuthorizationPolicyBenchmark + { + private DefaultAuthorizationPolicyProvider _policyProvider; + + [GlobalSetup] + public void Setup() + { + _policyProvider = new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions())); + } + + [Benchmark] + public Task CombineAsync() + { + return AuthorizationPolicy.CombineAsync(_policyProvider, Array.Empty()); + } + } +} diff --git a/benchmarks/Microsoft.AspNetCore.Security.Performance/Microsoft.AspNetCore.Security.Performance.csproj b/benchmarks/Microsoft.AspNetCore.Security.Performance/Microsoft.AspNetCore.Security.Performance.csproj new file mode 100644 index 000000000..2f16dd669 --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Security.Performance/Microsoft.AspNetCore.Security.Performance.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp3.0 + Exe + true + true + false + Microsoft.AspNetCore.Security + + + + + + + + + + + + + + + diff --git a/benchmarks/Microsoft.AspNetCore.Security.Performance/Properties/AssemblyInfo.cs b/benchmarks/Microsoft.AspNetCore.Security.Performance/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..cfb0b19c2 --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Security.Performance/Properties/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark] \ No newline at end of file diff --git a/benchmarks/Microsoft.AspNetCore.Security.Performance/readme.md b/benchmarks/Microsoft.AspNetCore.Security.Performance/readme.md new file mode 100644 index 000000000..cbe60261b --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Security.Performance/readme.md @@ -0,0 +1,16 @@ +Compile the solution in Release mode (so binaries are available in release) + +To run a specific benchmark add it as parameter. +``` +dotnet run -c Release --framework +``` + +To run all benchmarks use '*' as the name. +``` +dotnet run -c Release --framework * +``` + +If you run without any parameters, you'll be offered the list of all benchmarks and get to choose. +``` +dotnet run -c Release --framework +``` \ No newline at end of file diff --git a/build.cmd b/build.cmd index 7d4894cb4..c0050bda1 100644 --- a/build.cmd +++ b/build.cmd @@ -1,2 +1,2 @@ @ECHO OFF -PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0build.ps1' %*; exit $LASTEXITCODE" \ No newline at end of file +PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' default-build %*; exit $LASTEXITCODE" diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index 8f2f99691..000000000 --- a/build.ps1 +++ /dev/null @@ -1,67 +0,0 @@ -$ErrorActionPreference = "Stop" - -function DownloadWithRetry([string] $url, [string] $downloadLocation, [int] $retries) -{ - while($true) - { - try - { - Invoke-WebRequest $url -OutFile $downloadLocation - break - } - catch - { - $exceptionMessage = $_.Exception.Message - Write-Host "Failed to download '$url': $exceptionMessage" - if ($retries -gt 0) { - $retries-- - Write-Host "Waiting 10 seconds before retrying. Retries left: $retries" - Start-Sleep -Seconds 10 - - } - else - { - $exception = $_.Exception - throw $exception - } - } - } -} - -cd $PSScriptRoot - -$repoFolder = $PSScriptRoot -$env:REPO_FOLDER = $repoFolder - -$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" -if ($env:KOREBUILD_ZIP) -{ - $koreBuildZip=$env:KOREBUILD_ZIP -} - -$buildFolder = ".build" -$buildFile="$buildFolder\KoreBuild.ps1" - -if (!(Test-Path $buildFolder)) { - Write-Host "Downloading KoreBuild from $koreBuildZip" - - $tempFolder=$env:TEMP + "\KoreBuild-" + [guid]::NewGuid() - New-Item -Path "$tempFolder" -Type directory | Out-Null - - $localZipFile="$tempFolder\korebuild.zip" - - DownloadWithRetry -url $koreBuildZip -downloadLocation $localZipFile -retries 6 - - Add-Type -AssemblyName System.IO.Compression.FileSystem - [System.IO.Compression.ZipFile]::ExtractToDirectory($localZipFile, $tempFolder) - - New-Item -Path "$buildFolder" -Type directory | Out-Null - copy-item "$tempFolder\**\build\*" $buildFolder -Recurse - - # Cleanup - if (Test-Path $tempFolder) { - Remove-Item -Recurse -Force $tempFolder - } -} - -&"$buildFile" $args \ No newline at end of file diff --git a/build.sh b/build.sh index f4208100e..98a4b2276 100755 --- a/build.sh +++ b/build.sh @@ -1,46 +1,8 @@ #!/usr/bin/env bash -repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd $repoFolder -koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" -if [ ! -z $KOREBUILD_ZIP ]; then - koreBuildZip=$KOREBUILD_ZIP -fi +set -euo pipefail +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -buildFolder=".build" -buildFile="$buildFolder/KoreBuild.sh" - -if test ! -d $buildFolder; then - echo "Downloading KoreBuild from $koreBuildZip" - - tempFolder="/tmp/KoreBuild-$(uuidgen)" - mkdir $tempFolder - - localZipFile="$tempFolder/korebuild.zip" - - retries=6 - until (wget -O $localZipFile $koreBuildZip 2>/dev/null || curl -o $localZipFile --location $koreBuildZip 2>/dev/null) - do - echo "Failed to download '$koreBuildZip'" - if [ "$retries" -le 0 ]; then - exit 1 - fi - retries=$((retries - 1)) - echo "Waiting 10 seconds before retrying. Retries left: $retries" - sleep 10s - done - - unzip -q -d $tempFolder $localZipFile - - mkdir $buildFolder - cp -r $tempFolder/**/build/** $buildFolder - - chmod +x $buildFile - - # Cleanup - if test ! -d $tempFolder; then - rm -rf $tempFolder - fi -fi - -$buildFile -r $repoFolder "$@" \ No newline at end of file +# Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs) +chmod +x "$DIR/run.sh"; sync +"$DIR/run.sh" default-build "$@" diff --git a/tools/Key.snk b/build/Key.snk similarity index 100% rename from tools/Key.snk rename to build/Key.snk diff --git a/build/dependencies.props b/build/dependencies.props new file mode 100644 index 000000000..163a3d400 --- /dev/null +++ b/build/dependencies.props @@ -0,0 +1,56 @@ + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + 0.10.13 + 3.0.0-build-20181114.5 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-preview-181113-11 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.19.8 + 5.3.0 + 5.3.0 + 3.0.0-preview1-26907-05 + 15.6.1 + 3.0.1 + 3.0.1 + 3.0.1 + 4.10.0 + 2.0.3 + 11.0.2 + 5.3.0 + 0.10.0 + 2.3.1 + 2.4.0 + + + + + diff --git a/build/repo.props b/build/repo.props new file mode 100644 index 000000000..4402da1d0 --- /dev/null +++ b/build/repo.props @@ -0,0 +1,13 @@ + + + + + + Internal.AspNetCore.Universe.Lineup + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json + + + + + + diff --git a/build/sources.props b/build/sources.props new file mode 100644 index 000000000..9215df975 --- /dev/null +++ b/build/sources.props @@ -0,0 +1,17 @@ + + + + + $(DotNetRestoreSources) + + $(RestoreSources); + https://dotnet.myget.org/F/dotnet-core/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; + + + $(RestoreSources); + https://api.nuget.org/v3/index.json; + + + diff --git a/global.json b/global.json deleted file mode 100644 index 983ba0401..000000000 --- a/global.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "projects": ["src"] -} diff --git a/korebuild-lock.txt b/korebuild-lock.txt new file mode 100644 index 000000000..73613543d --- /dev/null +++ b/korebuild-lock.txt @@ -0,0 +1,2 @@ +version:3.0.0-build-20181114.5 +commithash:880e9a204d4ee4a18dfd83c9fb05a192a28bca60 diff --git a/korebuild.json b/korebuild.json new file mode 100644 index 000000000..8a276a7f3 --- /dev/null +++ b/korebuild.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", + "channel": "master" +} diff --git a/run.cmd b/run.cmd new file mode 100644 index 000000000..d52d5c7e6 --- /dev/null +++ b/run.cmd @@ -0,0 +1,2 @@ +@ECHO OFF +PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' %*; exit $LASTEXITCODE" diff --git a/run.ps1 b/run.ps1 new file mode 100644 index 000000000..34604c717 --- /dev/null +++ b/run.ps1 @@ -0,0 +1,209 @@ +#!/usr/bin/env powershell +#requires -version 4 + +<# +.SYNOPSIS +Executes KoreBuild commands. + +.DESCRIPTION +Downloads korebuild if required. Then executes the KoreBuild command. To see available commands, execute with `-Command help`. + +.PARAMETER Command +The KoreBuild command to run. + +.PARAMETER Path +The folder to build. Defaults to the folder containing this script. + +.PARAMETER Channel +The channel of KoreBuild to download. Overrides the value from the config file. + +.PARAMETER DotNetHome +The directory where .NET Core tools will be stored. + +.PARAMETER ToolsSource +The base url where build tools can be downloaded. Overrides the value from the config file. + +.PARAMETER Update +Updates KoreBuild to the latest version even if a lock file is present. + +.PARAMETER Reinstall +Re-installs KoreBuild + +.PARAMETER ConfigFile +The path to the configuration file that stores values. Defaults to korebuild.json. + +.PARAMETER ToolsSourceSuffix +The Suffix to append to the end of the ToolsSource. Useful for query strings in blob stores. + +.PARAMETER CI +Sets up CI specific settings and variables. + +.PARAMETER Arguments +Arguments to be passed to the command + +.NOTES +This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be. +When the lockfile is not present, KoreBuild will create one using latest available version from $Channel. + +The $ConfigFile is expected to be an JSON file. It is optional, and the configuration values in it are optional as well. Any options set +in the file are overridden by command line parameters. + +.EXAMPLE +Example config file: +```json +{ + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", + "channel": "master", + "toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools" +} +``` +#> +[CmdletBinding(PositionalBinding = $false)] +param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$Command, + [string]$Path = $PSScriptRoot, + [Alias('c')] + [string]$Channel, + [Alias('d')] + [string]$DotNetHome, + [Alias('s')] + [string]$ToolsSource, + [Alias('u')] + [switch]$Update, + [switch]$Reinstall, + [string]$ToolsSourceSuffix, + [string]$ConfigFile = $null, + [switch]$CI, + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$Arguments +) + +Set-StrictMode -Version 2 +$ErrorActionPreference = 'Stop' + +# +# Functions +# + +function Get-KoreBuild { + + $lockFile = Join-Path $Path 'korebuild-lock.txt' + + if (!(Test-Path $lockFile) -or $Update) { + Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile $ToolsSourceSuffix + } + + $version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1 + if (!$version) { + Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'" + } + $version = $version.TrimStart('version:').Trim() + $korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version) + + if ($Reinstall -and (Test-Path $korebuildPath)) { + Remove-Item -Force -Recurse $korebuildPath + } + + if (!(Test-Path $korebuildPath)) { + Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version" + New-Item -ItemType Directory -Path $korebuildPath | Out-Null + $remotePath = "$ToolsSource/korebuild/artifacts/$version/korebuild.$version.zip" + + try { + $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip" + Get-RemoteFile $remotePath $tmpfile $ToolsSourceSuffix + if (Get-Command -Name 'Microsoft.PowerShell.Archive\Expand-Archive' -ErrorAction Ignore) { + # Use built-in commands where possible as they are cross-plat compatible + Microsoft.PowerShell.Archive\Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath + } + else { + # Fallback to old approach for old installations of PowerShell + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory($tmpfile, $korebuildPath) + } + } + catch { + Remove-Item -Recurse -Force $korebuildPath -ErrorAction Ignore + throw + } + finally { + Remove-Item $tmpfile -ErrorAction Ignore + } + } + + return $korebuildPath +} + +function Join-Paths([string]$path, [string[]]$childPaths) { + $childPaths | ForEach-Object { $path = Join-Path $path $_ } + return $path +} + +function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$RemoteSuffix) { + if ($RemotePath -notlike 'http*') { + Copy-Item $RemotePath $LocalPath + return + } + + $retries = 10 + while ($retries -gt 0) { + $retries -= 1 + try { + Invoke-WebRequest -UseBasicParsing -Uri $($RemotePath + $RemoteSuffix) -OutFile $LocalPath + return + } + catch { + Write-Verbose "Request failed. $retries retries remaining" + } + } + + Write-Error "Download failed: '$RemotePath'." +} + +# +# Main +# + +# Load configuration or set defaults + +$Path = Resolve-Path $Path +if (!$ConfigFile) { $ConfigFile = Join-Path $Path 'korebuild.json' } + +if (Test-Path $ConfigFile) { + try { + $config = Get-Content -Raw -Encoding UTF8 -Path $ConfigFile | ConvertFrom-Json + if ($config) { + if (!($Channel) -and (Get-Member -Name 'channel' -InputObject $config)) { [string] $Channel = $config.channel } + if (!($ToolsSource) -and (Get-Member -Name 'toolsSource' -InputObject $config)) { [string] $ToolsSource = $config.toolsSource} + } + } + catch { + Write-Host -ForegroundColor Red $Error[0] + Write-Error "$ConfigFile contains invalid JSON." + exit 1 + } +} + +if (!$DotNetHome) { + $DotNetHome = if ($env:DOTNET_HOME) { $env:DOTNET_HOME } ` + elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet'} ` + elseif ($env:HOME) {Join-Path $env:HOME '.dotnet'}` + else { Join-Path $PSScriptRoot '.dotnet'} +} + +if (!$Channel) { $Channel = 'master' } +if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' } + +# Execute + +$korebuildPath = Get-KoreBuild +Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') + +try { + Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile -CI:$CI + Invoke-KoreBuildCommand $Command @Arguments +} +finally { + Remove-Module 'KoreBuild' -ErrorAction Ignore +} diff --git a/run.sh b/run.sh new file mode 100755 index 000000000..4c1fed564 --- /dev/null +++ b/run.sh @@ -0,0 +1,256 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# +# variables +# + +RESET="\033[0m" +RED="\033[0;31m" +YELLOW="\033[0;33m" +MAGENTA="\033[0;95m" +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +[ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet" +verbose=false +update=false +reinstall=false +repo_path="$DIR" +channel='' +tools_source='' +tools_source_suffix='' +ci=false + +# +# Functions +# +__usage() { + echo "Usage: $(basename "${BASH_SOURCE[0]}") command [options] [[--] ...]" + echo "" + echo "Arguments:" + echo " command The command to be run." + echo " ... Arguments passed to the command. Variable number of arguments allowed." + echo "" + echo "Options:" + echo " --verbose Show verbose output." + echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." + echo " --config-file The path to the configuration file that stores values. Defaults to korebuild.json." + echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." + echo " --path The directory to build. Defaults to the directory containing the script." + echo " -s|--tools-source|-ToolsSource The base url where build tools can be downloaded. Overrides the value from the config file." + echo " --tools-source-suffix|-ToolsSourceSuffix The suffix to append to tools-source. Useful for query strings." + echo " -u|--update Update to the latest KoreBuild even if the lock file is present." + echo " --reinstall Reinstall KoreBuild." + echo " --ci Apply CI specific settings and environment variables." + echo "" + echo "Description:" + echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be." + echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel." + + if [[ "${1:-}" != '--no-exit' ]]; then + exit 2 + fi +} + +get_korebuild() { + local version + local lock_file="$repo_path/korebuild-lock.txt" + if [ ! -f "$lock_file" ] || [ "$update" = true ]; then + __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" "$tools_source_suffix" + fi + version="$(grep 'version:*' -m 1 "$lock_file")" + if [[ "$version" == '' ]]; then + __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'" + return 1 + fi + version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" + + if [ "$reinstall" = true ] && [ -d "$korebuild_path" ]; then + rm -rf "$korebuild_path" + fi + + { + if [ ! -d "$korebuild_path" ]; then + mkdir -p "$korebuild_path" + local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" + tmpfile="$(mktemp)" + echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" + if __get_remote_file "$remote_path" "$tmpfile" "$tools_source_suffix"; then + unzip -q -d "$korebuild_path" "$tmpfile" + fi + rm "$tmpfile" || true + fi + + source "$korebuild_path/KoreBuild.sh" + } || { + if [ -d "$korebuild_path" ]; then + echo "Cleaning up after failed installation" + rm -rf "$korebuild_path" || true + fi + return 1 + } +} + +__error() { + echo -e "${RED}error: $*${RESET}" 1>&2 +} + +__warn() { + echo -e "${YELLOW}warning: $*${RESET}" +} + +__machine_has() { + hash "$1" > /dev/null 2>&1 + return $? +} + +__get_remote_file() { + local remote_path=$1 + local local_path=$2 + local remote_path_suffix=$3 + + if [[ "$remote_path" != 'http'* ]]; then + cp "$remote_path" "$local_path" + return 0 + fi + + local failed=false + if __machine_has wget; then + wget --tries 10 --quiet -O "$local_path" "${remote_path}${remote_path_suffix}" || failed=true + else + failed=true + fi + + if [ "$failed" = true ] && __machine_has curl; then + failed=false + curl --retry 10 -sSL -f --create-dirs -o "$local_path" "${remote_path}${remote_path_suffix}" || failed=true + fi + + if [ "$failed" = true ]; then + __error "Download failed: $remote_path" 1>&2 + return 1 + fi +} + +# +# main +# + +command="${1:-}" +shift + +while [[ $# -gt 0 ]]; do + case $1 in + -\?|-h|--help) + __usage --no-exit + exit 0 + ;; + -c|--channel|-Channel) + shift + channel="${1:-}" + [ -z "$channel" ] && __usage + ;; + --config-file|-ConfigFile) + shift + config_file="${1:-}" + [ -z "$config_file" ] && __usage + if [ ! -f "$config_file" ]; then + __error "Invalid value for --config-file. $config_file does not exist." + exit 1 + fi + ;; + -d|--dotnet-home|-DotNetHome) + shift + DOTNET_HOME="${1:-}" + [ -z "$DOTNET_HOME" ] && __usage + ;; + --path|-Path) + shift + repo_path="${1:-}" + [ -z "$repo_path" ] && __usage + ;; + -s|--tools-source|-ToolsSource) + shift + tools_source="${1:-}" + [ -z "$tools_source" ] && __usage + ;; + --tools-source-suffix|-ToolsSourceSuffix) + shift + tools_source_suffix="${1:-}" + [ -z "$tools_source_suffix" ] && __usage + ;; + -u|--update|-Update) + update=true + ;; + --reinstall|-[Rr]einstall) + reinstall=true + ;; + --ci|-[Cc][Ii]) + ci=true + ;; + --verbose|-Verbose) + verbose=true + ;; + --) + shift + break + ;; + *) + break + ;; + esac + shift +done + +if ! __machine_has unzip; then + __error 'Missing required command: unzip' + exit 1 +fi + +if ! __machine_has curl && ! __machine_has wget; then + __error 'Missing required command. Either wget or curl is required.' + exit 1 +fi + +[ -z "${config_file:-}" ] && config_file="$repo_path/korebuild.json" +if [ -f "$config_file" ]; then + if __machine_has jq ; then + if jq '.' "$config_file" >/dev/null ; then + config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")" + config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")" + else + __error "$config_file contains invalid JSON." + exit 1 + fi + elif __machine_has python ; then + if python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then + config_channel="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" + config_tools_source="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" + else + __error "$config_file contains invalid JSON." + exit 1 + fi + elif __machine_has python3 ; then + if python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then + config_channel="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" + config_tools_source="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" + else + __error "$config_file contains invalid JSON." + exit 1 + fi + else + __error 'Missing required command: jq or python. Could not parse the JSON file.' + exit 1 + fi + + [ ! -z "${config_channel:-}" ] && channel="$config_channel" + [ ! -z "${config_tools_source:-}" ] && tools_source="$config_tools_source" +fi + +[ -z "$channel" ] && channel='master' +[ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' + +get_korebuild +set_korebuildsettings "$tools_source" "$DOTNET_HOME" "$repo_path" "$config_file" "$ci" +invoke_korebuild_command "$command" "$@" diff --git a/samples/CookiePolicySample/CookiePolicySample.csproj b/samples/CookiePolicySample/CookiePolicySample.csproj new file mode 100644 index 000000000..010edb018 --- /dev/null +++ b/samples/CookiePolicySample/CookiePolicySample.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp3.0 + + + + + + + + + + + + + + diff --git a/samples/CookiePolicySample/Program.cs b/samples/CookiePolicySample/Program.cs new file mode 100644 index 000000000..3fc09a3db --- /dev/null +++ b/samples/CookiePolicySample/Program.cs @@ -0,0 +1,26 @@ +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace CookiePolicySample +{ + public static class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging(factory => + { + factory.AddConsole(); + factory.AddFilter("Microsoft", LogLevel.Trace); + }) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/samples/CookiePolicySample/Properties/launchSettings.json b/samples/CookiePolicySample/Properties/launchSettings.json new file mode 100644 index 000000000..38ca6fc37 --- /dev/null +++ b/samples/CookiePolicySample/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:1788/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "CookieSample": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:12345", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/samples/CookiePolicySample/Startup.cs b/samples/CookiePolicySample/Startup.cs new file mode 100644 index 000000000..4dcc6d4dc --- /dev/null +++ b/samples/CookiePolicySample/Startup.cs @@ -0,0 +1,127 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; + +namespace CookiePolicySample +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(); + services.Configure(options => + { + options.CheckConsentNeeded = context => context.Request.PathBase.Equals("/NeedsConsent"); + + options.OnAppendCookie = context => { }; + }); + } + + public void Configure(IApplicationBuilder app) + { + app.UseCookiePolicy(); + app.UseAuthentication(); + + app.Map("/NeedsConsent", NestedApp); + app.Map("/NeedsNoConsent", NestedApp); + NestedApp(app); + } + + private void NestedApp(IApplicationBuilder app) + { + app.Run(async context => + { + var path = context.Request.Path; + switch (path) + { + case "/Login": + var user = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "bob") }, + CookieAuthenticationDefaults.AuthenticationScheme)); + await context.SignInAsync(user); + break; + case "/Logout": + await context.SignOutAsync(); + break; + case "/CreateTempCookie": + context.Response.Cookies.Append("Temp", "1"); + break; + case "/RemoveTempCookie": + context.Response.Cookies.Delete("Temp"); + break; + case "/CreateEssentialCookie": + context.Response.Cookies.Append("EssentialCookie", "2", + new CookieOptions() { IsEssential = true }); + break; + case "/RemoveEssentialCookie": + context.Response.Cookies.Delete("EssentialCookie"); + break; + case "/GrantConsent": + context.Features.Get().GrantConsent(); + break; + case "/WithdrawConsent": + context.Features.Get().WithdrawConsent(); + break; + } + + // TODO: Debug log when cookie is suppressed + + await HomePage(context); + }); + } + + private async Task HomePage(HttpContext context) + { + var response = context.Response; + var cookies = context.Request.Cookies; + response.ContentType = "text/html"; + await response.WriteAsync("\r\n"); + + await response.WriteAsync($"Home
\r\n"); + await response.WriteAsync($"Login
\r\n"); + await response.WriteAsync($"Logout
\r\n"); + await response.WriteAsync($"Create Temp Cookie
\r\n"); + await response.WriteAsync($"Remove Temp Cookie
\r\n"); + await response.WriteAsync($"Create Essential Cookie
\r\n"); + await response.WriteAsync($"Remove Essential Cookie
\r\n"); + await response.WriteAsync($"Grant Consent
\r\n"); + await response.WriteAsync($"Withdraw Consent
\r\n"); + await response.WriteAsync("
\r\n"); + await response.WriteAsync($"Needs Consent
\r\n"); + await response.WriteAsync($"Needs No Consent
\r\n"); + await response.WriteAsync("
\r\n"); + + var feature = context.Features.Get(); + await response.WriteAsync($"Consent:
\r\n"); + await response.WriteAsync($" - IsNeeded: {feature.IsConsentNeeded}
\r\n"); + await response.WriteAsync($" - Has: {feature.HasConsent}
\r\n"); + await response.WriteAsync($" - Can Track: {feature.CanTrack}
\r\n"); + await response.WriteAsync("
\r\n"); + + await response.WriteAsync($"{cookies.Count} Request Cookies:
\r\n"); + foreach (var cookie in cookies) + { + await response.WriteAsync($" - {cookie.Key} = {cookie.Value}
\r\n"); + } + await response.WriteAsync("
\r\n"); + + var responseCookies = response.Headers[HeaderNames.SetCookie]; + await response.WriteAsync($"{responseCookies.Count} Response Cookies:
\r\n"); + foreach (var cookie in responseCookies) + { + await response.WriteAsync($" - {cookie}
\r\n"); + } + + await response.WriteAsync(""); + } + } +} diff --git a/samples/CookieSample/CookieSample.csproj b/samples/CookieSample/CookieSample.csproj new file mode 100644 index 000000000..bc92d68a2 --- /dev/null +++ b/samples/CookieSample/CookieSample.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.0 + + + + + + + + + + + + + + + + diff --git a/samples/CookieSample/CookieSample.xproj b/samples/CookieSample/CookieSample.xproj deleted file mode 100644 index bb23e6481..000000000 --- a/samples/CookieSample/CookieSample.xproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 558c2c2a-aed8-49de-bb60-d5f8ae06c714 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - 22569 - - - \ No newline at end of file diff --git a/samples/CookieSample/Program.cs b/samples/CookieSample/Program.cs new file mode 100644 index 000000000..3f40d3194 --- /dev/null +++ b/samples/CookieSample/Program.cs @@ -0,0 +1,26 @@ +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace CookieSample +{ + public static class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging(factory => + { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + }) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/samples/CookieSample/Properties/launchSettings.json b/samples/CookieSample/Properties/launchSettings.json index c85de8d26..38ca6fc37 100644 --- a/samples/CookieSample/Properties/launchSettings.json +++ b/samples/CookieSample/Properties/launchSettings.json @@ -12,15 +12,15 @@ "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { - "ASPNET_ENV": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" } }, - "web": { - "commandName": "web", + "CookieSample": { + "commandName": "Project", "launchBrowser": true, - "launchUrl": "http://localhost:12345", + "applicationUrl": "http://localhost:12345", "environmentVariables": { - "Hosting:Environment": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/samples/CookieSample/Startup.cs b/samples/CookieSample/Startup.cs index c2593c909..a91791070 100644 --- a/samples/CookieSample/Startup.cs +++ b/samples/CookieSample/Startup.cs @@ -1,11 +1,11 @@ using System.Linq; using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace CookieSample { @@ -13,24 +13,24 @@ public class Startup { public void ConfigureServices(IServiceCollection services) { - services.AddAuthentication(); + // This can be removed after https://github.com/aspnet/IISIntegration/issues/371 + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }).AddCookie(); } - public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) + public void Configure(IApplicationBuilder app) { - loggerfactory.AddConsole(LogLevel.Information); - - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AutomaticAuthenticate = true - }); + app.UseAuthentication(); app.Run(async context => { if (!context.User.Identities.Any(identity => identity.IsAuthenticated)) { var user = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "bob") }, CookieAuthenticationDefaults.AuthenticationScheme)); - await context.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, user); + await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, user); context.Response.ContentType = "text/plain"; await context.Response.WriteAsync("Hello First timer"); @@ -41,17 +41,5 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) await context.Response.WriteAsync("Hello old timer"); }); } - - public static void Main(string[] args) - { - var host = new WebHostBuilder() - .UseDefaultHostingConfiguration(args) - .UseKestrel() - .UseIISIntegration() - .UseStartup() - .Build(); - - host.Run(); - } } } diff --git a/samples/CookieSample/project.json b/samples/CookieSample/project.json deleted file mode 100644 index 531ea474c..000000000 --- a/samples/CookieSample/project.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1-*", - "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-*", - "Microsoft.AspNetCore.DataProtection": "1.0.0-*", - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-*", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*", - "Microsoft.Extensions.Logging.Console": "1.0.0-*" - }, - "compilationOptions": { - "emitEntryPoint": true - }, - "frameworks": { - "net451": {}, - "netcoreapp1.0": { - "imports": [ - "dnxcore50" - ], - "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.0.0-*", - "type": "platform" - } - } - } - }, - "content": [ - "web.config" - ] -} \ No newline at end of file diff --git a/samples/CookieSample/web.config b/samples/CookieSample/web.config deleted file mode 100644 index 8d6117761..000000000 --- a/samples/CookieSample/web.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/samples/CookieSessionSample/CookieSessionSample.csproj b/samples/CookieSessionSample/CookieSessionSample.csproj new file mode 100644 index 000000000..6230c98a5 --- /dev/null +++ b/samples/CookieSessionSample/CookieSessionSample.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.0 + + + + + + + + + + + + + + + + diff --git a/samples/CookieSessionSample/CookieSessionSample.xproj b/samples/CookieSessionSample/CookieSessionSample.xproj deleted file mode 100644 index c81393af1..000000000 --- a/samples/CookieSessionSample/CookieSessionSample.xproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 19711880-46da-4a26-9e0f-9b2e41d27651 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - 36505 - - - \ No newline at end of file diff --git a/samples/CookieSessionSample/Program.cs b/samples/CookieSessionSample/Program.cs new file mode 100644 index 000000000..1a19850e6 --- /dev/null +++ b/samples/CookieSessionSample/Program.cs @@ -0,0 +1,26 @@ +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace CookieSessionSample +{ + public static class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging(factory => + { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + }) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/samples/CookieSessionSample/Properties/launchSettings.json b/samples/CookieSessionSample/Properties/launchSettings.json index 8d4a0316a..25de3e478 100644 --- a/samples/CookieSessionSample/Properties/launchSettings.json +++ b/samples/CookieSessionSample/Properties/launchSettings.json @@ -12,15 +12,15 @@ "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { - "ASPNET_ENV": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" } }, - "web": { - "commandName": "web", + "CookieSessionSample": { + "commandName": "Project", "launchBrowser": true, - "launchUrl": "http://localhost:12345", + "applicationUrl": "http://localhost:12345", "environmentVariables": { - "Hosting:Environment": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/samples/CookieSessionSample/Startup.cs b/samples/CookieSessionSample/Startup.cs index 423c96120..f7b8f2bb8 100644 --- a/samples/CookieSessionSample/Startup.cs +++ b/samples/CookieSessionSample/Startup.cs @@ -4,9 +4,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace CookieSessionSample { @@ -14,18 +14,17 @@ public class Startup { public void ConfigureServices(IServiceCollection services) { - services.AddAuthentication(); + // This can be removed after https://github.com/aspnet/IISIntegration/issues/371 + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }).AddCookie(o => o.SessionStore = new MemoryCacheTicketStore()); } - public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) + public void Configure(IApplicationBuilder app) { - loggerfactory.AddConsole(LogLevel.Information); - - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AutomaticAuthenticate = true, - SessionStore = new MemoryCacheTicketStore() - }); + app.UseAuthentication(); app.Run(async context => { @@ -39,7 +38,7 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) claims.Add(new Claim(ClaimTypes.Role, "SomeRandomGroup" + i, ClaimValueTypes.String, "IssuedByBob", "OriginalIssuerJoe")); } - await context.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, + await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme))); context.Response.ContentType = "text/plain"; @@ -51,17 +50,5 @@ await context.Authentication.SignInAsync(CookieAuthenticationDefaults.Authentica await context.Response.WriteAsync("Hello old timer"); }); } - - public static void Main(string[] args) - { - var host = new WebHostBuilder() - .UseDefaultHostingConfiguration(args) - .UseKestrel() - .UseIISIntegration() - .UseStartup() - .Build(); - - host.Run(); - } } } diff --git a/samples/CookieSessionSample/project.json b/samples/CookieSessionSample/project.json deleted file mode 100644 index c1aee49c1..000000000 --- a/samples/CookieSessionSample/project.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1-*", - "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-*", - "Microsoft.AspNetCore.DataProtection": "1.0.0-*", - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-*", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*", - "Microsoft.Extensions.Caching.Memory": "1.0.0-*", - "Microsoft.Extensions.Logging.Console": "1.0.0-*" - }, - "compilationOptions": { - "emitEntryPoint": true - }, - "frameworks": { - "net451": {}, - "netcoreapp1.0": { - "imports": [ - "dnxcore50" - ], - "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.0.0-*", - "type": "platform" - } - } - } - }, - "content": [ - "web.config" - ] -} \ No newline at end of file diff --git a/samples/CookieSessionSample/web.config b/samples/CookieSessionSample/web.config deleted file mode 100644 index fb7e915bf..000000000 --- a/samples/CookieSessionSample/web.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/samples/JwtBearerSample/JwtBearerSample.csproj b/samples/JwtBearerSample/JwtBearerSample.csproj new file mode 100644 index 000000000..58bf5e2d9 --- /dev/null +++ b/samples/JwtBearerSample/JwtBearerSample.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.0 + aspnet5-JwtBearerSample-20151210102827 + + + + + + + + + + + + + + + + diff --git a/samples/JwtBearerSample/JwtBearerSample.xproj b/samples/JwtBearerSample/JwtBearerSample.xproj deleted file mode 100644 index ed0c9b32b..000000000 --- a/samples/JwtBearerSample/JwtBearerSample.xproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - d399b84f-591b-4e98-92ba-b0f63e7b6957 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - - - - - - \ No newline at end of file diff --git a/samples/JwtBearerSample/Program.cs b/samples/JwtBearerSample/Program.cs new file mode 100644 index 000000000..44d2fe0c4 --- /dev/null +++ b/samples/JwtBearerSample/Program.cs @@ -0,0 +1,21 @@ +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; + +namespace JwtBearerSample +{ + public static class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/samples/JwtBearerSample/Properties/launchSettings.json b/samples/JwtBearerSample/Properties/launchSettings.json index 49cbac543..6922375c9 100644 --- a/samples/JwtBearerSample/Properties/launchSettings.json +++ b/samples/JwtBearerSample/Properties/launchSettings.json @@ -15,10 +15,12 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "web": { - "commandName": "web", + "JwtBearer": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:42023", "environmentVariables": { - "Hosting:Environment": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/samples/JwtBearerSample/Startup.cs b/samples/JwtBearerSample/Startup.cs index c2dbfb7a9..8c4a63cad 100644 --- a/samples/JwtBearerSample/Startup.cs +++ b/samples/JwtBearerSample/Startup.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Runtime.ExceptionServices; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -13,16 +16,27 @@ namespace JwtBearerSample { public class Startup { - public Startup() + public Startup(IHostingEnvironment env) { - Configuration = new ConfigurationBuilder() - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); + Environment = env; + + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath); + + if (env.IsDevelopment()) + { + // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 + builder.AddUserSecrets(); + } + + builder.AddEnvironmentVariables(); + Configuration = builder.Build(); } public IConfiguration Configuration { get; set; } + public IHostingEnvironment Environment { get; set; } + // Shared between users in memory public IList Todos { get; } = new List(); @@ -30,55 +44,42 @@ public Startup() // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { - services.AddAuthentication(); + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(o => + { + // You also need to update /wwwroot/app/scripts/app.js + o.Authority = Configuration["oidc:authority"]; + o.Audience = Configuration["oidc:clientid"]; + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app) { - // Simple error page to avoid a repo dependency. - app.Use(async (context, next) => - { - try - { - await next(); - } - catch (Exception ex) - { - if (context.Response.HasStarted) - { - throw; - } - context.Response.StatusCode = 500; - await context.Response.WriteAsync(ex.ToString()); - } - }); + app.UseDeveloperExceptionPage(); app.UseDefaultFiles(); app.UseStaticFiles(); - app.UseJwtBearerAuthentication(new JwtBearerOptions - { - // You also need to update /wwwroot/app/scripts/app.js - Authority = Configuration["jwt:authority"], - Audience = Configuration["jwt:audience"] - }); + app.UseAuthentication(); // [Authorize] would usually handle this app.Use(async (context, next) => { - // Use this if options.AutomaticAuthenticate = false - // var user = await context.Authentication.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); - - var user = context.User; // We can do this because of options.AutomaticAuthenticate = true; - if (user?.Identity?.IsAuthenticated ?? false) + // Use this if there are multiple authentication schemes + var authResult = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); + if (authResult.Succeeded && authResult.Principal.Identity.IsAuthenticated) { await next(); } + else if (authResult.Failure != null) + { + // Rethrow, let the exception page handle it. + ExceptionDispatchInfo.Capture(authResult.Failure).Throw(); + } else { - // We can do this because of options.AutomaticChallenge = true; - await context.Authentication.ChallengeAsync(); + await context.ChallengeAsync(); } }); @@ -106,19 +107,5 @@ public void Configure(IApplicationBuilder app) }); }); } - - // Entry point for the application. - public static void Main(string[] args) - { - var host = new WebHostBuilder() - .UseDefaultHostingConfiguration(args) - .UseKestrel() - .UseIISIntegration() - .UseStartup() - .Build(); - - host.Run(); - } } -} - +} \ No newline at end of file diff --git a/samples/JwtBearerSample/project.json b/samples/JwtBearerSample/project.json deleted file mode 100644 index 29ecf0157..000000000 --- a/samples/JwtBearerSample/project.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "version": "1.0.0-*", - "compilationOptions": { - "emitEntryPoint": true - }, - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1-*", - "Microsoft.AspNetCore.Authentication.JwtBearer": "1.0.0-*", - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-*", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*", - "Microsoft.AspNetCore.StaticFiles": "1.0.0-*", - "Microsoft.Extensions.Configuration.UserSecrets": "1.0.0-*" - }, - "frameworks": { - "net451": {}, - "netcoreapp1.0": { - "imports": [ - "dnxcore50" - ], - "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.0.0-*", - "type": "platform" - } - } - } - }, - "content": [ - "project.json", - "wwwroot", - "web.config" - ], - "userSecretsId": "aspnet5-JwtBearerSample-20151210102827" -} \ No newline at end of file diff --git a/samples/JwtBearerSample/web.config b/samples/JwtBearerSample/web.config deleted file mode 100644 index 801dbfd85..000000000 --- a/samples/JwtBearerSample/web.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/samples/JwtBearerSample/wwwroot/index.html b/samples/JwtBearerSample/wwwroot/index.html index f71dccb69..4715ebd42 100644 --- a/samples/JwtBearerSample/wwwroot/index.html +++ b/samples/JwtBearerSample/wwwroot/index.html @@ -64,5 +64,6 @@

Todo List

+ diff --git a/samples/OpenIdConnect.AzureAdSample/AuthPropertiesTokenCache.cs b/samples/OpenIdConnect.AzureAdSample/AuthPropertiesTokenCache.cs index f174174cf..7d9b39121 100644 --- a/samples/OpenIdConnect.AzureAdSample/AuthPropertiesTokenCache.cs +++ b/samples/OpenIdConnect.AzureAdSample/AuthPropertiesTokenCache.cs @@ -1,5 +1,8 @@ using System; -using Microsoft.AspNetCore.Http.Authentication; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http; using Microsoft.IdentityModel.Clients.ActiveDirectory; namespace OpenIdConnect.AzureAdSample @@ -8,43 +11,81 @@ public class AuthPropertiesTokenCache : TokenCache { private const string TokenCacheKey = ".TokenCache"; + private HttpContext _httpContext; + private ClaimsPrincipal _principal; private AuthenticationProperties _authProperties; + private string _signInScheme; - public bool HasCacheChanged { get; internal set; } - - public AuthPropertiesTokenCache(AuthenticationProperties authProperties) : base() + private AuthPropertiesTokenCache(AuthenticationProperties authProperties) : base() { _authProperties = authProperties; - BeforeAccess = BeforeAccessNotification; - AfterAccess = AfterAccessNotification; + BeforeAccess = BeforeAccessNotificationWithProperties; + AfterAccess = AfterAccessNotificationWithProperties; + BeforeWrite = BeforeWriteNotification; + } + + private AuthPropertiesTokenCache(HttpContext httpContext, string signInScheme) : base() + { + _httpContext = httpContext; + _signInScheme = signInScheme; + BeforeAccess = BeforeAccessNotificationWithContext; + AfterAccess = AfterAccessNotificationWithContext; BeforeWrite = BeforeWriteNotification; + } + + public static TokenCache ForCodeRedemption(AuthenticationProperties authProperties) + { + return new AuthPropertiesTokenCache(authProperties); + } + + public static TokenCache ForApiCalls(HttpContext httpContext, + string signInScheme = CookieAuthenticationDefaults.AuthenticationScheme) + { + return new AuthPropertiesTokenCache(httpContext, signInScheme); + } + private void BeforeAccessNotificationWithProperties(TokenCacheNotificationArgs args) + { string cachedTokensText; - if (authProperties.Items.TryGetValue(TokenCacheKey, out cachedTokensText)) + if (_authProperties.Items.TryGetValue(TokenCacheKey, out cachedTokensText)) { var cachedTokens = Convert.FromBase64String(cachedTokensText); Deserialize(cachedTokens); } } - // Notification raised before ADAL accesses the cache. - // This is your chance to update the in-memory copy from the DB, if the in-memory version is stale - private void BeforeAccessNotification(TokenCacheNotificationArgs args) + private void BeforeAccessNotificationWithContext(TokenCacheNotificationArgs args) { + // Retrieve the auth session with the cached tokens + var result = _httpContext.AuthenticateAsync(_signInScheme).Result; + _authProperties = result.Ticket.Properties; + _principal = result.Ticket.Principal; + + BeforeAccessNotificationWithProperties(args); + } + private void AfterAccessNotificationWithProperties(TokenCacheNotificationArgs args) + { + // if state changed + if (HasStateChanged) + { + var cachedTokens = Serialize(); + var cachedTokensText = Convert.ToBase64String(cachedTokens); + _authProperties.Items[TokenCacheKey] = cachedTokensText; + } } - // Notification raised after ADAL accessed the cache. - // If the HasStateChanged flag is set, ADAL changed the content of the cache - private void AfterAccessNotification(TokenCacheNotificationArgs args) + private void AfterAccessNotificationWithContext(TokenCacheNotificationArgs args) { // if state changed if (HasStateChanged) { - HasCacheChanged = true; + AfterAccessNotificationWithProperties(args); + var cachedTokens = Serialize(); var cachedTokensText = Convert.ToBase64String(cachedTokens); _authProperties.Items[TokenCacheKey] = cachedTokensText; + _httpContext.SignInAsync(_signInScheme, _principal, _authProperties).Wait(); } } diff --git a/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.csproj b/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.csproj new file mode 100644 index 000000000..61754b60b --- /dev/null +++ b/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp3.0 + aspnet5-OpenIdConnectSample-20151210110318 + + + + + + + + + + + + + + + + + + diff --git a/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.xproj b/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.xproj deleted file mode 100644 index e3495a211..000000000 --- a/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.xproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 3a7ad414-ebde-4f92-b307-4e8f19b6117e - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - - - - - - \ No newline at end of file diff --git a/samples/OpenIdConnect.AzureAdSample/Program.cs b/samples/OpenIdConnect.AzureAdSample/Program.cs new file mode 100644 index 000000000..0e1285a9c --- /dev/null +++ b/samples/OpenIdConnect.AzureAdSample/Program.cs @@ -0,0 +1,27 @@ +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace OpenIdConnect.AzureAdSample +{ + public static class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging(factory => + { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + }) + .UseKestrel() + .UseUrls("http://localhost:42023") + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/samples/OpenIdConnect.AzureAdSample/Properties/launchSettings.json b/samples/OpenIdConnect.AzureAdSample/Properties/launchSettings.json index 49cbac543..e6436fee2 100644 --- a/samples/OpenIdConnect.AzureAdSample/Properties/launchSettings.json +++ b/samples/OpenIdConnect.AzureAdSample/Properties/launchSettings.json @@ -15,10 +15,12 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "web": { - "commandName": "web", + "OpenIdConnect": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:42023", "environmentVariables": { - "Hosting:Environment": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/samples/OpenIdConnect.AzureAdSample/Readme.md b/samples/OpenIdConnect.AzureAdSample/Readme.md new file mode 100644 index 000000000..767e336ac --- /dev/null +++ b/samples/OpenIdConnect.AzureAdSample/Readme.md @@ -0,0 +1,20 @@ +# How to set up the sample locally + +## Set up [Azure Active Directory](https://azure.microsoft.com/en-us/documentation/services/active-directory/) + +1. Create your own Azure Active Directory (AD). Save the "tenent name". +2. Add a new Application: in the Azure AD portal, select Application, and click Add in the drawer. +3. Set the sign-on url to `http://localhost:42023`. +4. Select the newly created Application, navigate to the Configure tab. +5. Find and save the "Client Id" +8. In the keys section add a new key. A key value will be generated. Save the value as "Client Secret" + +## Configure the local environment +1. Set environment ASPNETCORE_ENVIRONMENT to DEVELOPMENT. ([Working with Multiple Environments](https://docs.asp.net/en/latest/fundamentals/environments.html)) +2. Set up user secrets: +``` +dotnet user-secrets set oidc:clientid +dotnet user-secrets set oidc:clientsecret +dotnet user-secrets set oidc:authority https://login.windows.net/.onmicrosoft.com +``` + diff --git a/samples/OpenIdConnect.AzureAdSample/Startup.cs b/samples/OpenIdConnect.AzureAdSample/Startup.cs index 938a19c20..c3fa3c719 100644 --- a/samples/OpenIdConnect.AzureAdSample/Startup.cs +++ b/samples/OpenIdConnect.AzureAdSample/Startup.cs @@ -1,16 +1,17 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Clients.ActiveDirectory; using Microsoft.IdentityModel.Protocols.OpenIdConnect; @@ -18,150 +19,185 @@ namespace OpenIdConnect.AzureAdSample { public class Startup { - private const string GraphResourceID = "https://graph.windows.net"; - - public Startup() + public Startup(IHostingEnvironment env) { - Configuration = new ConfigurationBuilder() - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath); + + if (env.IsDevelopment()) + { + // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 + builder.AddUserSecrets(); + } + + builder.AddEnvironmentVariables(); + Configuration = builder.Build(); } public IConfiguration Configuration { get; set; } + private string ClientId => Configuration["oidc:clientid"]; + private string ClientSecret => Configuration["oidc:clientsecret"]; + private string Authority => Configuration["oidc:authority"]; + private string Resource => "https://graph.windows.net"; + public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(sharedOptions => - sharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme); - } - - public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) - { - loggerfactory.AddConsole(LogLevel.Information); - - // Simple error page - app.Use(async (context, next) => { - try - { - await next(); - } - catch (Exception ex) - { - if (!context.Response.HasStarted) - { - context.Response.Clear(); - context.Response.StatusCode = 500; - await context.Response.WriteAsync(ex.ToString()); - } - else - { - throw; - } - } - }); - - app.UseCookieAuthentication(new CookieAuthenticationOptions()); - - var clientId = Configuration["oidc:clientid"]; - var clientSecret = Configuration["oidc:clientsecret"]; - var authority = Configuration["oidc:authority"]; - var resource = "https://graph.windows.net"; - app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, "AAD", o => { - ClientId = clientId, - ClientSecret = clientSecret, // for code flow - Authority = authority, - ResponseType = OpenIdConnectResponseTypes.CodeIdToken, + o.ClientId = ClientId; + o.ClientSecret = ClientSecret; // for code flow + o.Authority = Authority; + o.ResponseType = OpenIdConnectResponseType.CodeIdToken; + o.SignedOutRedirectUri = "/signed-out"; // GetClaimsFromUserInfoEndpoint = true, - Events = new OpenIdConnectEvents() + o.Events = new OpenIdConnectEvents() { OnAuthorizationCodeReceived = async context => { var request = context.HttpContext.Request; - var currentUri = UriHelper.Encode(request.Scheme, request.Host, request.PathBase, request.Path); - var credential = new ClientCredential(clientId, clientSecret); - var authContext = new AuthenticationContext(authority, new AuthPropertiesTokenCache(context.Properties)); + var currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path); + var credential = new ClientCredential(ClientId, ClientSecret); + var authContext = new AuthenticationContext(Authority, AuthPropertiesTokenCache.ForCodeRedemption(context.Properties)); var result = await authContext.AcquireTokenByAuthorizationCodeAsync( - context.ProtocolMessage.Code, new Uri(currentUri), credential, resource); + context.ProtocolMessage.Code, new Uri(currentUri), credential, Resource); context.HandleCodeRedemption(result.AccessToken, result.IdToken); } - } + }; }); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + + app.UseAuthentication(); app.Run(async context => { - if (context.Request.Path.Equals("/signout")) + if (context.Request.Path.Equals("/signin")) { - await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - context.Response.ContentType = "text/html"; - await context.Response.WriteAsync($"Signing out {context.User.Identity.Name}
{Environment.NewLine}"); - await context.Response.WriteAsync("Sign In"); - await context.Response.WriteAsync($""); - return; - } + if (context.User.Identities.Any(identity => identity.IsAuthenticated)) + { + // User has already signed in + context.Response.Redirect("/"); + return; + } - if (!context.User.Identities.Any(identity => identity.IsAuthenticated)) + await context.ChallengeAsync(new AuthenticationProperties { RedirectUri = "/" }); + } + else if (context.Request.Path.Equals("/signout")) { - await context.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" }); - return; + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await WriteHtmlAsync(context.Response, + async response => + { + await response.WriteAsync($"

Signed out locally: {HtmlEncode(context.User.Identity.Name)}

"); + await response.WriteAsync("Sign In"); + }); } - - context.Response.ContentType = "text/html"; - await context.Response.WriteAsync($"Hello Authenticated User {context.User.Identity.Name}
{Environment.NewLine}"); - await context.Response.WriteAsync("Claims:
" + Environment.NewLine); - foreach (var claim in context.User.Claims) + else if (context.Request.Path.Equals("/signout-remote")) { - await context.Response.WriteAsync($"{claim.Type}: {claim.Value}
{Environment.NewLine}"); + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); } - - await context.Response.WriteAsync("Tokens:
" + Environment.NewLine); - try + else if (context.Request.Path.Equals("/signed-out")) { - // Retrieve the auth session with the cached tokens - var authenticateContext = new AuthenticateContext(CookieAuthenticationDefaults.AuthenticationScheme); - await context.Authentication.AuthenticateAsync(authenticateContext); - var authProperties = new AuthenticationProperties(authenticateContext.Properties); - var tokenCache = new AuthPropertiesTokenCache(authProperties); - - // Use ADAL to get the right token - var authContext = new AuthenticationContext(authority, tokenCache); - var credential = new ClientCredential(clientId, clientSecret); - string userObjectID = context.User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; - var result = authContext.AcquireTokenSilent(resource, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId)); - - // Update the cookie with the modified tokens - if (tokenCache.HasCacheChanged) - { - await context.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, authenticateContext.Principal, authProperties); - } - - await context.Response.WriteAsync($"access_token: {result.AccessToken}
{Environment.NewLine}"); + await WriteHtmlAsync(context.Response, + async response => + { + await response.WriteAsync($"

You have been signed out.

"); + await response.WriteAsync("Sign In"); + }); } - catch (Exception ex) + else if (context.Request.Path.Equals("/remote-signedout")) { - await context.Response.WriteAsync($"AquireToken error: {ex.Message}
{Environment.NewLine}"); + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await WriteHtmlAsync(context.Response, + async response => + { + await response.WriteAsync($"

Signed out remotely: {HtmlEncode(context.User.Identity.Name)}

"); + await response.WriteAsync("Sign In"); + }); } + else + { + if (!context.User.Identities.Any(identity => identity.IsAuthenticated)) + { + await context.ChallengeAsync(new AuthenticationProperties { RedirectUri = "/" }); + return; + } - await context.Response.WriteAsync("Sign Out"); - await context.Response.WriteAsync($""); + await WriteHtmlAsync(context.Response, async response => + { + await response.WriteAsync($"

Hello Authenticated User {HtmlEncode(context.User.Identity.Name)}

"); + await response.WriteAsync("Sign Out Locally"); + await response.WriteAsync("Sign Out Remotely"); + + await response.WriteAsync("

Claims:

"); + await WriteTableHeader(response, new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value })); + + await response.WriteAsync("

Tokens:

"); + try + { + // Use ADAL to get the right token + var authContext = new AuthenticationContext(Authority, AuthPropertiesTokenCache.ForApiCalls(context, CookieAuthenticationDefaults.AuthenticationScheme)); + var credential = new ClientCredential(ClientId, ClientSecret); + string userObjectID = context.User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; + var result = await authContext.AcquireTokenSilentAsync(Resource, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId)); + + await response.WriteAsync($"

access_token

{HtmlEncode(result.AccessToken)}
"); + } + catch (Exception ex) + { + await response.WriteAsync($"AquireToken error: {ex.Message}"); + } + }); + } }); } - public static void Main(string[] args) + private static async Task WriteHtmlAsync(HttpResponse response, Func writeContent) + { + var bootstrap = ""; + + response.ContentType = "text/html"; + await response.WriteAsync($"{bootstrap}
"); + await writeContent(response); + await response.WriteAsync("
"); + } + + private static async Task WriteTableHeader(HttpResponse response, IEnumerable columns, IEnumerable> data) { - var host = new WebHostBuilder() - .UseDefaultHostingConfiguration(args) - .UseKestrel() - .UseIISIntegration() - .UseStartup() - .Build(); - - host.Run(); + await response.WriteAsync(""); + await response.WriteAsync(""); + foreach (var column in columns) + { + await response.WriteAsync($""); + } + await response.WriteAsync(""); + foreach (var row in data) + { + await response.WriteAsync(""); + foreach (var column in row) + { + await response.WriteAsync($""); + } + await response.WriteAsync(""); + } + await response.WriteAsync("
{HtmlEncode(column)}
{HtmlEncode(column)}
"); } + + private static string HtmlEncode(string content) => + string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content); } } diff --git a/samples/OpenIdConnect.AzureAdSample/project.json b/samples/OpenIdConnect.AzureAdSample/project.json deleted file mode 100644 index b6879e577..000000000 --- a/samples/OpenIdConnect.AzureAdSample/project.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1-*", - "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-*", - "Microsoft.AspNetCore.Authentication.OpenIdConnect": "0.1.0-*", - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-*", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*", - "Microsoft.Extensions.Configuration.UserSecrets": "1.0.0-*", - "Microsoft.Extensions.Logging.Console": "1.0.0-*", - "Microsoft.IdentityModel.Clients.ActiveDirectory": "2.22.302111727" - }, - "frameworks": { - "net451": {} - }, - "compilationOptions": { - "emitEntryPoint": true - }, - "content": [ - "project.json", - "web.config" - ], - "userSecretsId": "aspnet5-OpenIdConnectSample-20151210110318" -} \ No newline at end of file diff --git a/samples/OpenIdConnect.AzureAdSample/web.config b/samples/OpenIdConnect.AzureAdSample/web.config deleted file mode 100644 index cdcb5a614..000000000 --- a/samples/OpenIdConnect.AzureAdSample/web.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/samples/OpenIdConnectSample/OpenIdConnectSample.csproj b/samples/OpenIdConnectSample/OpenIdConnectSample.csproj new file mode 100644 index 000000000..137ee47ba --- /dev/null +++ b/samples/OpenIdConnectSample/OpenIdConnectSample.csproj @@ -0,0 +1,33 @@ + + + + netcoreapp3.0 + aspnet5-OpenIdConnectSample-20151210110318 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/OpenIdConnectSample/OpenIdConnectSample.xproj b/samples/OpenIdConnectSample/OpenIdConnectSample.xproj deleted file mode 100644 index 905bce8e3..000000000 --- a/samples/OpenIdConnectSample/OpenIdConnectSample.xproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - bef0f5c3-ef4e-4649-9c49-d5e279a3ca2b - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - 42023 - - - \ No newline at end of file diff --git a/samples/OpenIdConnectSample/Program.cs b/samples/OpenIdConnectSample/Program.cs new file mode 100644 index 000000000..87e775508 --- /dev/null +++ b/samples/OpenIdConnectSample/Program.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; +using System.Net; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; + +namespace OpenIdConnectSample +{ + public static class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging(factory => + { + factory.AddConsole(); + factory.AddDebug(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + factory.AddFilter("Debug", level => level >= LogLevel.Information); + }) + .UseKestrel(options => + { + options.Listen(IPAddress.Loopback, 44318, listenOptions => + { + // Configure SSL + var serverCertificate = LoadCertificate(); + listenOptions.UseHttps(serverCertificate); + }); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + + private static X509Certificate2 LoadCertificate() + { + var assembly = typeof(Startup).GetTypeInfo().Assembly; + var embeddedFileProvider = new EmbeddedFileProvider(assembly, "OpenIdConnectSample"); + var certificateFileInfo = embeddedFileProvider.GetFileInfo("compiler/resources/cert.pfx"); + using (var certificateStream = certificateFileInfo.CreateReadStream()) + { + byte[] certificatePayload; + using (var memoryStream = new MemoryStream()) + { + certificateStream.CopyTo(memoryStream); + certificatePayload = memoryStream.ToArray(); + } + + return new X509Certificate2(certificatePayload, "testPassword"); + } + } + } +} diff --git a/samples/OpenIdConnectSample/Properties/launchSettings.json b/samples/OpenIdConnectSample/Properties/launchSettings.json index 5a0163016..058fa4c5d 100644 --- a/samples/OpenIdConnectSample/Properties/launchSettings.json +++ b/samples/OpenIdConnectSample/Properties/launchSettings.json @@ -4,24 +4,24 @@ "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:42023", - "sslPort": 0 + "sslPort": 44318 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "launchUrl": "https://localhost:44318/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "web": { - "commandName": "web", + "OpenIdConnectSample": { + "commandName": "Project", "launchBrowser": true, - "launchUrl": "http://localhost:42023", + "applicationUrl": "https://localhost:44318/", "environmentVariables": { - "Hosting:Environment": "Development", - "ASPNET_server.urls": "http://localhost:42023" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/samples/OpenIdConnectSample/Readme.md b/samples/OpenIdConnectSample/Readme.md new file mode 100644 index 000000000..846e3f8e6 --- /dev/null +++ b/samples/OpenIdConnectSample/Readme.md @@ -0,0 +1,44 @@ +# How to set up the sample locally + +The OpenIdConnect sample supports multilpe authentication providers. In these instruction, we will explore how to set up this sample with both Azure Active Directory and Google Identity Platform. + +## Determine your development environment and a few key variables + +This sample is configured to run on port __44318__ locally. In Visual Studio, the setting is carried out in `.\properties\launchSettings.json`. When the application is run from command line, the URL is coded in `Program.cs`. + +If the application is run from command line or terminal, environment variable ASPNETCORE_ENVIRONMENT should be set to DEVELOPMENT to enable user secret. + +## Configure the Authorization server + +### Configure with Azure Active Directory + +1. Set up a new Azure Active Directory (AAD) in your Azure Subscription. +2. Open the newly created AAD in Azure web portal. +3. Navigate to the Applications tab. +4. Add a new Application to the AAD. Set the "Sign-on URL" to sample application's URL. +5. Naigate to the Application, and click the Configure tab. +6. Find and save the "Client Id". +7. Add a new key in the "Keys" section. Save value of the key, which is the "Client Secret". +8. Click the "View Endpoints" on the drawer, a dialog will shows six endpoint URLs. Copy the "OAuth 2.0 Authorization Endpoint" to a text editor and remove the "/oauth2/authorize" from the string. The remaining part is the __authority URL__. It looks like `https://login.microsoftonline.com/`. + +### Configure with Google Identity Platform + +1. Create a new project through [Google APIs](https://console.developers.google.com). +2. In the sidebar choose "Credentials". +3. Navigate to "OAuth consent screen" tab, fill in the project name and save. +4. Navigate to "Credentials" tab. Click "Create credentials". Choose "OAuth client ID". +5. Select "Web application" as the application type. Fill in the "Authorized redirect URIs" with `https://localhost:44318/signin-oidc`. +6. Save the "Client ID" and "Client Secret" shown in the dialog. +7. The "Authority URL" for Google Authentication is `https://accounts.google.com/`. + +## Configure the sample application + +1. Restore the application. +2. Set user secrets: + + ``` +dotnet user-secrets set oidc:clientid +dotnet user-secrets set oidc:clientsecret +dotnet user-secrets set oidc:authority +``` + diff --git a/samples/OpenIdConnectSample/Startup.cs b/samples/OpenIdConnectSample/Startup.cs index 097ce771b..a2b5a9a5c 100644 --- a/samples/OpenIdConnectSample/Startup.cs +++ b/samples/OpenIdConnectSample/Startup.cs @@ -1,113 +1,308 @@ using System; +using System.Collections.Generic; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; using System.Linq; +using System.Net.Http; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Newtonsoft.Json.Linq; namespace OpenIdConnectSample { public class Startup { - public Startup() + public Startup(IHostingEnvironment env) { - Configuration = new ConfigurationBuilder() - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); + Environment = env; + + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath); + + if (env.IsDevelopment()) + { + // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 + builder.AddUserSecrets(); + } + + builder.AddEnvironmentVariables(); + Configuration = builder.Build(); } public IConfiguration Configuration { get; set; } + public IHostingEnvironment Environment { get; set; } + public void ConfigureServices(IServiceCollection services) { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + services.AddAuthentication(sharedOptions => - sharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme); + { + sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddOpenIdConnect(o => + { + o.ClientId = Configuration["oidc:clientid"]; + o.ClientSecret = Configuration["oidc:clientsecret"]; // for code flow + o.Authority = Configuration["oidc:authority"]; + + o.ResponseType = OpenIdConnectResponseType.CodeIdToken; + o.SaveTokens = true; + o.GetClaimsFromUserInfoEndpoint = true; + o.AccessDeniedPath = "/access-denied-from-remote"; + + o.ClaimActions.MapAllExcept("aud", "iss", "iat", "nbf", "exp", "aio", "c_hash", "uti", "nonce"); + + o.Events = new OpenIdConnectEvents() + { + OnAuthenticationFailed = c => + { + c.HandleResponse(); + + c.Response.StatusCode = 500; + c.Response.ContentType = "text/plain"; + if (Environment.IsDevelopment()) + { + // Debug only, in production do not share exceptions with the remote host. + return c.Response.WriteAsync(c.Exception.ToString()); + } + return c.Response.WriteAsync("An error occurred processing your authentication."); + } + }; + }); } - public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) + public void Configure(IApplicationBuilder app, IOptionsMonitor optionsMonitor) { - loggerfactory.AddConsole(LogLevel.Information); + app.UseDeveloperExceptionPage(); + app.UseAuthentication(); - // Simple error page - app.Use(async (context, next) => + app.Run(async context => { - try + var response = context.Response; + + if (context.Request.Path.Equals("/signedout")) { - await next(); + await WriteHtmlAsync(response, async res => + { + await res.WriteAsync($"

You have been signed out.

"); + await res.WriteAsync("Home"); + }); + return; } - catch (Exception ex) + + if (context.Request.Path.Equals("/signout")) { - if (!context.Response.HasStarted) + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await WriteHtmlAsync(response, async res => { - context.Response.Clear(); - context.Response.StatusCode = 500; - await context.Response.WriteAsync(ex.ToString()); - } - else + await res.WriteAsync($"

Signed out {HtmlEncode(context.User.Identity.Name)}

"); + await res.WriteAsync("Home"); + }); + return; + } + + if (context.Request.Path.Equals("/signout-remote")) + { + // Redirects + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties() { - throw; - } + RedirectUri = "/signedout" + }); + return; } - }); - app.UseCookieAuthentication(new CookieAuthenticationOptions()); + if (context.Request.Path.Equals("/access-denied-from-remote")) + { + await WriteHtmlAsync(response, async res => + { + await res.WriteAsync($"

Access Denied error received from the remote authorization server

"); + await res.WriteAsync("Home"); + }); + return; + } - app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions - { - ClientId = Configuration["oidc:clientid"], - ClientSecret = Configuration["oidc:clientsecret"], // for code flow - Authority = Configuration["oidc:authority"], - ResponseType = OpenIdConnectResponseTypes.Code, - GetClaimsFromUserInfoEndpoint = true - }); + if (context.Request.Path.Equals("/Account/AccessDenied")) + { + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await WriteHtmlAsync(response, async res => + { + await res.WriteAsync($"

Access Denied for user {HtmlEncode(context.User.Identity.Name)} to resource '{HtmlEncode(context.Request.Query["ReturnUrl"])}'

"); + await res.WriteAsync("Sign Out"); + await res.WriteAsync("Home"); + }); + return; + } - app.Run(async context => - { - if (context.Request.Path.Equals("/signout")) + // DefaultAuthenticateScheme causes User to be set + // var user = context.User; + + // This is what [Authorize] calls + var userResult = await context.AuthenticateAsync(); + var user = userResult.Principal; + var props = userResult.Properties; + + // This is what [Authorize(ActiveAuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)] calls + // var user = await context.AuthenticateAsync(OpenIdConnectDefaults.AuthenticationScheme); + + // Not authenticated + if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated)) { - await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - context.Response.ContentType = "text/html"; - await context.Response.WriteAsync($"Signing out {context.User.Identity.Name}
{Environment.NewLine}"); - await context.Response.WriteAsync("Sign In"); - await context.Response.WriteAsync($""); + // This is what [Authorize] calls + await context.ChallengeAsync(); + + // This is what [Authorize(ActiveAuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)] calls + // await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme); + return; } - if (!context.User.Identities.Any(identity => identity.IsAuthenticated)) + // Authenticated, but not authorized + if (context.Request.Path.Equals("/restricted") && !user.Identities.Any(identity => identity.HasClaim("special", "true"))) { - await context.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" }); + await context.ForbidAsync(); + return; + } + + if (context.Request.Path.Equals("/refresh")) + { + var refreshToken = props.GetTokenValue("refresh_token"); + + if (string.IsNullOrEmpty(refreshToken)) + { + await WriteHtmlAsync(response, async res => + { + await res.WriteAsync($"No refresh_token is available.
"); + await res.WriteAsync("Sign Out"); + }); + + return; + } + + var options = optionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme); + var metadata = await options.ConfigurationManager.GetConfigurationAsync(context.RequestAborted); + + var pairs = new Dictionary() + { + { "client_id", options.ClientId }, + { "client_secret", options.ClientSecret }, + { "grant_type", "refresh_token" }, + { "refresh_token", refreshToken } + }; + var content = new FormUrlEncodedContent(pairs); + var tokenResponse = await options.Backchannel.PostAsync(metadata.TokenEndpoint, content, context.RequestAborted); + tokenResponse.EnsureSuccessStatusCode(); + + var payload = JObject.Parse(await tokenResponse.Content.ReadAsStringAsync()); + + // Persist the new acess token + props.UpdateTokenValue("access_token", payload.Value("access_token")); + props.UpdateTokenValue("refresh_token", payload.Value("refresh_token")); + if (int.TryParse(payload.Value("expires_in"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds)) + { + var expiresAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(seconds); + props.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture)); + } + await context.SignInAsync(user, props); + + await WriteHtmlAsync(response, async res => + { + await res.WriteAsync($"

Refreshed.

"); + await res.WriteAsync("Refresh tokens"); + await res.WriteAsync("Home"); + + await res.WriteAsync("

Tokens:

"); + await WriteTableHeader(res, new string[] { "Token Type", "Value" }, props.GetTokens().Select(token => new string[] { token.Name, token.Value })); + + await res.WriteAsync("

Payload:

"); + await res.WriteAsync(HtmlEncoder.Default.Encode(payload.ToString()).Replace(",", ",
") + "
"); + }); + return; } - context.Response.ContentType = "text/html"; - await context.Response.WriteAsync($"Hello Authenticated User {context.User.Identity.Name}
{Environment.NewLine}"); - foreach (var claim in context.User.Claims) + if (context.Request.Path.Equals("/login-challenge")) { - await context.Response.WriteAsync($"{claim.Type}: {claim.Value}
{Environment.NewLine}"); + // Challenge the user authentication, and force a login prompt by overwriting the + // "prompt". This could be used for example to require the user to re-enter their + // credentials at the authentication provider, to add an extra confirmation layer. + await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new OpenIdConnectChallengeProperties() + { + Prompt = "login", + + // it is also possible to specify different scopes, e.g. + // Scope = new string[] { "openid", "profile", "other" } + }); + + return; } - await context.Response.WriteAsync("Sign Out"); - await context.Response.WriteAsync($""); + + await WriteHtmlAsync(response, async res => + { + await res.WriteAsync($"

Hello Authenticated User {HtmlEncode(user.Identity.Name)}

"); + await res.WriteAsync("Refresh tokens"); + await res.WriteAsync("Restricted"); + await res.WriteAsync("Login challenge"); + await res.WriteAsync("Sign Out"); + await res.WriteAsync("Sign Out Remote"); + + await res.WriteAsync("

Claims:

"); + await WriteTableHeader(res, new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value })); + + await res.WriteAsync("

Tokens:

"); + await WriteTableHeader(res, new string[] { "Token Type", "Value" }, props.GetTokens().Select(token => new string[] { token.Name, token.Value })); + }); }); } - public static void Main(string[] args) + private static async Task WriteHtmlAsync(HttpResponse response, Func writeContent) + { + var bootstrap = ""; + + response.ContentType = "text/html"; + await response.WriteAsync($"{bootstrap}
"); + await writeContent(response); + await response.WriteAsync("
"); + } + + private static async Task WriteTableHeader(HttpResponse response, IEnumerable columns, IEnumerable> data) { - var host = new WebHostBuilder() - .UseDefaultHostingConfiguration(args) - .UseKestrel() - .UseIISIntegration() - .UseStartup() - .Build(); - - host.Run(); + await response.WriteAsync(""); + await response.WriteAsync(""); + foreach (var column in columns) + { + await response.WriteAsync($""); + } + await response.WriteAsync(""); + foreach (var row in data) + { + await response.WriteAsync(""); + foreach (var column in row) + { + await response.WriteAsync($""); + } + await response.WriteAsync(""); + } + await response.WriteAsync("
{HtmlEncode(column)}
{HtmlEncode(column)}
"); } + + private static string HtmlEncode(string content) => + string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content); } } diff --git a/samples/OpenIdConnectSample/compiler/resources/cert.pfx b/samples/OpenIdConnectSample/compiler/resources/cert.pfx new file mode 100644 index 000000000..7118908c2 Binary files /dev/null and b/samples/OpenIdConnectSample/compiler/resources/cert.pfx differ diff --git a/samples/OpenIdConnectSample/project.json b/samples/OpenIdConnectSample/project.json deleted file mode 100644 index 553e170fc..000000000 --- a/samples/OpenIdConnectSample/project.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1-*", - "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-*", - "Microsoft.AspNetCore.Authentication.OpenIdConnect": "0.1.0-*", - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-*", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*", - "Microsoft.Extensions.Configuration.UserSecrets": "1.0.0-*", - "Microsoft.Extensions.Logging.Console": "1.0.0-*" - }, - "frameworks": { - "net451": {}, - "netcoreapp1.0": { - "imports": [ - "dnxcore50" - ], - "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.0.0-*", - "type": "platform" - } - } - } - }, - "compilationOptions": { - "emitEntryPoint": true - }, - "content": [ - "project.json", - "web.config" - ], - "userSecretsId": "aspnet5-OpenIdConnectSample-20151210110318" -} \ No newline at end of file diff --git a/samples/OpenIdConnectSample/web.config b/samples/OpenIdConnectSample/web.config deleted file mode 100644 index b1b731cce..000000000 --- a/samples/OpenIdConnectSample/web.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/samples/SocialSample/Program.cs b/samples/SocialSample/Program.cs new file mode 100644 index 000000000..a712b6c03 --- /dev/null +++ b/samples/SocialSample/Program.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; +using System.Net; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; + +namespace SocialSample +{ + public static class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging(factory => + { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + }) + .UseKestrel(options => + { + options.Listen(IPAddress.Loopback, 44318, listenOptions => + { + // Configure SSL + var serverCertificate = LoadCertificate(); + listenOptions.UseHttps(serverCertificate); + }); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + + private static X509Certificate2 LoadCertificate() + { + var socialSampleAssembly = typeof(Startup).GetTypeInfo().Assembly; + var embeddedFileProvider = new EmbeddedFileProvider(socialSampleAssembly, "SocialSample"); + var certificateFileInfo = embeddedFileProvider.GetFileInfo("compiler/resources/cert.pfx"); + using (var certificateStream = certificateFileInfo.CreateReadStream()) + { + byte[] certificatePayload; + using (var memoryStream = new MemoryStream()) + { + certificateStream.CopyTo(memoryStream); + certificatePayload = memoryStream.ToArray(); + } + + return new X509Certificate2(certificatePayload, "testPassword"); + } + } + } +} diff --git a/samples/SocialSample/Properties/launchSettings.json b/samples/SocialSample/Properties/launchSettings.json index e9d26ad03..30bf2e5f6 100644 --- a/samples/SocialSample/Properties/launchSettings.json +++ b/samples/SocialSample/Properties/launchSettings.json @@ -13,15 +13,15 @@ "launchBrowser": true, "launchUrl": "https://localhost:44318/", "environmentVariables": { - "ASPNET_ENV": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" } }, - "web": { - "commandName": "web", + "SocialSample": { + "commandName": "Project", "launchBrowser": true, - "launchUrl": "https://localhost:54541/", + "applicationUrl": "https://localhost:44318/", "environmentVariables": { - "Hosting:Environment": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/samples/SocialSample/SocialSample.csproj b/samples/SocialSample/SocialSample.csproj new file mode 100644 index 000000000..e1e2262b0 --- /dev/null +++ b/samples/SocialSample/SocialSample.csproj @@ -0,0 +1,36 @@ + + + + netcoreapp3.0 + aspnet5-SocialSample-20151210111056 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/SocialSample/SocialSample.xproj b/samples/SocialSample/SocialSample.xproj deleted file mode 100644 index 775eebbc2..000000000 --- a/samples/SocialSample/SocialSample.xproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 8c73d216-332d-41d8-bfd0-45bc4bc36552 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - 54540 - - - \ No newline at end of file diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs index f109d65fe..b5032072b 100644 --- a/samples/SocialSample/Startup.cs +++ b/samples/SocialSample/Startup.cs @@ -1,15 +1,15 @@ using System; -using System.IO; +using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; -using System.Reflection; using System.Security.Claims; -using System.Security.Cryptography.X509Certificates; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.Facebook; using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Authentication.MicrosoftAccount; using Microsoft.AspNetCore.Authentication.OAuth; @@ -17,13 +17,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.AspNetCore.Server.Kestrel.Filter; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; namespace SocialSample @@ -31,171 +28,175 @@ namespace SocialSample /* Note all servers must use the same address and port because these are pre-registered with the various providers. */ public class Startup { - public Startup() + public Startup(IHostingEnvironment env) { - Configuration = new ConfigurationBuilder() - .AddEnvironmentVariables() - .AddJsonFile("config.json") - .AddUserSecrets() - .Build(); + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true); + + if (env.IsDevelopment()) + { + // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 + builder.AddUserSecrets(); + } + + builder.AddEnvironmentVariables(); + Configuration = builder.Build(); } public IConfiguration Configuration { get; set; } public void ConfigureServices(IServiceCollection services) { - services.AddAuthentication(options => options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme); - } - - public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) - { - loggerfactory.AddConsole(LogLevel.Information); - - // Simple error page to avoid a repo dependency. - app.Use(async (context, next) => - { - try - { - await next(); - } - catch (Exception ex) - { - if (context.Response.HasStarted) - { - throw; - } - context.Response.StatusCode = 500; - await context.Response.WriteAsync(ex.ToString()); - } - }); - - app.UseCookieAuthentication(new CookieAuthenticationOptions + if (string.IsNullOrEmpty(Configuration["facebook:appid"])) { - AutomaticAuthenticate = true, - AutomaticChallenge = true, - LoginPath = new PathString("/login") - }); + // User-Secrets: https://docs.asp.net/en/latest/security/app-secrets.html + // See below for registration instructions for each provider. + throw new InvalidOperationException("User secrets must be configured for each authentication provider."); + } - // You must first create an app with facebook and add it's ID and Secret to your config.json or user-secrets. - // https://developers.facebook.com/apps/ - app.UseFacebookAuthentication(new FacebookOptions + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(o => o.LoginPath = new PathString("/login")) + // You must first create an app with Facebook and add its ID and Secret to your user-secrets. + // https://developers.facebook.com/apps/ + // https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow#login + .AddFacebook(o => { - AppId = Configuration["facebook:appid"], - AppSecret = Configuration["facebook:appsecret"], - Scope = { "email" }, - Fields = { "name", "email" }, - SaveTokens = true, - }); - - // See config.json - app.UseOAuthAuthentication(new OAuthOptions + o.AppId = Configuration["facebook:appid"]; + o.AppSecret = Configuration["facebook:appsecret"]; + o.Scope.Add("email"); + o.Fields.Add("name"); + o.Fields.Add("email"); + o.SaveTokens = true; + o.Events = new OAuthEvents() + { + OnRemoteFailure = HandleOnRemoteFailure + }; + }) + // You must first create an app with Google and add its ID and Secret to your user-secrets. + // https://console.developers.google.com/project + // https://developers.google.com/identity/protocols/OAuth2WebServer + // https://developers.google.com/+/web/people/ + .AddOAuth("Google-AccessToken", "Google AccessToken only", o => { - AuthenticationScheme = "Google-AccessToken", - DisplayName = "Google-AccessToken", - ClientId = Configuration["google:clientid"], - ClientSecret = Configuration["google:clientsecret"], - CallbackPath = new PathString("/signin-google-token"), - AuthorizationEndpoint = GoogleDefaults.AuthorizationEndpoint, - TokenEndpoint = GoogleDefaults.TokenEndpoint, - Scope = { "openid", "profile", "email" }, - SaveTokens = true - }); - - // See config.json - // https://console.developers.google.com/project - app.UseGoogleAuthentication(new GoogleOptions + o.ClientId = Configuration["google:clientid"]; + o.ClientSecret = Configuration["google:clientsecret"]; + o.CallbackPath = new PathString("/signin-google-token"); + o.AuthorizationEndpoint = GoogleDefaults.AuthorizationEndpoint; + o.TokenEndpoint = GoogleDefaults.TokenEndpoint; + o.Scope.Add("openid"); + o.Scope.Add("profile"); + o.Scope.Add("email"); + o.SaveTokens = true; + o.Events = new OAuthEvents() + { + OnRemoteFailure = HandleOnRemoteFailure + }; + }) + // You must first create an app with Google and add its ID and Secret to your user-secrets. + // https://console.developers.google.com/project + // https://developers.google.com/identity/protocols/OAuth2WebServer + // https://developers.google.com/+/web/people/ + .AddGoogle(o => { - ClientId = Configuration["google:clientid"], - ClientSecret = Configuration["google:clientsecret"], - SaveTokens = true, - Events = new OAuthEvents() + o.ClientId = Configuration["google:clientid"]; + o.ClientSecret = Configuration["google:clientsecret"]; + o.AuthorizationEndpoint += "?prompt=consent"; // Hack so we always get a refresh token, it only comes on the first authorization response + o.AccessType = "offline"; + o.SaveTokens = true; + o.Events = new OAuthEvents() { - OnRemoteFailure = ctx => - { - ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); - ctx.HandleResponse(); - return Task.FromResult(0); - } - } - }); - - // See config.json - // https://apps.twitter.com/ - app.UseTwitterAuthentication(new TwitterOptions + OnRemoteFailure = HandleOnRemoteFailure + }; + o.ClaimActions.MapJsonSubKey("urn:google:image", "image", "url"); + o.ClaimActions.Remove(ClaimTypes.GivenName); + }) + // You must first create an app with Twitter and add its key and Secret to your user-secrets. + // https://apps.twitter.com/ + // https://developer.twitter.com/en/docs/basics/authentication/api-reference/access_token + .AddTwitter(o => { - ConsumerKey = Configuration["twitter:consumerkey"], - ConsumerSecret = Configuration["twitter:consumersecret"], - SaveTokens = true, - Events = new TwitterEvents() + o.ConsumerKey = Configuration["twitter:consumerkey"]; + o.ConsumerSecret = Configuration["twitter:consumersecret"]; + // http://stackoverflow.com/questions/22627083/can-we-get-email-id-from-twitter-oauth-api/32852370#32852370 + // http://stackoverflow.com/questions/36330675/get-users-email-from-twitter-api-for-external-login-authentication-asp-net-mvc?lq=1 + o.RetrieveUserDetails = true; + o.SaveTokens = true; + o.ClaimActions.MapJsonKey("urn:twitter:profilepicture", "profile_image_url", ClaimTypes.Uri); + o.Events = new TwitterEvents() { - OnRemoteFailure = ctx => - { - ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); - ctx.HandleResponse(); - return Task.FromResult(0); - } - } - }); - - /* Azure AD app model v2 has restrictions that prevent the use of plain HTTP for redirect URLs. - Therefore, to authenticate through microsoft accounts, tryout the sample using the following URL: - https://localhost:54541/ - */ - // See config.json - // https://apps.dev.microsoft.com/ - app.UseOAuthAuthentication(new OAuthOptions + OnRemoteFailure = HandleOnRemoteFailure + }; + }) + /* Azure AD app model v2 has restrictions that prevent the use of plain HTTP for redirect URLs. + Therefore, to authenticate through microsoft accounts, tryout the sample using the following URL: + https://localhost:44318/ + */ + // You must first create an app with Microsoft Account and add its ID and Secret to your user-secrets. + // https://apps.dev.microsoft.com/ + .AddOAuth("Microsoft-AccessToken", "Microsoft AccessToken only", o => { - AuthenticationScheme = "Microsoft-AccessToken", - DisplayName = "MicrosoftAccount-AccessToken", - ClientId = Configuration["msa:clientid"], - ClientSecret = Configuration["msa:clientsecret"], - CallbackPath = new PathString("/signin-microsoft-token"), - AuthorizationEndpoint = MicrosoftAccountDefaults.AuthorizationEndpoint, - TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint, - Scope = { "https://graph.microsoft.com/user.read" }, - SaveTokens = true - }); - - // See config.json - // https://azure.microsoft.com/en-us/documentation/articles/active-directory-v2-app-registration/ - app.UseMicrosoftAccountAuthentication(new MicrosoftAccountOptions + o.ClientId = Configuration["microsoftaccount:clientid"]; + o.ClientSecret = Configuration["microsoftaccount:clientsecret"]; + o.CallbackPath = new PathString("/signin-microsoft-token"); + o.AuthorizationEndpoint = MicrosoftAccountDefaults.AuthorizationEndpoint; + o.TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint; + o.Scope.Add("https://graph.microsoft.com/user.read"); + o.SaveTokens = true; + o.Events = new OAuthEvents() + { + OnRemoteFailure = HandleOnRemoteFailure + }; + }) + // You must first create an app with Microsoft Account and add its ID and Secret to your user-secrets. + // https://azure.microsoft.com/en-us/documentation/articles/active-directory-v2-app-registration/ + .AddMicrosoftAccount(o => { - DisplayName = "MicrosoftAccount", - ClientId = Configuration["msa:clientid"], - ClientSecret = Configuration["msa:clientsecret"], - SaveTokens = true - }); - - // See config.json - // https://github.com/settings/applications/ - app.UseOAuthAuthentication(new OAuthOptions + o.ClientId = Configuration["microsoftaccount:clientid"]; + o.ClientSecret = Configuration["microsoftaccount:clientsecret"]; + o.SaveTokens = true; + o.Scope.Add("offline_access"); + o.Events = new OAuthEvents() + { + OnRemoteFailure = HandleOnRemoteFailure + }; + }) + // You must first create an app with GitHub and add its ID and Secret to your user-secrets. + // https://github.com/settings/applications/ + .AddOAuth("GitHub-AccessToken", "GitHub AccessToken only", o => { - AuthenticationScheme = "GitHub-AccessToken", - DisplayName = "Github-AccessToken", - ClientId = Configuration["github-token:clientid"], - ClientSecret = Configuration["github-token:clientsecret"], - CallbackPath = new PathString("/signin-github-token"), - AuthorizationEndpoint = "https://github.com/login/oauth/authorize", - TokenEndpoint = "https://github.com/login/oauth/access_token", - SaveTokens = true - }); - - // See config.json - app.UseOAuthAuthentication(new OAuthOptions + o.ClientId = Configuration["github-token:clientid"]; + o.ClientSecret = Configuration["github-token:clientsecret"]; + o.CallbackPath = new PathString("/signin-github-token"); + o.AuthorizationEndpoint = "https://github.com/login/oauth/authorize"; + o.TokenEndpoint = "https://github.com/login/oauth/access_token"; + o.SaveTokens = true; + o.Events = new OAuthEvents() + { + OnRemoteFailure = HandleOnRemoteFailure + }; + }) + // You must first create an app with GitHub and add its ID and Secret to your user-secrets. + // https://github.com/settings/applications/ + .AddOAuth("GitHub", "Github", o => { - AuthenticationScheme = "GitHub", - DisplayName = "Github", - ClientId = Configuration["github:clientid"], - ClientSecret = Configuration["github:clientsecret"], - CallbackPath = new PathString("/signin-github"), - AuthorizationEndpoint = "https://github.com/login/oauth/authorize", - TokenEndpoint = "https://github.com/login/oauth/access_token", - UserInformationEndpoint = "https://api.github.com/user", - ClaimsIssuer = "OAuth2-Github", - SaveTokens = true, + o.ClientId = Configuration["github:clientid"]; + o.ClientSecret = Configuration["github:clientsecret"]; + o.CallbackPath = new PathString("/signin-github"); + o.AuthorizationEndpoint = "https://github.com/login/oauth/authorize"; + o.TokenEndpoint = "https://github.com/login/oauth/access_token"; + o.UserInformationEndpoint = "https://api.github.com/user"; + o.ClaimsIssuer = "OAuth2-Github"; + o.SaveTokens = true; // Retrieving user information is unique to each provider. - Events = new OAuthEvents + o.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + o.ClaimActions.MapJsonKey(ClaimTypes.Name, "login"); + o.ClaimActions.MapJsonKey("urn:github:name", "name"); + o.ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email); + o.ClaimActions.MapJsonKey("urn:github:url", "url"); + o.Events = new OAuthEvents { + OnRemoteFailure = HandleOnRemoteFailure, OnCreatingTicket = async context => { // Get the GitHub user @@ -208,63 +209,185 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) var user = JObject.Parse(await response.Content.ReadAsStringAsync()); - var identifier = user.Value("id"); - if (!string.IsNullOrEmpty(identifier)) - { - context.Identity.AddClaim(new Claim( - ClaimTypes.NameIdentifier, identifier, - ClaimValueTypes.String, context.Options.ClaimsIssuer)); - } - - var userName = user.Value("login"); - if (!string.IsNullOrEmpty(userName)) - { - context.Identity.AddClaim(new Claim( - ClaimsIdentity.DefaultNameClaimType, userName, - ClaimValueTypes.String, context.Options.ClaimsIssuer)); - } + context.RunClaimActions(user); + } + }; + }); + } - var name = user.Value("name"); - if (!string.IsNullOrEmpty(name)) - { - context.Identity.AddClaim(new Claim( - "urn:github:name", name, - ClaimValueTypes.String, context.Options.ClaimsIssuer)); - } + private async Task HandleOnRemoteFailure(RemoteFailureContext context) + { + context.Response.StatusCode = 500; + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync(""); + await context.Response.WriteAsync("A remote failure has occurred: " + UrlEncoder.Default.Encode(context.Failure.Message) + "
"); - var link = user.Value("url"); - if (!string.IsNullOrEmpty(link)) - { - context.Identity.AddClaim(new Claim( - "urn:github:url", link, - ClaimValueTypes.String, context.Options.ClaimsIssuer)); - } - } + if (context.Properties != null) + { + await context.Response.WriteAsync("Properties:
"); + foreach (var pair in context.Properties.Items) + { + await context.Response.WriteAsync($"-{ UrlEncoder.Default.Encode(pair.Key)}={ UrlEncoder.Default.Encode(pair.Value)}
"); } - }); + } + + await context.Response.WriteAsync("Home"); + await context.Response.WriteAsync(""); + + // context.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(context.Failure.Message)); + + context.HandleResponse(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + + app.UseAuthentication(); // Choose an authentication type - app.Map("/login", signoutApp => + app.Map("/login", signinApp => { - signoutApp.Run(async context => + signinApp.Run(async context => { var authType = context.Request.Query["authscheme"]; if (!string.IsNullOrEmpty(authType)) { // By default the client will be redirect back to the URL that issued the challenge (/login?authtype=foo), // send them to the home page instead (/). - await context.Authentication.ChallengeAsync(authType, new AuthenticationProperties() { RedirectUri = "/" }); + await context.ChallengeAsync(authType, new AuthenticationProperties() { RedirectUri = "/" }); return; } - context.Response.ContentType = "text/html"; - await context.Response.WriteAsync(""); - await context.Response.WriteAsync("Choose an authentication scheme:
"); - foreach (var type in context.Authentication.GetAuthenticationSchemes()) + var response = context.Response; + response.ContentType = "text/html"; + await response.WriteAsync(""); + await response.WriteAsync("Choose an authentication scheme:
"); + var schemeProvider = context.RequestServices.GetRequiredService(); + foreach (var provider in await schemeProvider.GetAllSchemesAsync()) { - await context.Response.WriteAsync("" + (type.DisplayName ?? "(suppressed)") + "
"); + await response.WriteAsync("" + (provider.DisplayName ?? "(suppressed)") + "
"); } - await context.Response.WriteAsync(""); + await response.WriteAsync(""); + }); + }); + + // Refresh the access token + app.Map("/refresh_token", signinApp => + { + signinApp.Run(async context => + { + var response = context.Response; + + // Setting DefaultAuthenticateScheme causes User to be set + // var user = context.User; + + // This is what [Authorize] calls + var userResult = await context.AuthenticateAsync(); + var user = userResult.Principal; + var authProperties = userResult.Properties; + + // This is what [Authorize(ActiveAuthenticationSchemes = MicrosoftAccountDefaults.AuthenticationScheme)] calls + // var user = await context.AuthenticateAsync(MicrosoftAccountDefaults.AuthenticationScheme); + + // Deny anonymous request beyond this point. + if (!userResult.Succeeded || user == null || !user.Identities.Any(identity => identity.IsAuthenticated)) + { + // This is what [Authorize] calls + // The cookie middleware will handle this and redirect to /login + await context.ChallengeAsync(); + + // This is what [Authorize(ActiveAuthenticationSchemes = MicrosoftAccountDefaults.AuthenticationScheme)] calls + // await context.ChallengeAsync(MicrosoftAccountDefaults.AuthenticationScheme); + + return; + } + + var currentAuthType = user.Identities.First().AuthenticationType; + if (string.Equals(GoogleDefaults.AuthenticationScheme, currentAuthType) + || string.Equals(MicrosoftAccountDefaults.AuthenticationScheme, currentAuthType)) + { + var refreshToken = authProperties.GetTokenValue("refresh_token"); + + if (string.IsNullOrEmpty(refreshToken)) + { + response.ContentType = "text/html"; + await response.WriteAsync(""); + await response.WriteAsync("No refresh_token is available.
"); + await response.WriteAsync("Home"); + await response.WriteAsync(""); + return; + } + + var options = await GetOAuthOptionsAsync(context, currentAuthType); + + var pairs = new Dictionary() + { + { "client_id", options.ClientId }, + { "client_secret", options.ClientSecret }, + { "grant_type", "refresh_token" }, + { "refresh_token", refreshToken } + }; + var content = new FormUrlEncodedContent(pairs); + var refreshResponse = await options.Backchannel.PostAsync(options.TokenEndpoint, content, context.RequestAborted); + refreshResponse.EnsureSuccessStatusCode(); + + var payload = JObject.Parse(await refreshResponse.Content.ReadAsStringAsync()); + + // Persist the new acess token + authProperties.UpdateTokenValue("access_token", payload.Value("access_token")); + refreshToken = payload.Value("refresh_token"); + if (!string.IsNullOrEmpty(refreshToken)) + { + authProperties.UpdateTokenValue("refresh_token", refreshToken); + } + if (int.TryParse(payload.Value("expires_in"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds)) + { + var expiresAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(seconds); + authProperties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture)); + } + await context.SignInAsync(user, authProperties); + + await PrintRefreshedTokensAsync(response, payload, authProperties); + + return; + } + // https://developers.facebook.com/docs/facebook-login/access-tokens/expiration-and-extension + else if (string.Equals(FacebookDefaults.AuthenticationScheme, currentAuthType)) + { + var options = await GetOAuthOptionsAsync(context, currentAuthType); + + var accessToken = authProperties.GetTokenValue("access_token"); + + var query = new QueryBuilder() + { + { "grant_type", "fb_exchange_token" }, + { "client_id", options.ClientId }, + { "client_secret", options.ClientSecret }, + { "fb_exchange_token", accessToken }, + }.ToQueryString(); + + var refreshResponse = await options.Backchannel.GetStringAsync(options.TokenEndpoint + query); + var payload = JObject.Parse(refreshResponse); + + authProperties.UpdateTokenValue("access_token", payload.Value("access_token")); + if (int.TryParse(payload.Value("expires_in"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds)) + { + var expiresAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(seconds); + authProperties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture)); + } + await context.SignInAsync(user, authProperties); + + await PrintRefreshedTokensAsync(response, payload, authProperties); + + return; + } + + response.ContentType = "text/html"; + await response.WriteAsync(""); + await response.WriteAsync("Refresh has not been implemented for this provider.
"); + await response.WriteAsync("Home"); + await response.WriteAsync(""); }); }); @@ -273,12 +396,13 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) { signoutApp.Run(async context => { - context.Response.ContentType = "text/html"; - await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - await context.Response.WriteAsync(""); - await context.Response.WriteAsync("You have been logged out. Goodbye " + context.User.Identity.Name + "
"); - await context.Response.WriteAsync("Home"); - await context.Response.WriteAsync(""); + var response = context.Response; + response.ContentType = "text/html"; + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await response.WriteAsync(""); + await response.WriteAsync("You have been logged out. Goodbye " + context.User.Identity.Name + "
"); + await response.WriteAsync("Home"); + await response.WriteAsync(""); }); }); @@ -287,81 +411,97 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) { errorApp.Run(async context => { - context.Response.ContentType = "text/html"; - await context.Response.WriteAsync(""); - await context.Response.WriteAsync("An remote failure has occurred: " + context.Request.Query["FailureMessage"] + "
"); - await context.Response.WriteAsync("Home"); - await context.Response.WriteAsync(""); + var response = context.Response; + response.ContentType = "text/html"; + await response.WriteAsync(""); + await response.WriteAsync("An remote failure has occurred: " + context.Request.Query["FailureMessage"] + "
"); + await response.WriteAsync("Home"); + await response.WriteAsync(""); }); }); - // Deny anonymous request beyond this point. - app.Use(async (context, next) => + + app.Run(async context => { - if (!context.User.Identities.Any(identity => identity.IsAuthenticated)) + // Setting DefaultAuthenticateScheme causes User to be set + var user = context.User; + + // This is what [Authorize] calls + // var user = await context.AuthenticateAsync(); + + // This is what [Authorize(ActiveAuthenticationSchemes = MicrosoftAccountDefaults.AuthenticationScheme)] calls + // var user = await context.AuthenticateAsync(MicrosoftAccountDefaults.AuthenticationScheme); + + // Deny anonymous request beyond this point. + if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated)) { - // The cookie middleware will intercept this 401 and redirect to /login - await context.Authentication.ChallengeAsync(); + // This is what [Authorize] calls + // The cookie middleware will handle this and redirect to /login + await context.ChallengeAsync(); + + // This is what [Authorize(ActiveAuthenticationSchemes = MicrosoftAccountDefaults.AuthenticationScheme)] calls + // await context.ChallengeAsync(MicrosoftAccountDefaults.AuthenticationScheme); + return; } - await next(); - }); - // Display user information - app.Run(async context => - { - context.Response.ContentType = "text/html"; - await context.Response.WriteAsync(""); - await context.Response.WriteAsync("Hello " + (context.User.Identity.Name ?? "anonymous") + "
"); + // Display user information + var response = context.Response; + response.ContentType = "text/html"; + await response.WriteAsync(""); + await response.WriteAsync("Hello " + (context.User.Identity.Name ?? "anonymous") + "
"); foreach (var claim in context.User.Claims) { - await context.Response.WriteAsync(claim.Type + ": " + claim.Value + "
"); + await response.WriteAsync(claim.Type + ": " + claim.Value + "
"); } - await context.Response.WriteAsync("Tokens:
"); - - await context.Response.WriteAsync("Access Token: " + await context.Authentication.GetTokenAsync("access_token") + "
"); - await context.Response.WriteAsync("Refresh Token: " + await context.Authentication.GetTokenAsync("refresh_token") + "
"); - await context.Response.WriteAsync("Token Type: " + await context.Authentication.GetTokenAsync("token_type") + "
"); - await context.Response.WriteAsync("expires_at: " + await context.Authentication.GetTokenAsync("expires_at") + "
"); - await context.Response.WriteAsync("Logout"); - await context.Response.WriteAsync(""); + await response.WriteAsync("Tokens:
"); + + await response.WriteAsync("Access Token: " + await context.GetTokenAsync("access_token") + "
"); + await response.WriteAsync("Refresh Token: " + await context.GetTokenAsync("refresh_token") + "
"); + await response.WriteAsync("Token Type: " + await context.GetTokenAsync("token_type") + "
"); + await response.WriteAsync("expires_at: " + await context.GetTokenAsync("expires_at") + "
"); + await response.WriteAsync("Logout
"); + await response.WriteAsync("Refresh Token
"); + await response.WriteAsync(""); }); } - public static void Main(string[] args) + private Task GetOAuthOptionsAsync(HttpContext context, string currentAuthType) { - var host = new WebHostBuilder() - .UseDefaultHostingConfiguration(args) - .UseKestrel(options => - { - //Configure SSL - var serverCertificate = LoadCertificate(); - options.UseHttps(serverCertificate); - }) - .UseIISIntegration() - .UseStartup() - .Build(); - - host.Run(); + if (string.Equals(GoogleDefaults.AuthenticationScheme, currentAuthType)) + { + return Task.FromResult(context.RequestServices.GetRequiredService>().Get(currentAuthType)); + } + else if (string.Equals(MicrosoftAccountDefaults.AuthenticationScheme, currentAuthType)) + { + return Task.FromResult(context.RequestServices.GetRequiredService>().Get(currentAuthType)); + } + else if (string.Equals(FacebookDefaults.AuthenticationScheme, currentAuthType)) + { + return Task.FromResult(context.RequestServices.GetRequiredService>().Get(currentAuthType)); + } + + throw new NotImplementedException(currentAuthType); } - private static X509Certificate2 LoadCertificate() + private async Task PrintRefreshedTokensAsync(HttpResponse response, JObject payload, AuthenticationProperties authProperties) { - var socialSampleAssembly = typeof(Startup).GetTypeInfo().Assembly; - var embeddedFileProvider = new EmbeddedFileProvider(socialSampleAssembly, "SocialSample"); - var certificateFileInfo = embeddedFileProvider.GetFileInfo("compiler/resources/cert.pfx"); - using (var certificateStream = certificateFileInfo.CreateReadStream()) - { - byte[] certificatePayload; - using (var memoryStream = new MemoryStream()) - { - certificateStream.CopyTo(memoryStream); - certificatePayload = memoryStream.ToArray(); - } + response.ContentType = "text/html"; + await response.WriteAsync(""); + await response.WriteAsync("Refreshed.
"); + await response.WriteAsync(HtmlEncoder.Default.Encode(payload.ToString()).Replace(",", ",
") + "
"); - return new X509Certificate2(certificatePayload, "testPassword"); - } + await response.WriteAsync("
Tokens:
"); + + await response.WriteAsync("Access Token: " + authProperties.GetTokenValue("access_token") + "
"); + await response.WriteAsync("Refresh Token: " + authProperties.GetTokenValue("refresh_token") + "
"); + await response.WriteAsync("Token Type: " + authProperties.GetTokenValue("token_type") + "
"); + await response.WriteAsync("expires_at: " + authProperties.GetTokenValue("expires_at") + "
"); + + await response.WriteAsync("Home
"); + await response.WriteAsync("Refresh Token
"); + await response.WriteAsync(""); } } } diff --git a/samples/SocialSample/config.json b/samples/SocialSample/config.json deleted file mode 100644 index 5c1453e39..000000000 --- a/samples/SocialSample/config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "google:clientid": "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com", - "google:clientsecret": "n2Q-GEw9RQjzcRbU3qhfTj8f", - "twitter:consumerkey": "6XaCTaLbMqfj6ww3zvZ5g", - "twitter:consumersecret": "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI", - "github:clientid": "49e302895d8b09ea5656", - "github:clientsecret": "98f1bf028608901e9df91d64ee61536fe562064b", - "github-token:clientid": "8c0c5a572abe8fe89588", - "github-token:clientsecret": "e1d95eaf03461d27acd6f49d4fc7bf19d6ac8cda", - "msa:clientid": "e2105565-1f56-434a-ae61-9849ebaf606c", - "msa:clientsecret": "pjqtt3RXrFwcfSJyQ0BeUez" -} diff --git a/samples/SocialSample/project.json b/samples/SocialSample/project.json deleted file mode 100644 index fe1d2bfdc..000000000 --- a/samples/SocialSample/project.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1-*", - "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-*", - "Microsoft.AspNetCore.Authentication.Facebook": "1.0.0-*", - "Microsoft.AspNetCore.Authentication.Google": "1.0.0-*", - "Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.0.0-*", - "Microsoft.AspNetCore.Authentication.Twitter": "1.0.0-*", - "Microsoft.AspNetCore.DataProtection": "1.0.0-*", - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-*", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*", - "Microsoft.AspNetCore.Server.Kestrel.Https": "1.0.0-*", - "Microsoft.Extensions.Configuration.UserSecrets": "1.0.0-*", - "Microsoft.Extensions.FileProviders.Embedded": "1.0.0-*", - "Microsoft.Extensions.Logging.Console": "1.0.0-*" - }, - "compilationOptions": { - "emitEntryPoint": true - }, - "frameworks": { - "net451": {}, - "netcoreapp1.0": { - "imports": [ - "dnxcore50" - ], - "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.0.0-*", - "type": "platform" - } - } - } - }, - "userSecretsId": "aspnet5-SocialSample-20151210111056", - "content": [ - "config.json", - "project.json", - "web.config" - ] -} \ No newline at end of file diff --git a/samples/SocialSample/web.config b/samples/SocialSample/web.config index 5611f93f1..f7ac67933 100644 --- a/samples/SocialSample/web.config +++ b/samples/SocialSample/web.config @@ -4,6 +4,6 @@ - + \ No newline at end of file diff --git a/samples/WsFedSample/Program.cs b/samples/WsFedSample/Program.cs new file mode 100644 index 000000000..40e1945c6 --- /dev/null +++ b/samples/WsFedSample/Program.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; + +namespace WsFedSample +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .ConfigureLogging(factory => + { + factory.AddConsole(); + factory.AddDebug(); + factory.AddFilter("Console", level => level >= LogLevel.Information); + factory.AddFilter("Debug", level => level >= LogLevel.Information); + }) + .UseKestrel(options => + { + options.Listen(IPAddress.Loopback, 44307, listenOptions => + { + // Configure SSL + var serverCertificate = LoadCertificate(); + listenOptions.UseHttps(serverCertificate); + }); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + + private static X509Certificate2 LoadCertificate() + { + var assembly = typeof(Startup).GetTypeInfo().Assembly; + var embeddedFileProvider = new EmbeddedFileProvider(assembly, "WsFedSample"); + var certificateFileInfo = embeddedFileProvider.GetFileInfo("compiler/resources/cert.pfx"); + using (var certificateStream = certificateFileInfo.CreateReadStream()) + { + byte[] certificatePayload; + using (var memoryStream = new MemoryStream()) + { + certificateStream.CopyTo(memoryStream); + certificatePayload = memoryStream.ToArray(); + } + + return new X509Certificate2(certificatePayload, "testPassword"); + } + } + } +} diff --git a/samples/WsFedSample/Properties/launchSettings.json b/samples/WsFedSample/Properties/launchSettings.json new file mode 100644 index 000000000..bdf80e248 --- /dev/null +++ b/samples/WsFedSample/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "https://localhost:44307/", + "sslPort": 44318 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "https://localhost:44307/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "WsFedSample": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:44307/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/samples/WsFedSample/Startup.cs b/samples/WsFedSample/Startup.cs new file mode 100644 index 000000000..0fc32769e --- /dev/null +++ b/samples/WsFedSample/Startup.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.WsFederation; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace WsFedSample +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme; + }) + .AddWsFederation(options => + { + options.Wtrealm = "https://Tratcheroutlook.onmicrosoft.com/WsFedSample"; + options.MetadataAddress = "https://login.windows.net/cdc690f9-b6b8-4023-813a-bae7143d1f87/FederationMetadata/2007-06/FederationMetadata.xml"; + // options.CallbackPath = "/"; + // options.SkipUnrecognizedRequests = true; + }) + .AddCookie(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + app.UseAuthentication(); + + app.Run(async context => + { + if (context.Request.Path.Equals("/signedout")) + { + await WriteHtmlAsync(context.Response, async res => + { + await res.WriteAsync($"

You have been signed out.

"); + await res.WriteAsync("Sign In"); + }); + return; + } + + if (context.Request.Path.Equals("/signout")) + { + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await WriteHtmlAsync(context.Response, async res => + { + await context.Response.WriteAsync($"

Signed out {HtmlEncode(context.User.Identity.Name)}

"); + await context.Response.WriteAsync("Sign In"); + }); + return; + } + + if (context.Request.Path.Equals("/signout-remote")) + { + // Redirects + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await context.SignOutAsync(WsFederationDefaults.AuthenticationScheme, new AuthenticationProperties() + { + RedirectUri = "/signedout" + }); + return; + } + + if (context.Request.Path.Equals("/Account/AccessDenied")) + { + await WriteHtmlAsync(context.Response, async res => + { + await context.Response.WriteAsync($"

Access Denied for user {HtmlEncode(context.User.Identity.Name)} to resource '{HtmlEncode(context.Request.Query["ReturnUrl"])}'

"); + await context.Response.WriteAsync("Sign Out"); + }); + return; + } + + // DefaultAuthenticateScheme causes User to be set + var user = context.User; + + // This is what [Authorize] calls + // var user = await context.AuthenticateAsync(); + + // This is what [Authorize(ActiveAuthenticationSchemes = WsFederationDefaults.AuthenticationScheme)] calls + // var user = await context.AuthenticateAsync(WsFederationDefaults.AuthenticationScheme); + + // Not authenticated + if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated)) + { + // This is what [Authorize] calls + await context.ChallengeAsync(); + + // This is what [Authorize(ActiveAuthenticationSchemes = WsFederationDefaults.AuthenticationScheme)] calls + // await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme); + + return; + } + + // Authenticated, but not authorized + if (context.Request.Path.Equals("/restricted") && !user.Identities.Any(identity => identity.HasClaim("special", "true"))) + { + await context.ForbidAsync(); + return; + } + + await WriteHtmlAsync(context.Response, async response => + { + await response.WriteAsync($"

Hello Authenticated User {HtmlEncode(user.Identity.Name)}

"); + await response.WriteAsync("Restricted"); + await response.WriteAsync("Sign Out"); + await response.WriteAsync("Sign Out Remote"); + + await response.WriteAsync("

Claims:

"); + await WriteTableHeader(response, new string[] { "Claim Type", "Value" }, context.User.Claims.Select(c => new string[] { c.Type, c.Value })); + }); + }); + } + + private static async Task WriteHtmlAsync(HttpResponse response, Func writeContent) + { + var bootstrap = ""; + + response.ContentType = "text/html"; + await response.WriteAsync($"{bootstrap}
"); + await writeContent(response); + await response.WriteAsync("
"); + } + + private static async Task WriteTableHeader(HttpResponse response, IEnumerable columns, IEnumerable> data) + { + await response.WriteAsync(""); + await response.WriteAsync(""); + foreach (var column in columns) + { + await response.WriteAsync($""); + } + await response.WriteAsync(""); + foreach (var row in data) + { + await response.WriteAsync(""); + foreach (var column in row) + { + await response.WriteAsync($""); + } + await response.WriteAsync(""); + } + await response.WriteAsync("
{HtmlEncode(column)}
{HtmlEncode(column)}
"); + } + + private static string HtmlEncode(string content) => + string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content); + } +} diff --git a/samples/WsFedSample/WsFedSample.csproj b/samples/WsFedSample/WsFedSample.csproj new file mode 100644 index 000000000..e1191ac81 --- /dev/null +++ b/samples/WsFedSample/WsFedSample.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp3.0 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/WsFedSample/compiler/resources/cert.pfx b/samples/WsFedSample/compiler/resources/cert.pfx new file mode 100644 index 000000000..7118908c2 Binary files /dev/null and b/samples/WsFedSample/compiler/resources/cert.pfx differ diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/ChunkingCookieManager.cs b/shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs similarity index 67% rename from src/Microsoft.AspNetCore.Authentication.Cookies/ChunkingCookieManager.cs rename to shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs index 380e6f937..42cc4e2f0 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/ChunkingCookieManager.cs +++ b/shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs @@ -5,11 +5,13 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text.Encodings.Web; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; +// Keep the type public for Security repo as it would be a breaking change to change the accessor now. +// Make this type internal for other repos as it could be used by multiple projects and having it public causes type conflicts. +#if SECURITY namespace Microsoft.AspNetCore.Authentication.Cookies { /// @@ -18,13 +20,30 @@ namespace Microsoft.AspNetCore.Authentication.Cookies /// public class ChunkingCookieManager : ICookieManager { - public ChunkingCookieManager(UrlEncoder urlEncoder) +#else +namespace Microsoft.AspNetCore.Internal +{ + /// + /// This handles cookies that are limited by per cookie length. It breaks down long cookies for responses, and reassembles them + /// from requests. + /// + internal class ChunkingCookieManager + { +#endif + /// + /// The default maximum size of characters in a cookie to send back to the client. + /// + public const int DefaultChunkSize = 4050; + + private const string ChunkKeySuffix = "C"; + private const string ChunkCountPrefix = "chunks-"; + + public ChunkingCookieManager() { // Lowest common denominator. Safari has the lowest known limit (4093), and we leave little extra just in case. // See http://browsercookielimits.x64.me/. - ChunkSize = 4090; - ThrowForPartialCookies = true; - Encoder = urlEncoder ?? UrlEncoder.Default; + // Leave at least 40 in case CookiePolicy tries to add 'secure', 'samesite=strict' and/or 'httponly'. + ChunkSize = DefaultChunkSize; } /// @@ -41,14 +60,12 @@ public ChunkingCookieManager(UrlEncoder urlEncoder) /// public bool ThrowForPartialCookies { get; set; } - private UrlEncoder Encoder { get; set; } - - // Parse the "chunks:XX" to determine how many chunks there should be. + // Parse the "chunks-XX" to determine how many chunks there should be. private static int ParseChunksCount(string value) { - if (value != null && value.StartsWith("chunks:", StringComparison.Ordinal)) + if (value != null && value.StartsWith(ChunkCountPrefix, StringComparison.Ordinal)) { - var chunksCountString = value.Substring("chunks:".Length); + var chunksCountString = value.Substring(ChunkCountPrefix.Length); int chunksCount; if (int.TryParse(chunksCountString, NumberStyles.None, CultureInfo.InvariantCulture, out chunksCount)) { @@ -60,7 +77,7 @@ private static int ParseChunksCount(string value) /// /// Get the reassembled cookie. Non chunked cookies are returned normally. - /// Cookies with missing chunks just have their "chunks:XX" header returned. + /// Cookies with missing chunks just have their "chunks-XX" header returned. /// /// /// @@ -82,11 +99,10 @@ public string GetRequestCookie(HttpContext context, string key) var chunksCount = ParseChunksCount(value); if (chunksCount > 0) { - var quoted = false; var chunks = new string[chunksCount]; for (var chunkId = 1; chunkId <= chunksCount; chunkId++) { - var chunk = requestCookies[key + "C" + chunkId.ToString(CultureInfo.InvariantCulture)]; + var chunk = requestCookies[key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture)]; if (string.IsNullOrEmpty(chunk)) { if (ThrowForPartialCookies) @@ -97,25 +113,21 @@ public string GetRequestCookie(HttpContext context, string key) totalSize += chunks[i].Length; } throw new FormatException( - string.Format(CultureInfo.CurrentCulture, Resources.Exception_ImcompleteChunkedCookie, chunkId - 1, chunksCount, totalSize)); + string.Format( + CultureInfo.CurrentCulture, + "The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded.", + chunkId - 1, + chunksCount, + totalSize)); } // Missing chunk, abort by returning the original cookie value. It may have been a false positive? return value; } - if (IsQuoted(chunk)) - { - // Note: Since we assume these cookies were generated by our code, then we can assume that if one cookie has quotes then they all do. - quoted = true; - chunk = RemoveQuotes(chunk); - } + chunks[chunkId - 1] = chunk; } - var merged = string.Join(string.Empty, chunks); - if (quoted) - { - merged = Quote(merged); - } - return merged; + + return string.Join(string.Empty, chunks); } return value; } @@ -123,7 +135,7 @@ public string GetRequestCookie(HttpContext context, string key) /// /// Appends a new response cookie to the Set-Cookie header. If the cookie is larger than the given size limit /// then it will be broken down into multiple cookies as follows: - /// Set-Cookie: CookieName=chunks:3; path=/ + /// Set-Cookie: CookieName=chunks-3; path=/ /// Set-Cookie: CookieNameC1=Segment1; path=/ /// Set-Cookie: CookieNameC2=Segment2; path=/ /// Set-Cookie: CookieNameC3=Segment3; path=/ @@ -149,12 +161,11 @@ public void AppendResponseCookie(HttpContext context, string key, string value, throw new ArgumentNullException(nameof(options)); } - var escapedKey = Encoder.Encode(key); - - var template = new SetCookieHeaderValue(escapedKey) + var template = new SetCookieHeaderValue(key) { Domain = options.Domain, Expires = options.Expires, + SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite, HttpOnly = options.HttpOnly, Path = options.Path, Secure = options.Secure, @@ -163,55 +174,42 @@ public void AppendResponseCookie(HttpContext context, string key, string value, var templateLength = template.ToString().Length; value = value ?? string.Empty; - var quoted = false; - if (IsQuoted(value)) - { - quoted = true; - value = RemoveQuotes(value); - } - var escapedValue = Encoder.Encode(value); // Normal cookie - var responseHeaders = context.Response.Headers; - if (!ChunkSize.HasValue || ChunkSize.Value > templateLength + escapedValue.Length + (quoted ? 2 : 0)) + var responseCookies = context.Response.Cookies; + if (!ChunkSize.HasValue || ChunkSize.Value > templateLength + value.Length) { - template.Value = quoted ? Quote(escapedValue) : escapedValue; - responseHeaders.Append(Constants.Headers.SetCookie, template.ToString()); + responseCookies.Append(key, value, options); } - else if (ChunkSize.Value < templateLength + (quoted ? 2 : 0) + 10) + else if (ChunkSize.Value < templateLength + 10) { // 10 is the minimum data we want to put in an individual cookie, including the cookie chunk identifier "CXX". // No room for data, we can't chunk the options and name - throw new InvalidOperationException(Resources.Exception_CookieLimitTooSmall); + throw new InvalidOperationException("The cookie key and options are larger than ChunksSize, leaving no room for data."); } else { // Break the cookie down into multiple cookies. // Key = CookieName, value = "Segment1Segment2Segment2" - // Set-Cookie: CookieName=chunks:3; path=/ + // Set-Cookie: CookieName=chunks-3; path=/ // Set-Cookie: CookieNameC1="Segment1"; path=/ // Set-Cookie: CookieNameC2="Segment2"; path=/ // Set-Cookie: CookieNameC3="Segment3"; path=/ - var dataSizePerCookie = ChunkSize.Value - templateLength - (quoted ? 2 : 0) - 3; // Budget 3 chars for the chunkid. - var cookieChunkCount = (int)Math.Ceiling(escapedValue.Length * 1.0 / dataSizePerCookie); + var dataSizePerCookie = ChunkSize.Value - templateLength - 3; // Budget 3 chars for the chunkid. + var cookieChunkCount = (int)Math.Ceiling(value.Length * 1.0 / dataSizePerCookie); - template.Value = "chunks:" + cookieChunkCount.ToString(CultureInfo.InvariantCulture); - responseHeaders.Append(Constants.Headers.SetCookie, template.ToString()); + responseCookies.Append(key, ChunkCountPrefix + cookieChunkCount.ToString(CultureInfo.InvariantCulture), options); - var chunks = new string[cookieChunkCount]; var offset = 0; for (var chunkId = 1; chunkId <= cookieChunkCount; chunkId++) { - var remainingLength = escapedValue.Length - offset; + var remainingLength = value.Length - offset; var length = Math.Min(dataSizePerCookie, remainingLength); - var segment = escapedValue.Substring(offset, length); + var segment = value.Substring(offset, length); offset += length; - template.Name = escapedKey + "C" + chunkId.ToString(CultureInfo.InvariantCulture); - template.Value = quoted ? Quote(segment) : segment; - chunks[chunkId - 1] = template.ToString(); + responseCookies.Append(key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture), segment, options); } - responseHeaders.Append(Constants.Headers.SetCookie, chunks); } } @@ -239,9 +237,8 @@ public void DeleteCookie(HttpContext context, string key, CookieOptions options) throw new ArgumentNullException(nameof(options)); } - var escapedKey = Encoder.Encode(key); var keys = new List(); - keys.Add(escapedKey + "="); + keys.Add(key + "="); var requestCookie = context.Request.Cookies[key]; var chunks = ParseChunksCount(requestCookie); @@ -249,7 +246,7 @@ public void DeleteCookie(HttpContext context, string key, CookieOptions options) { for (int i = 1; i <= chunks + 1; i++) { - var subkey = escapedKey + "C" + i.ToString(CultureInfo.InvariantCulture); + var subkey = key + ChunkKeySuffix + i.ToString(CultureInfo.InvariantCulture); keys.Add(subkey + "="); } } @@ -273,10 +270,10 @@ public void DeleteCookie(HttpContext context, string key, CookieOptions options) } var responseHeaders = context.Response.Headers; - var existingValues = responseHeaders[Constants.Headers.SetCookie]; + var existingValues = responseHeaders[HeaderNames.SetCookie]; if (!StringValues.IsNullOrEmpty(existingValues)) { - responseHeaders[Constants.Headers.SetCookie] = existingValues.Where(value => !rejectPredicate(value)).ToArray(); + responseHeaders[HeaderNames.SetCookie] = existingValues.Where(value => !rejectPredicate(value)).ToArray(); } AppendResponseCookie( @@ -287,6 +284,8 @@ public void DeleteCookie(HttpContext context, string key, CookieOptions options) { Path = options.Path, Domain = options.Domain, + SameSite = options.SameSite, + IsEssential = options.IsEssential, Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -300,39 +299,11 @@ public void DeleteCookie(HttpContext context, string key, CookieOptions options) { Path = options.Path, Domain = options.Domain, + SameSite = options.SameSite, + IsEssential = options.IsEssential, Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), }); } } - - private static bool IsQuoted(string value) - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - return value.Length >= 2 && value[0] == '"' && value[value.Length - 1] == '"'; - } - - private static string RemoveQuotes(string value) - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - return value.Substring(1, value.Length - 2); - } - - private static string Quote(string value) - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - return '"' + value + '"'; - } } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 000000000..1e0980f66 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAppBuilderExtensions.cs index 765d1f51c..bdfd43c79 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAppBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAppBuilderExtensions.cs @@ -1,9 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { @@ -13,38 +12,26 @@ namespace Microsoft.AspNetCore.Builder public static class CookieAppBuilderExtensions { /// - /// Adds the middleware to the specified , which enables cookie authentication capabilities. + /// UseCookieAuthentication is obsolete. Configure Cookie authentication with AddAuthentication().AddCookie in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. + /// The to add the handler to. /// A reference to this instance after the operation has completed. + [Obsolete("UseCookieAuthentication is obsolete. Configure Cookie authentication with AddAuthentication().AddCookie in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseCookieAuthentication(this IApplicationBuilder app) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware(); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } /// - /// Adds the middleware to the specified , which enables cookie authentication capabilities. + /// UseCookieAuthentication is obsolete. Configure Cookie authentication with AddAuthentication().AddCookie in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. - /// A that specifies options for the middleware. + /// The to add the handler to. + /// A that specifies options for the handler. /// A reference to this instance after the operation has completed. + [Obsolete("UseCookieAuthentication is obsolete. Configure Cookie authentication with AddAuthentication().AddCookie in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseCookieAuthentication(this IApplicationBuilder app, CookieAuthenticationOptions options) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - return app.UseMiddleware(Options.Create(options)); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationDefaults.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationDefaults.cs index ad0e17a09..700b60797 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationDefaults.cs +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationDefaults.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies { /// - /// Default values related to cookie-based authentication middleware + /// Default values related to cookie-based authentication handler /// public static class CookieAuthenticationDefaults { diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs index b056d0787..ff6bb3cbc 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs @@ -1,32 +1,61 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - using System; using System.Linq; using System.Security.Claims; +using System.Text.Encodings.Web; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Features.Authentication; -using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Authentication.Cookies { - internal class CookieAuthenticationHandler : AuthenticationHandler + public class CookieAuthenticationHandler : SignInAuthenticationHandler { private const string HeaderValueNoCache = "no-cache"; - private const string HeaderValueMinusOne = "-1"; + private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT"; private const string SessionIdClaim = "Microsoft.AspNetCore.Authentication.Cookies-SessionId"; private bool _shouldRefresh; + private bool _signInCalled; + private bool _signOutCalled; + private DateTimeOffset? _refreshIssuedUtc; private DateTimeOffset? _refreshExpiresUtc; private string _sessionKey; private Task _readCookieTask; + private AuthenticationTicket _refreshTicket; + + public CookieAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new CookieAuthenticationEvents Events + { + get { return (CookieAuthenticationEvents)base.Events; } + set { base.Events = value; } + } + + protected override Task InitializeHandlerAsync() + { + // Cookies needs to finish the response + Context.Response.OnStarting(FinishResponseAsync); + return Task.CompletedTask; + } + + /// + /// Creates a new instance of the events instance. + /// + /// A new instance of the events instance. + protected override Task CreateEventsAsync() => Task.FromResult(new CookieAuthenticationEvents()); private Task EnsureCookieTicket() { @@ -40,7 +69,7 @@ private Task EnsureCookieTicket() private void CheckForRefresh(AuthenticationTicket ticket) { - var currentUtc = Options.SystemClock.UtcNow; + var currentUtc = Clock.UtcNow; var issuedUtc = ticket.Properties.IssuedUtc; var expiresUtc = ticket.Properties.ExpiresUtc; var allowRefresh = ticket.Properties.AllowRefresh ?? true; @@ -56,21 +85,46 @@ private void CheckForRefresh(AuthenticationTicket ticket) } } - private void RequestRefresh(AuthenticationTicket ticket) + private void RequestRefresh(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal = null) + { + var issuedUtc = ticket.Properties.IssuedUtc; + var expiresUtc = ticket.Properties.ExpiresUtc; + + if (issuedUtc != null && expiresUtc != null) + { + _shouldRefresh = true; + var currentUtc = Clock.UtcNow; + _refreshIssuedUtc = currentUtc; + var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); + _refreshExpiresUtc = currentUtc.Add(timeSpan); + _refreshTicket = CloneTicket(ticket, replacedPrincipal); + } + } + + private AuthenticationTicket CloneTicket(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal) { - _shouldRefresh = true; - var currentUtc = Options.SystemClock.UtcNow; - _refreshIssuedUtc = currentUtc; - var timeSpan = ticket.Properties.ExpiresUtc.Value.Subtract(ticket.Properties.IssuedUtc.Value); - _refreshExpiresUtc = currentUtc.Add(timeSpan); + var principal = replacedPrincipal ?? ticket.Principal; + var newPrincipal = new ClaimsPrincipal(); + foreach (var identity in principal.Identities) + { + newPrincipal.AddIdentity(identity.Clone()); + } + + var newProperties = new AuthenticationProperties(); + foreach (var item in ticket.Properties.Items) + { + newProperties.Items[item.Key] = item.Value; + } + + return new AuthenticationTicket(newPrincipal, newProperties, ticket.AuthenticationScheme); } private async Task ReadCookieTicket() { - var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName); + var cookie = Options.CookieManager.GetRequestCookie(Context, Options.Cookie.Name); if (string.IsNullOrEmpty(cookie)) { - return AuthenticateResult.Skip(); + return AuthenticateResult.NoResult(); } var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding()); @@ -94,8 +148,7 @@ private async Task ReadCookieTicket() } } - var currentUtc = Options.SystemClock.UtcNow; - var issuedUtc = ticket.Properties.IssuedUtc; + var currentUtc = Clock.UtcNow; var expiresUtc = ticket.Properties.ExpiresUtc; if (expiresUtc != null && expiresUtc.Value < currentUtc) @@ -121,8 +174,8 @@ protected override async Task HandleAuthenticateAsync() return result; } - var context = new CookieValidatePrincipalContext(Context, result.Ticket, Options); - await Options.Events.ValidatePrincipal(context); + var context = new CookieValidatePrincipalContext(Context, Scheme, Options, result.Ticket); + await Events.ValidatePrincipal(context); if (context.Principal == null) { @@ -131,50 +184,42 @@ protected override async Task HandleAuthenticateAsync() if (context.ShouldRenew) { - RequestRefresh(result.Ticket); + RequestRefresh(result.Ticket, context.Principal); } - return AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme)); + return AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name)); } private CookieOptions BuildCookieOptions() { - var cookieOptions = new CookieOptions - { - Domain = Options.CookieDomain, - HttpOnly = Options.CookieHttpOnly, - Path = Options.CookiePath ?? (OriginalPathBase.HasValue ? OriginalPathBase.ToString() : "/"), - }; - if (Options.CookieSecure == CookieSecureOption.SameAsRequest) - { - cookieOptions.Secure = Request.IsHttps; - } - else - { - cookieOptions.Secure = Options.CookieSecure == CookieSecureOption.Always; - } + var cookieOptions = Options.Cookie.Build(Context); + // ignore the 'Expires' value as this will be computed elsewhere + cookieOptions.Expires = null; + return cookieOptions; } - protected override async Task FinishResponseAsync() + protected virtual async Task FinishResponseAsync() { // Only renew if requested, and neither sign in or sign out was called - if (!_shouldRefresh || SignInAccepted || SignOutAccepted) + if (!_shouldRefresh || _signInCalled || _signOutCalled) { return; } - // REVIEW: Should this check if there was an error, and then if that error was already handled?? - var ticket = (await HandleAuthenticateOnceAsync())?.Ticket; + var ticket = _refreshTicket; if (ticket != null) { + var properties = ticket.Properties; + if (_refreshIssuedUtc.HasValue) { - ticket.Properties.IssuedUtc = _refreshIssuedUtc; + properties.IssuedUtc = _refreshIssuedUtc; } + if (_refreshExpiresUtc.HasValue) { - ticket.Properties.ExpiresUtc = _refreshExpiresUtc; + properties.ExpiresUtc = _refreshExpiresUtc; } if (Options.SessionStore != null && _sessionKey != null) @@ -183,40 +228,49 @@ protected override async Task FinishResponseAsync() var principal = new ClaimsPrincipal( new ClaimsIdentity( new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, - Options.AuthenticationScheme)); - ticket = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); + Scheme.Name)); + ticket = new AuthenticationTicket(principal, null, Scheme.Name); } var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding()); var cookieOptions = BuildCookieOptions(); - if (ticket.Properties.IsPersistent && _refreshExpiresUtc.HasValue) + if (properties.IsPersistent && _refreshExpiresUtc.HasValue) { - cookieOptions.Expires = _refreshExpiresUtc.Value.ToUniversalTime().DateTime; + cookieOptions.Expires = _refreshExpiresUtc.Value.ToUniversalTime(); } Options.CookieManager.AppendResponseCookie( Context, - Options.CookieName, + Options.Cookie.Name, cookieValue, cookieOptions); - await ApplyHeaders(shouldRedirectToReturnUrl: false, properties: ticket.Properties); + await ApplyHeaders(shouldRedirectToReturnUrl: false, properties: properties); } } - protected override async Task HandleSignInAsync(SignInContext signin) + protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + properties = properties ?? new AuthenticationProperties(); + + _signInCalled = true; + // Process the request cookie to initialize members like _sessionKey. - var result = await EnsureCookieTicket(); + await EnsureCookieTicket(); var cookieOptions = BuildCookieOptions(); var signInContext = new CookieSigningInContext( Context, + Scheme, Options, - Options.AuthenticationScheme, - signin.Principal, - new AuthenticationProperties(signin.Properties), + user, + properties, cookieOptions); DateTimeOffset issuedUtc; @@ -226,7 +280,7 @@ protected override async Task HandleSignInAsync(SignInContext signin) } else { - issuedUtc = Options.SystemClock.UtcNow; + issuedUtc = Clock.UtcNow; signInContext.Properties.IssuedUtc = issuedUtc; } @@ -235,15 +289,16 @@ protected override async Task HandleSignInAsync(SignInContext signin) signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan); } - await Options.Events.SigningIn(signInContext); + await Events.SigningIn(signInContext); if (signInContext.Properties.IsPersistent) { var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan); - signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; + signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime(); } - var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.AuthenticationScheme); + var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name); + if (Options.SessionStore != null) { if (_sessionKey != null) @@ -255,34 +310,41 @@ protected override async Task HandleSignInAsync(SignInContext signin) new ClaimsIdentity( new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, Options.ClaimsIssuer)); - ticket = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); + ticket = new AuthenticationTicket(principal, null, Scheme.Name); } + var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding()); Options.CookieManager.AppendResponseCookie( Context, - Options.CookieName, + Options.Cookie.Name, cookieValue, signInContext.CookieOptions); var signedInContext = new CookieSignedInContext( Context, - Options, - Options.AuthenticationScheme, + Scheme, signInContext.Principal, - signInContext.Properties); + signInContext.Properties, + Options); - await Options.Events.SignedIn(signedInContext); + await Events.SignedIn(signedInContext); // Only redirect on the login path var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath; await ApplyHeaders(shouldRedirect, signedInContext.Properties); + + Logger.SignedIn(Scheme.Name); } - protected override async Task HandleSignOutAsync(SignOutContext signOutContext) + protected async override Task HandleSignOutAsync(AuthenticationProperties properties) { + properties = properties ?? new AuthenticationProperties(); + + _signOutCalled = true; + // Process the request cookie to initialize members like _sessionKey. - var ticket = await EnsureCookieTicket(); + await EnsureCookieTicket(); var cookieOptions = BuildCookieOptions(); if (Options.SessionStore != null && _sessionKey != null) { @@ -291,39 +353,55 @@ protected override async Task HandleSignOutAsync(SignOutContext signOutContext) var context = new CookieSigningOutContext( Context, + Scheme, Options, - new AuthenticationProperties(signOutContext.Properties), + properties, cookieOptions); - await Options.Events.SigningOut(context); + await Events.SigningOut(context); Options.CookieManager.DeleteCookie( Context, - Options.CookieName, + Options.Cookie.Name, context.CookieOptions); // Only redirect on the logout path var shouldRedirect = Options.LogoutPath.HasValue && OriginalPath == Options.LogoutPath; await ApplyHeaders(shouldRedirect, context.Properties); + + Logger.SignedOut(Scheme.Name); } private async Task ApplyHeaders(bool shouldRedirectToReturnUrl, AuthenticationProperties properties) { Response.Headers[HeaderNames.CacheControl] = HeaderValueNoCache; Response.Headers[HeaderNames.Pragma] = HeaderValueNoCache; - Response.Headers[HeaderNames.Expires] = HeaderValueMinusOne; + Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate; + if (shouldRedirectToReturnUrl && Response.StatusCode == 200) { - var query = Request.Query; - var redirectUri = query[Options.ReturnUrlParameter]; - if (!StringValues.IsNullOrEmpty(redirectUri) - && IsHostRelative(redirectUri)) + // set redirect uri in order: + // 1. properties.RedirectUri + // 2. query parameter ReturnUrlParameter + // + // Absolute uri is not allowed if it is from query string as query string is not + // a trusted source. + var redirectUri = properties.RedirectUri; + if (string.IsNullOrEmpty(redirectUri)) { - var redirectContext = new CookieRedirectContext(Context, Options, redirectUri, properties); - await Options.Events.RedirectToReturnUrl(redirectContext); + redirectUri = Request.Query[Options.ReturnUrlParameter]; + if (string.IsNullOrEmpty(redirectUri) || !IsHostRelative(redirectUri)) + { + redirectUri = null; + } } - } + if (redirectUri != null) + { + await Events.RedirectToReturnUrl( + new RedirectContext(Context, Scheme, Options, properties, redirectUri)); + } + } } private static bool IsHostRelative(string path) @@ -339,39 +417,29 @@ private static bool IsHostRelative(string path) return path[0] == '/' && path[1] != '/' && path[1] != '\\'; } - protected override async Task HandleForbiddenAsync(ChallengeContext context) + protected override async Task HandleForbiddenAsync(AuthenticationProperties properties) { - var properties = new AuthenticationProperties(context.Properties); var returnUrl = properties.RedirectUri; if (string.IsNullOrEmpty(returnUrl)) { - returnUrl = OriginalPathBase + Request.Path + Request.QueryString; + returnUrl = OriginalPathBase + OriginalPath + Request.QueryString; } var accessDeniedUri = Options.AccessDeniedPath + QueryString.Create(Options.ReturnUrlParameter, returnUrl); - var redirectContext = new CookieRedirectContext(Context, Options, BuildRedirectUri(accessDeniedUri), properties); - await Options.Events.RedirectToAccessDenied(redirectContext); - return true; + var redirectContext = new RedirectContext(Context, Scheme, Options, properties, BuildRedirectUri(accessDeniedUri)); + await Events.RedirectToAccessDenied(redirectContext); } - protected override async Task HandleUnauthorizedAsync(ChallengeContext context) + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - var properties = new AuthenticationProperties(context.Properties); var redirectUri = properties.RedirectUri; if (string.IsNullOrEmpty(redirectUri)) { - redirectUri = OriginalPathBase + Request.Path + Request.QueryString; + redirectUri = OriginalPathBase + OriginalPath + Request.QueryString; } var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri); - var redirectContext = new CookieRedirectContext(Context, Options, BuildRedirectUri(loginUri), properties); - await Options.Events.RedirectToLogin(redirectContext); - return true; - + var redirectContext = new RedirectContext(Context, Scheme, Options, properties, BuildRedirectUri(loginUri)); + await Events.RedirectToLogin(redirectContext); } private string GetTlsTokenBinding() diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationMiddleware.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationMiddleware.cs deleted file mode 100644 index ff54957cf..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationMiddleware.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Authentication.Cookies -{ - public class CookieAuthenticationMiddleware : AuthenticationMiddleware - { - public CookieAuthenticationMiddleware( - RequestDelegate next, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - UrlEncoder urlEncoder, - IOptions options) - : base(next, options, loggerFactory, urlEncoder) - { - if (dataProtectionProvider == null) - { - throw new ArgumentNullException(nameof(dataProtectionProvider)); - } - - if (Options.Events == null) - { - Options.Events = new CookieAuthenticationEvents(); - } - if (String.IsNullOrEmpty(Options.CookieName)) - { - Options.CookieName = CookieAuthenticationDefaults.CookiePrefix + Options.AuthenticationScheme; - } - if (Options.TicketDataFormat == null) - { - var provider = Options.DataProtectionProvider ?? dataProtectionProvider; - var dataProtector = provider.CreateProtector(typeof(CookieAuthenticationMiddleware).FullName, Options.AuthenticationScheme, "v2"); - Options.TicketDataFormat = new TicketDataFormat(dataProtector); - } - if (Options.CookieManager == null) - { - Options.CookieManager = new ChunkingCookieManager(urlEncoder); - } - if (!Options.LoginPath.HasValue) - { - Options.LoginPath = CookieAuthenticationDefaults.LoginPath; - } - if (!Options.LogoutPath.HasValue) - { - Options.LogoutPath = CookieAuthenticationDefaults.LogoutPath; - } - if (!Options.AccessDeniedPath.HasValue) - { - Options.AccessDeniedPath = CookieAuthenticationDefaults.AccessDeniedPath; - } - } - - protected override AuthenticationHandler CreateHandler() - { - return new CookieAuthenticationHandler(); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs index 8a451c9c7..78156caec 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs @@ -2,148 +2,126 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.ComponentModel; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.Internal; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Authentication.Cookies { /// - /// Configuration options for . + /// Configuration options for . /// - public class CookieAuthenticationOptions : AuthenticationOptions, IOptions + public class CookieAuthenticationOptions : AuthenticationSchemeOptions { - private string _cookieName; + private CookieBuilder _cookieBuilder = new RequestPathBaseCookieBuilder + { + // the default name is configured in PostConfigureCookieAuthenticationOptions + + // To support OAuth authentication, a lax mode is required, see https://github.com/aspnet/Security/issues/1231. + SameSite = SameSiteMode.Lax, + HttpOnly = true, + SecurePolicy = CookieSecurePolicy.SameAsRequest, + IsEssential = true, + }; /// /// Create an instance of the options initialized with the default values /// public CookieAuthenticationOptions() { - AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme; - AutomaticAuthenticate = true; - ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter; ExpireTimeSpan = TimeSpan.FromDays(14); + ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter; SlidingExpiration = true; - CookieHttpOnly = true; - CookieSecure = CookieSecureOption.SameAsRequest; - SystemClock = new SystemClock(); Events = new CookieAuthenticationEvents(); } /// - /// Determines the cookie name used to persist the identity. The default value is ".AspNetCore.Cookies". - /// This value should be changed if you change the name of the AuthenticationScheme, especially if your - /// system uses the cookie authentication middleware multiple times. + /// + /// Determines the settings used to create the cookie. + /// + /// + /// defaults to . + /// defaults to true. + /// defaults to . + /// /// - public string CookieName + /// + /// + /// The default value for cookie name is ".AspNetCore.Cookies". + /// This value should be changed if you change the name of the AuthenticationScheme, especially if your + /// system uses the cookie authentication handler multiple times. + /// + /// + /// determines if the browser should allow the cookie to be attached to same-site or cross-site requests. + /// The default is Lax, which means the cookie is only allowed to be attached to cross-site requests using safe HTTP methods and same-site requests. + /// + /// + /// determines if the browser should allow the cookie to be accessed by client-side javascript. + /// The default is true, which means the cookie will only be passed to http requests and is not made available to script on the page. + /// + /// + /// is currently ignored. Use to control lifetime of cookie authentication. + /// + /// + public CookieBuilder Cookie { - get { return _cookieName; } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - _cookieName = value; - } + get => _cookieBuilder; + set => _cookieBuilder = value ?? throw new ArgumentNullException(nameof(value)); } /// - /// Determines the domain used to create the cookie. Is not provided by default. - /// - public string CookieDomain { get; set; } - - /// - /// Determines the path used to create the cookie. The default value is "/" for highest browser compatability. - /// - public string CookiePath { get; set; } - - /// - /// Determines if the browser should allow the cookie to be accessed by client-side javascript. The - /// default is true, which means the cookie will only be passed to http requests and is not made available - /// to script on the page. - /// - public bool CookieHttpOnly { get; set; } - - /// - /// Determines if the cookie should only be transmitted on HTTPS request. The default is to limit the cookie - /// to HTTPS requests if the page which is doing the SignIn is also HTTPS. If you have an HTTPS sign in page - /// and portions of your site are HTTP you may need to change this value. - /// - public CookieSecureOption CookieSecure { get; set; } - - /// - /// If set this will be used by the CookieAuthenticationMiddleware for data protection. + /// If set this will be used by the CookieAuthenticationHandler for data protection. /// public IDataProtectionProvider DataProtectionProvider { get; set; } /// - /// Controls how much time the cookie will remain valid from the point it is created. The expiration - /// information is in the protected cookie ticket. Because of that an expired cookie will be ignored - /// even if it is passed to the server after the browser should have purged it - /// - public TimeSpan ExpireTimeSpan { get; set; } - - /// - /// The SlidingExpiration is set to true to instruct the middleware to re-issue a new cookie with a new + /// The SlidingExpiration is set to true to instruct the handler to re-issue a new cookie with a new /// expiration time any time it processes a request which is more than halfway through the expiration window. /// public bool SlidingExpiration { get; set; } /// - /// The LoginPath property informs the middleware that it should change an outgoing 401 Unauthorized status - /// code into a 302 redirection onto the given login path. The current url which generated the 401 is added - /// to the LoginPath as a query string parameter named by the ReturnUrlParameter. Once a request to the - /// LoginPath grants a new SignIn identity, the ReturnUrlParameter value is used to redirect the browser back - /// to the url which caused the original unauthorized status code. + /// The LoginPath property is used by the handler for the redirection target when handling ChallengeAsync. + /// The current url which is added to the LoginPath as a query string parameter named by the ReturnUrlParameter. + /// Once a request to the LoginPath grants a new SignIn identity, the ReturnUrlParameter value is used to redirect + /// the browser back to the original url. /// public PathString LoginPath { get; set; } /// - /// If the LogoutPath is provided the middleware then a request to that path will redirect based on the ReturnUrlParameter. + /// If the LogoutPath is provided the handler then a request to that path will redirect based on the ReturnUrlParameter. /// public PathString LogoutPath { get; set; } /// - /// The AccessDeniedPath property informs the middleware that it should change an outgoing 403 Forbidden status - /// code into a 302 redirection onto the given path. + /// The AccessDeniedPath property is used by the handler for the redirection target when handling ForbidAsync. /// public PathString AccessDeniedPath { get; set; } /// - /// The ReturnUrlParameter determines the name of the query string parameter which is appended by the middleware - /// when a 401 Unauthorized status code is changed to a 302 redirect onto the login path. This is also the query - /// string parameter looked for when a request arrives on the login path or logout path, in order to return to the - /// original url after the action is performed. + /// The ReturnUrlParameter determines the name of the query string parameter which is appended by the handler + /// when during a Challenge. This is also the query string parameter looked for when a request arrives on the + /// login path or logout path, in order to return to the original url after the action is performed. /// public string ReturnUrlParameter { get; set; } /// - /// The Provider may be assigned to an instance of an object created by the application at startup time. The middleware - /// calls methods on the provider which give the application control at certain points where processing is occurring. + /// The Provider may be assigned to an instance of an object created by the application at startup time. The handler + /// calls methods on the provider which give the application control at certain points where processing is occurring. /// If it is not provided a default instance is supplied which does nothing when the methods are called. /// - public ICookieAuthenticationEvents Events { get; set; } + public new CookieAuthenticationEvents Events + { + get => (CookieAuthenticationEvents)base.Events; + set => base.Events = value; + } /// /// The TicketDataFormat is used to protect and unprotect the identity and other properties which are stored in the - /// cookie value. If it is not provided a default data handler is created using the data protection service contained - /// in the IApplicationBuilder.Properties. The default data protection service is based on machine key when running on ASP.NET, - /// and on DPAPI when running in a different process. + /// cookie value. If not provided one will be created using . /// public ISecureDataFormat TicketDataFormat { get; set; } - /// - /// For testing purposes only. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public ISystemClock SystemClock { get; set; } - /// /// The component used to get cookies from the request or set them on the response. /// @@ -157,12 +135,17 @@ public string CookieName /// public ITicketStore SessionStore { get; set; } - CookieAuthenticationOptions IOptions.Value - { - get - { - return this; - } - } + /// + /// + /// Controls how much time the authentication ticket stored in the cookie will remain valid from the point it is created + /// The expiration information is stored in the protected cookie ticket. Because of that an expired cookie will be ignored + /// even if it is passed to the server after the browser should have purged it. + /// + /// + /// This is separate from the value of , which specifies + /// how long the browser will keep the cookie. + /// + /// + public TimeSpan ExpireTimeSpan { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieExtensions.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieExtensions.cs new file mode 100644 index 000000000..4c41f54a9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class CookieExtensions + { + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder) + => builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme); + + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme) + => builder.AddCookie(authenticationScheme, configureOptions: null); + + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddCookie(authenticationScheme, displayName: null, configureOptions: configureOptions); + + public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, PostConfigureCookieAuthenticationOptions>()); + return builder.AddScheme(authenticationScheme, displayName, configureOptions); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieSecureOption.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieSecureOption.cs deleted file mode 100644 index 5a35415d1..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieSecureOption.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - - -namespace Microsoft.AspNetCore.Authentication.Cookies -{ - /// - /// Determines how the identity cookie's security property is set. - /// - public enum CookieSecureOption - { - /// - /// If the URI that provides the cookie is HTTPS, then the cookie will only be returned to the server on - /// subsequent HTTPS requests. Otherwise if the URI that provides the cookie is HTTP, then the cookie will - /// be returned to the server on all HTTP and HTTPS requests. This is the default value because it ensures - /// HTTPS for all authenticated requests on deployed servers, and also supports HTTP for localhost development - /// and for servers that do not have HTTPS support. - /// - SameAsRequest, - - /// - /// CookieOptions.Secure is never marked true. Use this value when your login page is HTTPS, but other pages - /// on the site which are HTTP also require authentication information. This setting is not recommended because - /// the authentication information provided with an HTTP request may be observed and used by other computers - /// on your local network or wireless connection. - /// - Never, - - /// - /// CookieOptions.Secure is always marked true. Use this value when your login page and all subsequent pages - /// requiring the authenticated identity are HTTPS. Local development will also need to be done with HTTPS urls. - /// - Always, - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/BaseCookieContext.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/Events/BaseCookieContext.cs deleted file mode 100644 index e5423fed2..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/BaseCookieContext.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Authentication.Cookies -{ - public class BaseCookieContext : BaseContext - { - public BaseCookieContext( - HttpContext context, - CookieAuthenticationOptions options) - : base(context) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - Options = options; - } - - public CookieAuthenticationOptions Options { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieAuthenticationEvents.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieAuthenticationEvents.cs index ffe687e5f..2b8b0416b 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieAuthenticationEvents.cs +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieAuthenticationEvents.cs @@ -12,32 +12,32 @@ namespace Microsoft.AspNetCore.Authentication.Cookies /// application only needs to override a few of the interface methods. This may be used as a base class /// or may be instantiated directly. /// - public class CookieAuthenticationEvents : ICookieAuthenticationEvents + public class CookieAuthenticationEvents { /// /// A delegate assigned to this property will be invoked when the related method is called. /// - public Func OnValidatePrincipal { get; set; } = context => Task.FromResult(0); + public Func OnValidatePrincipal { get; set; } = context => Task.CompletedTask; /// /// A delegate assigned to this property will be invoked when the related method is called. /// - public Func OnSigningIn { get; set; } = context => Task.FromResult(0); + public Func OnSigningIn { get; set; } = context => Task.CompletedTask; /// /// A delegate assigned to this property will be invoked when the related method is called. /// - public Func OnSignedIn { get; set; } = context => Task.FromResult(0); + public Func OnSignedIn { get; set; } = context => Task.CompletedTask; /// /// A delegate assigned to this property will be invoked when the related method is called. /// - public Func OnSigningOut { get; set; } = context => Task.FromResult(0); + public Func OnSigningOut { get; set; } = context => Task.CompletedTask; /// /// A delegate assigned to this property will be invoked when the related method is called. /// - public Func OnRedirectToLogin { get; set; } = context => + public Func, Task> OnRedirectToLogin { get; set; } = context => { if (IsAjaxRequest(context.Request)) { @@ -48,13 +48,13 @@ public class CookieAuthenticationEvents : ICookieAuthenticationEvents { context.Response.Redirect(context.RedirectUri); } - return Task.FromResult(0); + return Task.CompletedTask; }; /// /// A delegate assigned to this property will be invoked when the related method is called. /// - public Func OnRedirectToAccessDenied { get; set; } = context => + public Func, Task> OnRedirectToAccessDenied { get; set; } = context => { if (IsAjaxRequest(context.Request)) { @@ -65,13 +65,13 @@ public class CookieAuthenticationEvents : ICookieAuthenticationEvents { context.Response.Redirect(context.RedirectUri); } - return Task.FromResult(0); + return Task.CompletedTask; }; /// /// A delegate assigned to this property will be invoked when the related method is called. /// - public Func OnRedirectToLogout { get; set; } = context => + public Func, Task> OnRedirectToLogout { get; set; } = context => { if (IsAjaxRequest(context.Request)) { @@ -81,13 +81,13 @@ public class CookieAuthenticationEvents : ICookieAuthenticationEvents { context.Response.Redirect(context.RedirectUri); } - return Task.FromResult(0); + return Task.CompletedTask; }; /// /// A delegate assigned to this property will be invoked when the related method is called. /// - public Func OnRedirectToReturnUrl { get; set; } = context => + public Func, Task> OnRedirectToReturnUrl { get; set; } = context => { if (IsAjaxRequest(context.Request)) { @@ -97,7 +97,7 @@ public class CookieAuthenticationEvents : ICookieAuthenticationEvents { context.Response.Redirect(context.RedirectUri); } - return Task.FromResult(0); + return Task.CompletedTask; }; private static bool IsAjaxRequest(HttpRequest request) @@ -135,24 +135,24 @@ private static bool IsAjaxRequest(HttpRequest request) /// Implements the interface method by invoking the related delegate method. /// /// Contains information about the event - public virtual Task RedirectToLogout(CookieRedirectContext context) => OnRedirectToLogout(context); + public virtual Task RedirectToLogout(RedirectContext context) => OnRedirectToLogout(context); /// /// Implements the interface method by invoking the related delegate method. /// /// Contains information about the event - public virtual Task RedirectToLogin(CookieRedirectContext context) => OnRedirectToLogin(context); + public virtual Task RedirectToLogin(RedirectContext context) => OnRedirectToLogin(context); /// /// Implements the interface method by invoking the related delegate method. /// /// Contains information about the event - public virtual Task RedirectToReturnUrl(CookieRedirectContext context) => OnRedirectToReturnUrl(context); + public virtual Task RedirectToReturnUrl(RedirectContext context) => OnRedirectToReturnUrl(context); /// /// Implements the interface method by invoking the related delegate method. /// /// Contains information about the event - public virtual Task RedirectToAccessDenied(CookieRedirectContext context) => OnRedirectToAccessDenied(context); + public virtual Task RedirectToAccessDenied(RedirectContext context) => OnRedirectToAccessDenied(context); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSignedInContext.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSignedInContext.cs index cfb7c5f1d..98c31dd19 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSignedInContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSignedInContext.cs @@ -2,51 +2,32 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Security.Claims; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; namespace Microsoft.AspNetCore.Authentication.Cookies { /// /// Context object passed to the ICookieAuthenticationEvents method SignedIn. /// - public class CookieSignedInContext : BaseCookieContext + public class CookieSignedInContext : PrincipalContext { /// /// Creates a new instance of the context object. /// /// The HTTP request context - /// The middleware options - /// Initializes AuthenticationScheme property + /// The scheme data /// Initializes Principal property /// Initializes Properties property + /// The handler options public CookieSignedInContext( HttpContext context, - CookieAuthenticationOptions options, - string authenticationScheme, + AuthenticationScheme scheme, ClaimsPrincipal principal, - AuthenticationProperties properties) - : base(context, options) + AuthenticationProperties properties, + CookieAuthenticationOptions options) + : base(context, scheme, options, properties) { - AuthenticationScheme = authenticationScheme; Principal = principal; - Properties = properties; } - - /// - /// The name of the AuthenticationScheme creating a cookie - /// - public string AuthenticationScheme { get; } - - /// - /// Contains the claims that were converted into the outgoing cookie. - /// - public ClaimsPrincipal Principal { get; } - - /// - /// Contains the extra data that was contained in the outgoing cookie. - /// - public AuthenticationProperties Properties { get; } } } diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningInContext.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningInContext.cs index d8b2307f3..41d7b4f6a 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningInContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningInContext.cs @@ -2,58 +2,37 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Security.Claims; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; namespace Microsoft.AspNetCore.Authentication.Cookies { /// - /// Context object passed to the ICookieAuthenticationEvents method SigningIn. + /// Context object passed to the . /// - public class CookieSigningInContext : BaseCookieContext + public class CookieSigningInContext : PrincipalContext { /// /// Creates a new instance of the context object. /// /// The HTTP request context - /// The middleware options - /// Initializes AuthenticationScheme property + /// The scheme data + /// The handler options /// Initializes Principal property - /// Initializes Extra property + /// The authentication properties. /// Initializes options for the authentication cookie. public CookieSigningInContext( HttpContext context, + AuthenticationScheme scheme, CookieAuthenticationOptions options, - string authenticationScheme, ClaimsPrincipal principal, AuthenticationProperties properties, CookieOptions cookieOptions) - : base(context, options) + : base(context, scheme, options, properties) { - AuthenticationScheme = authenticationScheme; - Principal = principal; - Properties = properties; CookieOptions = cookieOptions; + Principal = principal; } - /// - /// The name of the AuthenticationScheme creating a cookie - /// - public string AuthenticationScheme { get; } - - /// - /// Contains the claims about to be converted into the outgoing cookie. - /// May be replaced or altered during the SigningIn call. - /// - public ClaimsPrincipal Principal { get; set; } - - /// - /// Contains the extra data about to be contained in the outgoing cookie. - /// May be replaced or altered during the SigningIn call. - /// - public AuthenticationProperties Properties { get; set; } - /// /// The options for creating the outgoing cookie. /// May be replace or altered during the SigningIn call. diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningOutContext.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningOutContext.cs index 51c04a56b..34f6e49ab 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningOutContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieSigningOutContext.cs @@ -1,41 +1,36 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; namespace Microsoft.AspNetCore.Authentication.Cookies { /// - /// Context object passed to the ICookieAuthenticationEvents method SigningOut + /// Context object passed to the /// - public class CookieSigningOutContext : BaseCookieContext + public class CookieSigningOutContext : PropertiesContext { /// /// /// /// + /// /// /// /// public CookieSigningOutContext( - HttpContext context, + HttpContext context, + AuthenticationScheme scheme, CookieAuthenticationOptions options, AuthenticationProperties properties, CookieOptions cookieOptions) - : base(context, options) - { - CookieOptions = cookieOptions; - Properties = properties; - } + : base(context, scheme, options, properties) + => CookieOptions = cookieOptions; /// /// The options for creating the outgoing cookie. /// May be replace or altered during the SigningOut call. /// public CookieOptions CookieOptions { get; set; } - - public AuthenticationProperties Properties { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs index 57a28191c..d2161e42a 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieValidatePrincipalContext.cs @@ -3,56 +3,33 @@ using System; using System.Security.Claims; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; namespace Microsoft.AspNetCore.Authentication.Cookies { /// - /// Context object passed to the ICookieAuthenticationProvider method ValidatePrincipal. + /// Context object passed to the CookieAuthenticationEvents ValidatePrincipal method. /// - public class CookieValidatePrincipalContext : BaseCookieContext + public class CookieValidatePrincipalContext : PrincipalContext { /// /// Creates a new instance of the context object. /// /// + /// /// Contains the initial values for identity and extra data /// - public CookieValidatePrincipalContext(HttpContext context, AuthenticationTicket ticket, CookieAuthenticationOptions options) - : base(context, options) + public CookieValidatePrincipalContext(HttpContext context, AuthenticationScheme scheme, CookieAuthenticationOptions options, AuthenticationTicket ticket) + : base(context, scheme, options, ticket?.Properties) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - if (ticket == null) { throw new ArgumentNullException(nameof(ticket)); } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - Principal = ticket.Principal; - Properties = ticket.Properties; } - /// - /// Contains the claims principal arriving with the request. May be altered to change the - /// details of the authenticated user. - /// - public ClaimsPrincipal Principal { get; private set; } - - /// - /// Contains the extra meta-data arriving with the request ticket. May be altered. - /// - public AuthenticationProperties Properties { get; private set; } - /// /// If true, the cookie will be renewed /// @@ -63,18 +40,12 @@ public CookieValidatePrincipalContext(HttpContext context, AuthenticationTicket /// Principal property, which determines the identity of the authenticated request. /// /// The used as the replacement - public void ReplacePrincipal(ClaimsPrincipal principal) - { - Principal = principal; - } + public void ReplacePrincipal(ClaimsPrincipal principal) => Principal = principal; /// /// Called to reject the incoming principal. This may be done if the application has determined the /// account is no longer active, and the request should be treated as if it was anonymous. /// - public void RejectPrincipal() - { - Principal = null; - } + public void RejectPrincipal() => Principal = null; } } diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/ICookieAuthenticationEvents.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/Events/ICookieAuthenticationEvents.cs deleted file mode 100644 index 1406d872d..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/ICookieAuthenticationEvents.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Authentication.Cookies -{ - /// - /// Specifies callback methods which the invokes to enable developer control over the authentication process. /> - /// - public interface ICookieAuthenticationEvents - { - /// - /// Called each time a request principal has been validated by the middleware. By implementing this method the - /// application may alter or reject the principal which has arrived with the request. - /// - /// Contains information about the login session as well as the user . - /// A representing the completed operation. - Task ValidatePrincipal(CookieValidatePrincipalContext context); - - /// - /// Called when an endpoint has provided sign in information before it is converted into a cookie. By - /// implementing this method the claims and extra information that go into the ticket may be altered. - /// - /// Contains information about the login session as well as the user . - Task SigningIn(CookieSigningInContext context); - - /// - /// Called when an endpoint has provided sign in information after it is converted into a cookie. - /// - /// Contains information about the login session as well as the user . - Task SignedIn(CookieSignedInContext context); - - /// - /// Called when a SignOut causes a redirect in the cookie middleware. - /// - /// Contains information about the event - Task RedirectToLogout(CookieRedirectContext context); - - /// - /// Called when a SignIn causes a redirect in the cookie middleware. - /// - /// Contains information about the event - Task RedirectToLogin(CookieRedirectContext context); - - /// - /// Called when redirecting back to the return url in the cookie middleware. - /// - /// Contains information about the event - Task RedirectToReturnUrl(CookieRedirectContext context); - - /// - /// Called when an access denied causes a redirect in the cookie middleware. - /// - /// Contains information about the event - Task RedirectToAccessDenied(CookieRedirectContext context); - - /// - /// Called during the sign-out flow to augment the cookie cleanup process. - /// - /// Contains information about the login session as well as information about the authentication cookie. - Task SigningOut(CookieSigningOutContext context); - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/LoggingExtensions.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/LoggingExtensions.cs new file mode 100644 index 000000000..d12735443 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/LoggingExtensions.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging +{ + internal static class LoggingExtensions + { + private static Action _authSchemeSignedIn; + private static Action _authSchemeSignedOut; + + static LoggingExtensions() + { + _authSchemeSignedIn = LoggerMessage.Define( + eventId: 10, + logLevel: LogLevel.Information, + formatString: "AuthenticationScheme: {AuthenticationScheme} signed in."); + _authSchemeSignedOut = LoggerMessage.Define( + eventId: 11, + logLevel: LogLevel.Information, + formatString: "AuthenticationScheme: {AuthenticationScheme} signed out."); + } + + public static void SignedIn(this ILogger logger, string authenticationScheme) + { + _authSchemeSignedIn(logger, authenticationScheme, null); + } + + public static void SignedOut(this ILogger logger, string authenticationScheme) + { + _authSchemeSignedOut(logger, authenticationScheme, null); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj b/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj new file mode 100644 index 000000000..093f60a3d --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj @@ -0,0 +1,20 @@ + + + + ASP.NET Core middleware that enables an application to use cookie based authentication. + netcoreapp3.0 + $(DefineConstants);SECURITY + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.xproj b/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.xproj deleted file mode 100644 index 8c4f3cd41..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.xproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - fc152cc4-054b-457e-8d91-389c5de3c561 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/PostConfigureCookieAuthenticationOptions.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/PostConfigureCookieAuthenticationOptions.cs new file mode 100644 index 000000000..48895072e --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/PostConfigureCookieAuthenticationOptions.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication.Cookies +{ + /// + /// Used to setup defaults for all . + /// + public class PostConfigureCookieAuthenticationOptions : IPostConfigureOptions + { + private readonly IDataProtectionProvider _dp; + + public PostConfigureCookieAuthenticationOptions(IDataProtectionProvider dataProtection) + { + _dp = dataProtection; + } + + /// + /// Invoked to post configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configure. + public void PostConfigure(string name, CookieAuthenticationOptions options) + { + options.DataProtectionProvider = options.DataProtectionProvider ?? _dp; + + if (string.IsNullOrEmpty(options.Cookie.Name)) + { + options.Cookie.Name = CookieAuthenticationDefaults.CookiePrefix + name; + } + if (options.TicketDataFormat == null) + { + // Note: the purpose for the data protector must remain fixed for interop to work. + var dataProtector = options.DataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", name, "v2"); + options.TicketDataFormat = new TicketDataFormat(dataProtector); + } + if (options.CookieManager == null) + { + options.CookieManager = new ChunkingCookieManager(); + } + if (!options.LoginPath.HasValue) + { + options.LoginPath = CookieAuthenticationDefaults.LoginPath; + } + if (!options.LogoutPath.HasValue) + { + options.LogoutPath = CookieAuthenticationDefaults.LogoutPath; + } + if (!options.AccessDeniedPath.HasValue) + { + options.AccessDeniedPath = CookieAuthenticationDefaults.AccessDeniedPath; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/Properties/AssemblyInfo.cs deleted file mode 100644 index 76feceeff..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Reflection; -using System.Resources; - -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: AssemblyCompany("Microsoft Corporation.")] -[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/Properties/Resources.Designer.cs deleted file mode 100644 index e2719f39d..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/Properties/Resources.Designer.cs +++ /dev/null @@ -1,62 +0,0 @@ -// -namespace Microsoft.AspNetCore.Authentication.Cookies -{ - using System.Globalization; - using System.Reflection; - using System.Resources; - - internal static class Resources - { - private static readonly ResourceManager _resourceManager - = new ResourceManager("Microsoft.AspNetCore.Authentication.Cookies.Resources", typeof(Resources).GetTypeInfo().Assembly); - - /// - /// The cookie key and options are larger than ChunksSize, leaving no room for data. - /// - internal static string Exception_CookieLimitTooSmall - { - get { return GetString("Exception_CookieLimitTooSmall"); } - } - - /// - /// The cookie key and options are larger than ChunksSize, leaving no room for data. - /// - internal static string FormatException_CookieLimitTooSmall() - { - return GetString("Exception_CookieLimitTooSmall"); - } - - /// - /// The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded. - /// - internal static string Exception_ImcompleteChunkedCookie - { - get { return GetString("Exception_ImcompleteChunkedCookie"); } - } - - /// - /// The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded. - /// - internal static string FormatException_ImcompleteChunkedCookie(object p0, object p1, object p2) - { - return string.Format(CultureInfo.CurrentCulture, GetString("Exception_ImcompleteChunkedCookie"), p0, p1, p2); - } - - private static string GetString(string name, params string[] formatterNames) - { - var value = _resourceManager.GetString(name); - - System.Diagnostics.Debug.Assert(value != null); - - if (formatterNames != null) - { - for (var i = 0; i < formatterNames.Length; i++) - { - value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); - } - } - - return value; - } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json new file mode 100644 index 000000000..b218669b7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json @@ -0,0 +1,1621 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Cookies, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.CookieExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddCookie", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddCookie", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddCookie", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddCookie", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddCookie", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.CookieAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseCookieAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseCookieAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "CookiePrefix", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "LoginPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "LogoutPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AccessDeniedPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "ReturnUrlParameter", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Cookies\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.SignInAuthenticationHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeHandlerAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FinishResponseAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignInAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleForbiddenAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Cookie", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Cookie", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieBuilder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DataProtectionProvider", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DataProtectionProvider", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SlidingExpiration", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SlidingExpiration", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LoginPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LoginPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LogoutPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LogoutPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AccessDeniedPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AccessDeniedPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ReturnUrlParameter", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ReturnUrlParameter", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TicketDataFormat", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TicketDataFormat", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieManager", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieManager", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SessionStore", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.Cookies.ITicketStore", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SessionStore", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.ITicketStore" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ExpireTimeSpan", + "Parameters": [], + "ReturnType": "System.TimeSpan", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ExpireTimeSpan", + "Parameters": [ + { + "Name": "value", + "Type": "System.TimeSpan" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieDomain", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieDomain", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookiePath", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookiePath", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieHttpOnly", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieHttpOnly", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieSecure", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieSecurePolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieSecure", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieSecurePolicy" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnValidatePrincipal", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnValidatePrincipal", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnSigningIn", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnSigningIn", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnSignedIn", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnSignedIn", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnSigningOut", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnSigningOut", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToLogin", + "Parameters": [], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToLogin", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToAccessDenied", + "Parameters": [], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToAccessDenied", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToLogout", + "Parameters": [], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToLogout", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToReturnUrl", + "Parameters": [], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToReturnUrl", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ValidatePrincipal", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieValidatePrincipalContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SigningIn", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieSigningInContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignedIn", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieSignedInContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SigningOut", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieSigningOutContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToLogout", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToLogin", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToReturnUrl", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToAccessDenied", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieSignedInContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PrincipalContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieSigningInContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PrincipalContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_CookieOptions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieOptions", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "cookieOptions", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieSigningOutContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_CookieOptions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieOptions", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "cookieOptions", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieValidatePrincipalContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PrincipalContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ShouldRenew", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ShouldRenew", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReplacePrincipal", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RejectPrincipal", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions" + }, + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetRequestCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendResponseCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DeleteCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.ITicketStore", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "StoreAsync", + "Parameters": [ + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RenewAsync", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RetrieveAsync", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RemoveAsync", + "Parameters": [ + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.PostConfigureCookieAuthenticationOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Options.IPostConfigureOptions" + ], + "Members": [ + { + "Kind": "Method", + "Name": "PostConfigure", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "dataProtection", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ChunkSize", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ChunkSize", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ThrowForPartialCookies", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ThrowForPartialCookies", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetRequestCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendResponseCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DeleteCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.Cookies.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DefaultChunkSize", + "Parameters": [], + "ReturnType": "System.Int32", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "4050" + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/breakingchanges.netcore.json b/src/Microsoft.AspNetCore.Authentication.Cookies/breakingchanges.netcore.json new file mode 100644 index 000000000..7a1ab7962 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/breakingchanges.netcore.json @@ -0,0 +1,52 @@ + [ + { + "TypeId": "public class Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "MemberId": "public Microsoft.AspNetCore.Http.CookieSecurePolicy get_CookieSecure()", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "MemberId": "public System.Boolean get_CookieHttpOnly()", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "MemberId": "public System.String get_CookieDomain()", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "MemberId": "public System.String get_CookieName()", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "MemberId": "public System.String get_CookiePath()", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "MemberId": "public System.Void set_CookieDomain(System.String value)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "MemberId": "public System.Void set_CookieHttpOnly(System.Boolean value)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "MemberId": "public System.Void set_CookieName(System.String value)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "MemberId": "public System.Void set_CookiePath(System.String value)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "MemberId": "public System.Void set_CookieSecure(Microsoft.AspNetCore.Http.CookieSecurePolicy value)", + "Kind": "Removal" + } + ] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/project.json b/src/Microsoft.AspNetCore.Authentication.Cookies/project.json deleted file mode 100644 index 82438e402..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/project.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "ASP.NET Core middleware that enables an application to use cookie based authentication.", - "tags": [ - "aspnetcore", - "authentication", - "security" - ], - "repository": { - "type": "git", - "url": "git://github.com/aspnet/security" - }, - "compilationOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk", - "nowarn": [ - "CS1591" - ], - "xmlDoc": true - }, - "dependencies": { - "Microsoft.AspNetCore.Authentication": "1.0.0-*", - "Microsoft.Extensions.Options": "1.0.0-*", - "Microsoft.Extensions.WebEncoders": "1.0.0-*" - }, - "frameworks": { - "net451": {}, - "netstandard1.3": {} - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookAppBuilderExtensions.cs index 0435db794..a94dc7bc4 100644 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookAppBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookAppBuilderExtensions.cs @@ -1,9 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using Microsoft.AspNetCore.Authentication.Facebook; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { @@ -13,38 +12,26 @@ namespace Microsoft.AspNetCore.Builder public static class FacebookAppBuilderExtensions { /// - /// Adds the middleware to the specified , which enables Facebook authentication capabilities. + /// UseFacebookAuthentication is obsolete. Configure Facebook authentication with AddAuthentication().AddFacebook in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. + /// The to add the handler to. /// A reference to this instance after the operation has completed. + [Obsolete("UseFacebookAuthentication is obsolete. Configure Facebook authentication with AddAuthentication().AddFacebook in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseFacebookAuthentication(this IApplicationBuilder app) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware(); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } /// - /// Adds the middleware to the specified , which enables Facebook authentication capabilities. + /// UseFacebookAuthentication is obsolete. Configure Facebook authentication with AddAuthentication().AddFacebook in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. - /// A that specifies options for the middleware. + /// The to add the handler to. + /// A that specifies options for the handler. /// A reference to this instance after the operation has completed. + [Obsolete("UseFacebookAuthentication is obsolete. Configure Facebook authentication with AddAuthentication().AddFacebook in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseFacebookAuthentication(this IApplicationBuilder app, FacebookOptions options) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - return app.UseMiddleware(Options.Create(options)); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs index da65e246b..ff9bb40bf 100644 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs @@ -7,10 +7,13 @@ public static class FacebookDefaults { public const string AuthenticationScheme = "Facebook"; - public static readonly string AuthorizationEndpoint = "https://www.facebook.com/v2.5/dialog/oauth"; + public static readonly string DisplayName = "Facebook"; - public static readonly string TokenEndpoint = "https://graph.facebook.com/v2.5/oauth/access_token"; + // https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow#login + public static readonly string AuthorizationEndpoint = "https://www.facebook.com/v3.1/dialog/oauth"; - public static readonly string UserInformationEndpoint = "https://graph.facebook.com/v2.5/me"; + public static readonly string TokenEndpoint = "https://graph.facebook.com/v3.1/oauth/access_token"; + + public static readonly string UserInformationEndpoint = "https://graph.facebook.com/v3.1/me"; } } diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookExtensions.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookExtensions.cs new file mode 100644 index 000000000..2273724a4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Facebook; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class FacebookAuthenticationOptionsExtensions + { + public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder) + => builder.AddFacebook(FacebookDefaults.AuthenticationScheme, _ => { }); + + public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddFacebook(FacebookDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddFacebook(authenticationScheme, FacebookDefaults.DisplayName, configureOptions); + + public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + => builder.AddOAuth(authenticationScheme, displayName, configureOptions); + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs index 675ae5149..eb4251143 100644 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs @@ -1,26 +1,27 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; using System.Globalization; using System.Net.Http; using System.Security.Claims; using System.Security.Cryptography; using System.Text; +using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.OAuth; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.Authentication.Facebook { - internal class FacebookHandler : OAuthHandler + public class FacebookHandler : OAuthHandler { - public FacebookHandler(HttpClient httpClient) - : base(httpClient) - { - } + public FacebookHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } protected override async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) { @@ -35,100 +36,20 @@ protected override async Task CreateTicketAsync(ClaimsIden } var response = await Backchannel.GetAsync(endpoint, Context.RequestAborted); - response.EnsureSuccessStatusCode(); - - var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); - - var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), properties, Options.AuthenticationScheme); - var context = new OAuthCreatingTicketContext(ticket, Context, Options, Backchannel, tokens, payload); - - var identifier = FacebookHelper.GetId(payload); - if (!string.IsNullOrEmpty(identifier)) - { - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, identifier, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var ageRangeMin = FacebookHelper.GetAgeRangeMin(payload); - if (!string.IsNullOrEmpty(ageRangeMin)) - { - identity.AddClaim(new Claim("urn:facebook:age_range_min", ageRangeMin, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var ageRangeMax = FacebookHelper.GetAgeRangeMax(payload); - if (!string.IsNullOrEmpty(ageRangeMax)) + if (!response.IsSuccessStatusCode) { - identity.AddClaim(new Claim("urn:facebook:age_range_max", ageRangeMax, ClaimValueTypes.String, Options.ClaimsIssuer)); + throw new HttpRequestException($"An error occurred when retrieving Facebook user information ({response.StatusCode}). Please check if the authentication information is correct and the corresponding Facebook Graph API is enabled."); } - var birthday = FacebookHelper.GetBirthday(payload); - if (!string.IsNullOrEmpty(birthday)) - { - identity.AddClaim(new Claim(ClaimTypes.DateOfBirth, birthday, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var email = FacebookHelper.GetEmail(payload); - if (!string.IsNullOrEmpty(email)) - { - identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var firstName = FacebookHelper.GetFirstName(payload); - if (!string.IsNullOrEmpty(firstName)) - { - identity.AddClaim(new Claim(ClaimTypes.GivenName, firstName, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var gender = FacebookHelper.GetGender(payload); - if (!string.IsNullOrEmpty(gender)) - { - identity.AddClaim(new Claim(ClaimTypes.Gender, gender, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var lastName = FacebookHelper.GetLastName(payload); - if (!string.IsNullOrEmpty(lastName)) - { - identity.AddClaim(new Claim(ClaimTypes.Surname, lastName, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var link = FacebookHelper.GetLink(payload); - if (!string.IsNullOrEmpty(link)) - { - identity.AddClaim(new Claim("urn:facebook:link", link, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var location = FacebookHelper.GetLocation(payload); - if (!string.IsNullOrEmpty(location)) - { - identity.AddClaim(new Claim("urn:facebook:location", location, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var locale = FacebookHelper.GetLocale(payload); - if (!string.IsNullOrEmpty(locale)) - { - identity.AddClaim(new Claim(ClaimTypes.Locality, locale, ClaimValueTypes.String, Options.ClaimsIssuer)); - } + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); - var middleName = FacebookHelper.GetMiddleName(payload); - if (!string.IsNullOrEmpty(middleName)) - { - identity.AddClaim(new Claim("urn:facebook:middle_name", middleName, ClaimValueTypes.String, Options.ClaimsIssuer)); - } + var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload); + context.RunClaimActions(); - var name = FacebookHelper.GetName(payload); - if (!string.IsNullOrEmpty(name)) - { - identity.AddClaim(new Claim(ClaimTypes.Name, name, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var timeZone = FacebookHelper.GetTimeZone(payload); - if (!string.IsNullOrEmpty(timeZone)) - { - identity.AddClaim(new Claim("urn:facebook:timezone", timeZone, ClaimValueTypes.String, Options.ClaimsIssuer)); - } + await Events.CreatingTicket(context); - await Options.Events.CreatingTicket(context); + return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); - return context.Ticket; } private string GenerateAppSecretProof(string accessToken) @@ -145,12 +66,15 @@ private string GenerateAppSecretProof(string accessToken) } } - protected override string FormatScope() + protected override string FormatScope(IEnumerable scopes) { // Facebook deviates from the OAuth spec here. They require comma separated instead of space separated. // https://developers.facebook.com/docs/reference/dialogs/oauth // http://tools.ietf.org/html/rfc6749#section-3.3 - return string.Join(",", Options.Scope); + return string.Join(",", scopes); } + + protected override string FormatScope() + => base.FormatScope(); } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHelper.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHelper.cs deleted file mode 100644 index 48e359099..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHelper.cs +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Newtonsoft.Json.Linq; - -namespace Microsoft.AspNetCore.Authentication.Facebook -{ - /// - /// Contains static methods that allow to extract user's information from a - /// instance retrieved from Facebook after a successful authentication process. - /// - public static class FacebookHelper - { - /// - /// Gets the Facebook user ID. - /// - public static string GetId(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("id"); - } - - /// - /// Gets the user's min age. - /// - public static string GetAgeRangeMin(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return TryGetValue(user, "age_range", "min"); - } - - /// - /// Gets the user's max age. - /// - public static string GetAgeRangeMax(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return TryGetValue(user, "age_range", "max"); - } - - /// - /// Gets the user's birthday. - /// - public static string GetBirthday(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return user.Value("birthday"); - } - - /// - /// Gets the Facebook email. - /// - public static string GetEmail(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("email"); - } - - /// - /// Gets the user's first name. - /// - public static string GetFirstName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("first_name"); - } - - /// - /// Gets the user's gender. - /// - public static string GetGender(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return user.Value("gender"); - } - - /// - /// Gets the user's family name. - /// - public static string GetLastName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("last_name"); - } - - /// - /// Gets the user's link. - /// - public static string GetLink(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return user.Value("link"); - } - - /// - /// Gets the user's location. - /// - public static string GetLocation(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return TryGetValue(user, "location", "name"); - } - - /// - /// Gets the user's locale. - /// - public static string GetLocale(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return user.Value("locale"); - } - - /// - /// Gets the user's middle name. - /// - public static string GetMiddleName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("middle_name"); - } - - /// - /// Gets the user's name. - /// - public static string GetName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("name"); - } - - /// - /// Gets the user's timezone. - /// - public static string GetTimeZone(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return user.Value("timezone"); - } - - // Get the given subProperty from a property. - private static string TryGetValue(JObject user, string propertyName, string subProperty) - { - JToken value; - if (user.TryGetValue(propertyName, out value)) - { - var subObject = JObject.Parse(value.ToString()); - if (subObject != null && subObject.TryGetValue(subProperty, out value)) - { - return value.ToString(); - } - } - return null; - } - - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookMiddleware.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookMiddleware.cs deleted file mode 100644 index ac57e8dde..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookMiddleware.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Globalization; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Authentication.OAuth; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Authentication.Facebook -{ - /// - /// An ASP.NET Core middleware for authenticating users using Facebook. - /// - public class FacebookMiddleware : OAuthMiddleware - { - /// - /// Initializes a new . - /// - /// The next middleware in the HTTP pipeline to invoke. - /// - /// - /// - /// - /// Configuration options for the middleware. - public FacebookMiddleware( - RequestDelegate next, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - UrlEncoder encoder, - IOptions sharedOptions, - IOptions options) - : base(next, dataProtectionProvider, loggerFactory, encoder, sharedOptions, options) - { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - - if (dataProtectionProvider == null) - { - throw new ArgumentNullException(nameof(dataProtectionProvider)); - } - - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - if (sharedOptions == null) - { - throw new ArgumentNullException(nameof(sharedOptions)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (string.IsNullOrEmpty(Options.AppId)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.AppId))); - } - - if (string.IsNullOrEmpty(Options.AppSecret)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.AppSecret))); - } - } - - /// - /// Provides the object for processing authentication-related requests. - /// - /// An configured with the supplied to the constructor. - protected override AuthenticationHandler CreateHandler() - { - return new FacebookHandler(Backchannel); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs index 8e86b37c1..c2078a017 100644 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs @@ -1,14 +1,18 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; -using Microsoft.AspNetCore.Authentication.Facebook; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using System.Globalization; +using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Authentication.Facebook { /// - /// Configuration options for . + /// Configuration options for . /// public class FacebookOptions : OAuthOptions { @@ -17,19 +21,49 @@ public class FacebookOptions : OAuthOptions /// public FacebookOptions() { - AuthenticationScheme = FacebookDefaults.AuthenticationScheme; - DisplayName = AuthenticationScheme; CallbackPath = new PathString("/signin-facebook"); SendAppSecretProof = true; AuthorizationEndpoint = FacebookDefaults.AuthorizationEndpoint; TokenEndpoint = FacebookDefaults.TokenEndpoint; UserInformationEndpoint = FacebookDefaults.UserInformationEndpoint; - Scope.Add("public_profile"); Scope.Add("email"); Fields.Add("name"); Fields.Add("email"); Fields.Add("first_name"); Fields.Add("last_name"); + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + ClaimActions.MapJsonSubKey("urn:facebook:age_range_min", "age_range", "min"); + ClaimActions.MapJsonSubKey("urn:facebook:age_range_max", "age_range", "max"); + ClaimActions.MapJsonKey(ClaimTypes.DateOfBirth, "birthday"); + ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); + ClaimActions.MapJsonKey(ClaimTypes.GivenName, "first_name"); + ClaimActions.MapJsonKey("urn:facebook:middle_name", "middle_name"); + ClaimActions.MapJsonKey(ClaimTypes.Surname, "last_name"); + ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender"); + ClaimActions.MapJsonKey("urn:facebook:link", "link"); + ClaimActions.MapJsonSubKey("urn:facebook:location", "location", "name"); + ClaimActions.MapJsonKey(ClaimTypes.Locality, "locale"); + ClaimActions.MapJsonKey("urn:facebook:timezone", "timezone"); + } + + /// + /// Check that the options are valid. Should throw an exception if things are not ok. + /// + public override void Validate() + { + if (string.IsNullOrEmpty(AppId)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(AppId)), nameof(AppId)); + } + + if (string.IsNullOrEmpty(AppSecret)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(AppSecret)), nameof(AppSecret)); + } + + base.Validate(); } // Facebook uses a non-standard term for this field. diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj b/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj new file mode 100644 index 000000000..f57fe26e0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj @@ -0,0 +1,15 @@ + + + + ASP.NET Core middleware that enables an application to support Facebook's OAuth 2.0 authentication workflow. + netcoreapp3.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.xproj b/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.xproj deleted file mode 100644 index 395259212..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.xproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - eeaaee68-607b-4e33-af3e-45c66b4dba5a - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/AssemblyInfo.cs deleted file mode 100644 index 76feceeff..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Reflection; -using System.Resources; - -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: AssemblyCompany("Microsoft Corporation.")] -[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/Resources.Designer.cs index a3a2f2874..655da24a3 100644 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/Properties/Resources.Designer.cs @@ -15,16 +15,14 @@ private static readonly ResourceManager _resourceManager /// internal static string Exception_OptionMustBeProvided { - get { return GetString("Exception_OptionMustBeProvided"); } + get => GetString("Exception_OptionMustBeProvided"); } /// /// The '{0}' option must be provided. /// internal static string FormatException_OptionMustBeProvided(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); - } + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); private static string GetString(string name, params string[] formatterNames) { diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json new file mode 100644 index 000000000..5d95efca6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json @@ -0,0 +1,390 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Facebook, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.FacebookAuthenticationOptionsExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddFacebook", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddFacebook", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddFacebook", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddFacebook", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Facebook.FacebookDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthorizationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "TokenEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "UserInformationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Facebook\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Facebook.FacebookHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CreateTicketAsync", + "Parameters": [ + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokens", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatScope", + "Parameters": [ + { + "Name": "scopes", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatScope", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Facebook.FacebookOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AppId", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AppId", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AppSecret", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AppSecret", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SendAppSecretProof", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SendAppSecretProof", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Fields", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.FacebookAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseFacebookAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseFacebookAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Facebook.FacebookOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/project.json b/src/Microsoft.AspNetCore.Authentication.Facebook/project.json deleted file mode 100644 index 663991465..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/project.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "ASP.NET Core middleware that enables an application to support Facebook's OAuth 2.0 authentication workflow.", - "tags": [ - "aspnetcore", - "authentication", - "security" - ], - "repository": { - "type": "git", - "url": "git://github.com/aspnet/security" - }, - "compilationOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk", - "nowarn": [ - "CS1591" - ], - "xmlDoc": true - }, - "dependencies": { - "Microsoft.AspNetCore.Authentication.OAuth": "1.0.0-*" - }, - "frameworks": { - "net451": {}, - "netstandard1.3": { - "imports": [ - "portable-net451+win8" - ] - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Google/GoogleAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Authentication.Google/GoogleAppBuilderExtensions.cs index fe693a61b..4302d20db 100644 --- a/src/Microsoft.AspNetCore.Authentication.Google/GoogleAppBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Google/GoogleAppBuilderExtensions.cs @@ -3,7 +3,6 @@ using System; using Microsoft.AspNetCore.Authentication.Google; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { @@ -13,38 +12,26 @@ namespace Microsoft.AspNetCore.Builder public static class GoogleAppBuilderExtensions { /// - /// Adds the middleware to the specified , which enables Google authentication capabilities. + /// UseGoogleAuthentication is obsolete. Configure Google authentication with AddAuthentication().AddGoogle in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. + /// The to add the handler to. /// A reference to this instance after the operation has completed. + [Obsolete("UseGoogleAuthentication is obsolete. Configure Google authentication with AddAuthentication().AddGoogle in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseGoogleAuthentication(this IApplicationBuilder app) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware(); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } /// - /// Adds the middleware to the specified , which enables Google authentication capabilities. + /// UseGoogleAuthentication is obsolete. Configure Google authentication with AddAuthentication().AddGoogle in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. - /// A that specifies options for the middleware. + /// The to add the handler to. + /// A that specifies options for the handler. /// A reference to this instance after the operation has completed. + [Obsolete("UseGoogleAuthentication is obsolete. Configure Google authentication with AddAuthentication().AddGoogle in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseGoogleAuthentication(this IApplicationBuilder app, GoogleOptions options) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - return app.UseMiddleware(Options.Create(options)); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } } } diff --git a/src/Microsoft.AspNetCore.Authentication.Google/GoogleChallengeProperties.cs b/src/Microsoft.AspNetCore.Authentication.Google/GoogleChallengeProperties.cs new file mode 100644 index 000000000..714df4565 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Google/GoogleChallengeProperties.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Authentication.OAuth; + +namespace Microsoft.AspNetCore.Authentication.Google +{ + public class GoogleChallengeProperties : OAuthChallengeProperties + { + /// + /// The parameter key for the "access_type" argument being used for a challenge request. + /// + public static readonly string AccessTypeKey = "access_type"; + + /// + /// The parameter key for the "approval_prompt" argument being used for a challenge request. + /// + public static readonly string ApprovalPromptKey = "approval_prompt"; + + /// + /// The parameter key for the "include_granted_scopes" argument being used for a challenge request. + /// + public static readonly string IncludeGrantedScopesKey = "include_granted_scopes"; + + /// + /// The parameter key for the "login_hint" argument being used for a challenge request. + /// + public static readonly string LoginHintKey = "login_hint"; + + /// + /// The parameter key for the "prompt" argument being used for a challenge request. + /// + public static readonly string PromptParameterKey = "prompt"; + + public GoogleChallengeProperties() + { } + + public GoogleChallengeProperties(IDictionary items) + : base(items) + { } + + public GoogleChallengeProperties(IDictionary items, IDictionary parameters) + : base(items, parameters) + { } + + /// + /// The "access_type" parameter value being used for a challenge request. + /// + public string AccessType + { + get => GetParameter(AccessTypeKey); + set => SetParameter(AccessTypeKey, value); + } + + /// + /// The "approval_prompt" parameter value being used for a challenge request. + /// + public string ApprovalPrompt + { + get => GetParameter(ApprovalPromptKey); + set => SetParameter(ApprovalPromptKey, value); + } + + /// + /// The "include_granted_scopes" parameter value being used for a challenge request. + /// + public bool? IncludeGrantedScopes + { + get => GetParameter(IncludeGrantedScopesKey); + set => SetParameter(IncludeGrantedScopesKey, value); + } + + /// + /// The "login_hint" parameter value being used for a challenge request. + /// + public string LoginHint + { + get => GetParameter(LoginHintKey); + set => SetParameter(LoginHintKey, value); + } + + /// + /// The "prompt" parameter value being used for a challenge request. + /// + public string Prompt + { + get => GetParameter(PromptParameterKey); + set => SetParameter(PromptParameterKey, value); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.cs b/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.cs index 06fd12eb1..26b3b8f01 100644 --- a/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.cs +++ b/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.cs @@ -3,14 +3,21 @@ namespace Microsoft.AspNetCore.Authentication.Google { + /// + /// Default values for Google authentication + /// public static class GoogleDefaults { public const string AuthenticationScheme = "Google"; - public static readonly string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/auth"; + public static readonly string DisplayName = "Google"; - public static readonly string TokenEndpoint = "https://www.googleapis.com/oauth2/v3/token"; + // https://developers.google.com/identity/protocols/OAuth2WebServer + public static readonly string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth"; + public static readonly string TokenEndpoint = "https://www.googleapis.com/oauth2/v4/token"; + + // https://developers.google.com/+/web/people/ public static readonly string UserInformationEndpoint = "https://www.googleapis.com/plus/v1/people/me"; } } diff --git a/src/Microsoft.AspNetCore.Authentication.Google/GoogleExtensions.cs b/src/Microsoft.AspNetCore.Authentication.Google/GoogleExtensions.cs new file mode 100644 index 000000000..95547014c --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Google/GoogleExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Google; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class GoogleExtensions + { + public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder) + => builder.AddGoogle(GoogleDefaults.AuthenticationScheme, _ => { }); + + public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddGoogle(GoogleDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddGoogle(authenticationScheme, GoogleDefaults.DisplayName, configureOptions); + + public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + => builder.AddOAuth(authenticationScheme, displayName, configureOptions); + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs b/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs index affde917a..88d48d446 100644 --- a/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs @@ -6,92 +6,63 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; +using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.OAuth; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.Authentication.Google { - internal class GoogleHandler : OAuthHandler + public class GoogleHandler : OAuthHandler { - public GoogleHandler(HttpClient httpClient) - : base(httpClient) - { - } - - protected override async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) + public GoogleHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } + + protected override async Task CreateTicketAsync( + ClaimsIdentity identity, + AuthenticationProperties properties, + OAuthTokenResponse tokens) { // Get the Google user var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); var response = await Backchannel.SendAsync(request, Context.RequestAborted); - response.EnsureSuccessStatusCode(); - - var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); - - var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), properties, Options.AuthenticationScheme); - var context = new OAuthCreatingTicketContext(ticket, Context, Options, Backchannel, tokens, payload); - - var identifier = GoogleHelper.GetId(payload); - if (!string.IsNullOrEmpty(identifier)) - { - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, identifier, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var givenName = GoogleHelper.GetGivenName(payload); - if (!string.IsNullOrEmpty(givenName)) - { - identity.AddClaim(new Claim(ClaimTypes.GivenName, givenName, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var familyName = GoogleHelper.GetFamilyName(payload); - if (!string.IsNullOrEmpty(familyName)) + if (!response.IsSuccessStatusCode) { - identity.AddClaim(new Claim(ClaimTypes.Surname, familyName, ClaimValueTypes.String, Options.ClaimsIssuer)); + throw new HttpRequestException($"An error occurred when retrieving Google user information ({response.StatusCode}). Please check if the authentication information is correct and the corresponding Google+ API is enabled."); } - var name = GoogleHelper.GetName(payload); - if (!string.IsNullOrEmpty(name)) - { - identity.AddClaim(new Claim(ClaimTypes.Name, name, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var email = GoogleHelper.GetEmail(payload); - if (!string.IsNullOrEmpty(email)) - { - identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var profile = GoogleHelper.GetProfile(payload); - if (!string.IsNullOrEmpty(profile)) - { - identity.AddClaim(new Claim("urn:google:profile", profile, ClaimValueTypes.String, Options.ClaimsIssuer)); - } + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); - await Options.Events.CreatingTicket(context); + var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload); + context.RunClaimActions(); - return context.Ticket; + await Events.CreatingTicket(context); + return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); } // TODO: Abstract this properties override pattern into the base class? protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) { - var scope = FormatScope(); + // Google Identity Platform Manual: + // https://developers.google.com/identity/protocols/OAuth2WebServer var queryStrings = new Dictionary(StringComparer.OrdinalIgnoreCase); queryStrings.Add("response_type", "code"); queryStrings.Add("client_id", Options.ClientId); queryStrings.Add("redirect_uri", redirectUri); - AddQueryString(queryStrings, properties, "scope", scope); - - AddQueryString(queryStrings, properties, "access_type", Options.AccessType); - AddQueryString(queryStrings, properties, "approval_prompt"); - AddQueryString(queryStrings, properties, "login_hint"); + AddQueryString(queryStrings, properties, GoogleChallengeProperties.ScopeKey, FormatScope, Options.Scope); + AddQueryString(queryStrings, properties, GoogleChallengeProperties.AccessTypeKey, Options.AccessType); + AddQueryString(queryStrings, properties, GoogleChallengeProperties.ApprovalPromptKey); + AddQueryString(queryStrings, properties, GoogleChallengeProperties.PromptParameterKey); + AddQueryString(queryStrings, properties, GoogleChallengeProperties.LoginHintKey); + AddQueryString(queryStrings, properties, GoogleChallengeProperties.IncludeGrantedScopesKey, v => v?.ToString().ToLower(), (bool?)null); var state = Options.StateDataFormat.Protect(properties); queryStrings.Add("state", state); @@ -100,26 +71,38 @@ protected override string BuildChallengeUrl(AuthenticationProperties properties, return authorizationEndpoint; } - private static void AddQueryString(IDictionary queryStrings, AuthenticationProperties properties, - string name, string defaultValue = null) + private void AddQueryString( + IDictionary queryStrings, + AuthenticationProperties properties, + string name, + Func formatter, + T defaultValue) { - string value; - if (!properties.Items.TryGetValue(name, out value)) + string value = null; + var parameterValue = properties.GetParameter(name); + if (parameterValue != null) { - value = defaultValue; + value = formatter(parameterValue); } - else + else if (!properties.Items.TryGetValue(name, out value)) { - // Remove the parameter from AuthenticationProperties so it won't be serialized to state parameter - properties.Items.Remove(name); + value = formatter(defaultValue); } - if (value == null) + // Remove the parameter from AuthenticationProperties so it won't be serialized into the state + properties.Items.Remove(name); + + if (value != null) { - return; + queryStrings[name] = value; } - - queryStrings[name] = value; } + + private void AddQueryString( + IDictionary queryStrings, + AuthenticationProperties properties, + string name, + string defaultValue = null) + => AddQueryString(queryStrings, properties, name, x => x, defaultValue); } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs b/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs index 0a763d569..2cac949a0 100644 --- a/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs +++ b/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.Extensions.Internal; using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.Authentication.Google @@ -13,71 +12,6 @@ namespace Microsoft.AspNetCore.Authentication.Google /// public static class GoogleHelper { - /// - /// Gets the Google user ID. - /// - public static string GetId(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("id"); - } - - /// - /// Gets the user's name. - /// - public static string GetName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("displayName"); - } - - /// - /// Gets the user's given name. - /// - public static string GetGivenName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return TryGetValue(user, "name", "givenName"); - } - - /// - /// Gets the user's family name. - /// - public static string GetFamilyName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return TryGetValue(user, "name", "familyName"); - } - - /// - /// Gets the user's profile link. - /// - public static string GetProfile(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("url"); - } - /// /// Gets the user's email. /// @@ -91,21 +25,6 @@ public static string GetEmail(JObject user) return TryGetFirstValue(user, "emails", "value"); } - // Get the given subProperty from a property. - private static string TryGetValue(JObject user, string propertyName, string subProperty) - { - JToken value; - if (user.TryGetValue(propertyName, out value)) - { - var subObject = JObject.Parse(value.ToString()); - if (subObject != null && subObject.TryGetValue(subProperty, out value)) - { - return value.ToString(); - } - } - return null; - } - // Get the given subProperty from a list property. private static string TryGetFirstValue(JObject user, string propertyName, string subProperty) { diff --git a/src/Microsoft.AspNetCore.Authentication.Google/GoogleMiddleware.cs b/src/Microsoft.AspNetCore.Authentication.Google/GoogleMiddleware.cs deleted file mode 100644 index eb98e447b..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Google/GoogleMiddleware.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Authentication.OAuth; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Authentication.Google -{ - /// - /// An ASP.NET Core middleware for authenticating users using Google OAuth 2.0. - /// - public class GoogleMiddleware : OAuthMiddleware - { - /// - /// Initializes a new . - /// - /// The next middleware in the HTTP pipeline to invoke. - /// - /// - /// - /// - /// Configuration options for the middleware. - public GoogleMiddleware( - RequestDelegate next, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - UrlEncoder encoder, - IOptions sharedOptions, - IOptions options) - : base(next, dataProtectionProvider, loggerFactory, encoder, sharedOptions, options) - { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - - if (dataProtectionProvider == null) - { - throw new ArgumentNullException(nameof(dataProtectionProvider)); - } - - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - if (sharedOptions == null) - { - throw new ArgumentNullException(nameof(sharedOptions)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - } - - /// - /// Provides the object for processing authentication-related requests. - /// - /// An configured with the supplied to the constructor. - protected override AuthenticationHandler CreateHandler() - { - return new GoogleHandler(Backchannel); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs b/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs index 3d93b96ea..34028bc52 100644 --- a/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs @@ -1,13 +1,15 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Authentication.Google; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Authentication.Google { /// - /// Configuration options for . + /// Configuration options for . /// public class GoogleOptions : OAuthOptions { @@ -16,8 +18,6 @@ public class GoogleOptions : OAuthOptions /// public GoogleOptions() { - AuthenticationScheme = GoogleDefaults.AuthenticationScheme; - DisplayName = AuthenticationScheme; CallbackPath = new PathString("/signin-google"); AuthorizationEndpoint = GoogleDefaults.AuthorizationEndpoint; TokenEndpoint = GoogleDefaults.TokenEndpoint; @@ -25,6 +25,13 @@ public GoogleOptions() Scope.Add("openid"); Scope.Add("profile"); Scope.Add("email"); + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName"); + ClaimActions.MapJsonSubKey(ClaimTypes.GivenName, "name", "givenName"); + ClaimActions.MapJsonSubKey(ClaimTypes.Surname, "name", "familyName"); + ClaimActions.MapJsonKey("urn:google:profile", "url"); + ClaimActions.MapCustomJson(ClaimTypes.Email, GoogleHelper.GetEmail); } /// diff --git a/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj b/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj new file mode 100644 index 000000000..eba5b98ad --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj @@ -0,0 +1,15 @@ + + + + ASP.NET Core contains middleware to support Google's OpenId and OAuth 2.0 authentication workflows. + netcoreapp3.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.xproj b/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.xproj deleted file mode 100644 index ff412f08a..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.xproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 76579c39-b829-490d-b8be-1bd35fe8412e - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Google/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Authentication.Google/Properties/AssemblyInfo.cs deleted file mode 100644 index 76feceeff..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Google/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Reflection; -using System.Resources; - -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: AssemblyCompany("Microsoft Corporation.")] -[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Authentication.Google/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Authentication.Google/Properties/Resources.Designer.cs index 690c5a280..03448b408 100644 --- a/src/Microsoft.AspNetCore.Authentication.Google/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Authentication.Google/Properties/Resources.Designer.cs @@ -15,32 +15,28 @@ private static readonly ResourceManager _resourceManager /// internal static string Exception_OptionMustBeProvided { - get { return GetString("Exception_OptionMustBeProvided"); } + get => GetString("Exception_OptionMustBeProvided"); } /// /// The '{0}' option must be provided. /// internal static string FormatException_OptionMustBeProvided(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); - } + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); /// /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. /// internal static string Exception_ValidatorHandlerMismatch { - get { return GetString("Exception_ValidatorHandlerMismatch"); } + get => GetString("Exception_ValidatorHandlerMismatch"); } /// /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. /// internal static string FormatException_ValidatorHandlerMismatch() - { - return GetString("Exception_ValidatorHandlerMismatch"); - } + => GetString("Exception_ValidatorHandlerMismatch"); private static string GetString(string name, params string[] formatterNames) { diff --git a/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json new file mode 100644 index 000000000..0a623b3b8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json @@ -0,0 +1,550 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Google, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.GoogleExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddGoogle", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddGoogle", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddGoogle", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddGoogle", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleChallengeProperties", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthChallengeProperties", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AccessType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AccessType", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ApprovalPrompt", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ApprovalPrompt", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IncludeGrantedScopes", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IncludeGrantedScopes", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LoginHint", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_LoginHint", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Prompt", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Prompt", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "parameters", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AccessTypeKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "ApprovalPromptKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "IncludeGrantedScopesKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "LoginHintKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "PromptParameterKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthorizationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "TokenEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "UserInformationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Google\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CreateTicketAsync", + "Parameters": [ + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokens", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "BuildChallengeUrl", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "redirectUri", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleHelper", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetEmail", + "Parameters": [ + { + "Name": "user", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Google.GoogleOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AccessType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AccessType", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.GoogleAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseGoogleAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseGoogleAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Google.GoogleOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Google/project.json b/src/Microsoft.AspNetCore.Authentication.Google/project.json deleted file mode 100644 index 9740543ec..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Google/project.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "ASP.NET Core contains middleware to support Google's OpenId and OAuth 2.0 authentication workflows.", - "tags": [ - "aspnetcore", - "authentication", - "security" - ], - "repository": { - "type": "git", - "url": "git://github.com/aspnet/security" - }, - "compilationOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk", - "nowarn": [ - "CS1591" - ], - "xmlDoc": true - }, - "dependencies": { - "Microsoft.AspNetCore.Authentication.OAuth": "1.0.0-*" - }, - "frameworks": { - "net451": {}, - "netstandard1.3": { - "imports": [ - "portable-net451+win8" - ] - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs index b3e0f0bdc..1c2efd6c7 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/AuthenticationFailedContext.cs @@ -2,17 +2,17 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Authentication.JwtBearer { - public class AuthenticationFailedContext : BaseJwtBearerContext + public class AuthenticationFailedContext : ResultContext { - public AuthenticationFailedContext(HttpContext context, JwtBearerOptions options) - : base(context, options) - { - } + public AuthenticationFailedContext( + HttpContext context, + AuthenticationScheme scheme, + JwtBearerOptions options) + : base(context, scheme, options) { } public Exception Exception { get; set; } } diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/BaseJwtBearerContext.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/BaseJwtBearerContext.cs deleted file mode 100644 index 5c28f2976..000000000 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/BaseJwtBearerContext.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Authentication.JwtBearer -{ - public class BaseJwtBearerContext : BaseControlContext - { - public BaseJwtBearerContext(HttpContext context, JwtBearerOptions options) - : base(context) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - Options = options; - } - - public JwtBearerOptions Options { get; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/IJwtBearerEvents.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/IJwtBearerEvents.cs deleted file mode 100644 index a7b8aeb55..000000000 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/IJwtBearerEvents.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Authentication.JwtBearer -{ - /// - /// Specifies events which the invokes to enable developer control over the authentication process. - /// - public interface IJwtBearerEvents - { - /// - /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. - /// - Task AuthenticationFailed(AuthenticationFailedContext context); - - /// - /// Invoked when a protocol message is first received. - /// - Task MessageReceived(MessageReceivedContext context); - - /// - /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. - /// - Task TokenValidated(TokenValidatedContext context); - - /// - /// Invoked to apply a challenge sent back to the caller. - /// - Task Challenge(JwtBearerChallengeContext context); - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs index b3a4d21ba..6500e1e3f 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs @@ -1,20 +1,53 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Builder; +using System; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; namespace Microsoft.AspNetCore.Authentication.JwtBearer { - public class JwtBearerChallengeContext : BaseJwtBearerContext + public class JwtBearerChallengeContext : PropertiesContext { - public JwtBearerChallengeContext(HttpContext context, JwtBearerOptions options, AuthenticationProperties properties) - : base(context, options) - { - Properties = properties; - } + public JwtBearerChallengeContext( + HttpContext context, + AuthenticationScheme scheme, + JwtBearerOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) { } - public AuthenticationProperties Properties { get; } + /// + /// Any failures encountered during the authentication process. + /// + public Exception AuthenticateFailure { get; set; } + + /// + /// Gets or sets the "error" value returned to the caller as part + /// of the WWW-Authenticate header. This property may be null when + /// is set to false. + /// + public string Error { get; set; } + + /// + /// Gets or sets the "error_description" value returned to the caller as part + /// of the WWW-Authenticate header. This property may be null when + /// is set to false. + /// + public string ErrorDescription { get; set; } + + /// + /// Gets or sets the "error_uri" value returned to the caller as part of the + /// WWW-Authenticate header. This property is always null unless explicitly set. + /// + public string ErrorUri { get; set; } + + /// + /// If true, will skip any default logic for this challenge. + /// + public bool Handled { get; private set; } + + /// + /// Skips any default logic for this challenge. + /// + public void HandleResponse() => Handled = true; } } diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerEvents.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerEvents.cs index 38a877f66..a9b35c310 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerEvents.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerEvents.cs @@ -7,29 +7,29 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer { /// - /// Specifies events which the invokes to enable developer control over the authentication process. + /// Specifies events which the invokes to enable developer control over the authentication process. /// - public class JwtBearerEvents : IJwtBearerEvents + public class JwtBearerEvents { /// /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. /// - public Func OnAuthenticationFailed { get; set; } = context => Task.FromResult(0); + public Func OnAuthenticationFailed { get; set; } = context => Task.CompletedTask; /// /// Invoked when a protocol message is first received. /// - public Func OnMessageReceived { get; set; } = context => Task.FromResult(0); + public Func OnMessageReceived { get; set; } = context => Task.CompletedTask; /// /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. /// - public Func OnTokenValidated { get; set; } = context => Task.FromResult(0); + public Func OnTokenValidated { get; set; } = context => Task.CompletedTask; /// /// Invoked before a challenge is sent back to the caller. /// - public Func OnChallenge { get; set; } = context => Task.FromResult(0); + public Func OnChallenge { get; set; } = context => Task.CompletedTask; public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context); diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/MessageReceivedContext.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/MessageReceivedContext.cs index a23f8356d..1850ad049 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/MessageReceivedContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/MessageReceivedContext.cs @@ -1,20 +1,20 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Authentication.JwtBearer { - public class MessageReceivedContext : BaseJwtBearerContext + public class MessageReceivedContext : ResultContext { - public MessageReceivedContext(HttpContext context, JwtBearerOptions options) - : base(context, options) - { - } + public MessageReceivedContext( + HttpContext context, + AuthenticationScheme scheme, + JwtBearerOptions options) + : base(context, scheme, options) { } /// - /// Bearer Token. This will give application an opportunity to retrieve token from an alternation location. + /// Bearer Token. This will give the application an opportunity to retrieve a token from an alternative location. /// public string Token { get; set; } } diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/TokenValidatedContext.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/TokenValidatedContext.cs index d6de5ca87..39b677b96 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/TokenValidatedContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/TokenValidatedContext.cs @@ -1,18 +1,18 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.IdentityModel.Tokens; namespace Microsoft.AspNetCore.Authentication.JwtBearer { - public class TokenValidatedContext : BaseJwtBearerContext + public class TokenValidatedContext : ResultContext { - public TokenValidatedContext(HttpContext context, JwtBearerOptions options) - : base(context, options) - { - } + public TokenValidatedContext( + HttpContext context, + AuthenticationScheme scheme, + JwtBearerOptions options) + : base(context, scheme, options) { } public SecurityToken SecurityToken { get; set; } } diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerAppBuilderExtensions.cs index 13c06ca38..0cfc97573 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerAppBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerAppBuilderExtensions.cs @@ -1,9 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { @@ -13,50 +12,26 @@ namespace Microsoft.AspNetCore.Builder public static class JwtBearerAppBuilderExtensions { /// - /// Adds the middleware to the specified , which enables Bearer token processing capabilities. - /// This middleware understands appropriately - /// formatted and secured tokens which appear in the request header. If the Options.AuthenticationMode is Active, the - /// claims within the bearer token are added to the current request's IPrincipal User. If the Options.AuthenticationMode - /// is Passive, then the current request is not modified, but IAuthenticationManager AuthenticateAsync may be used at - /// any time to obtain the claims from the request's bearer token. - /// See also http://tools.ietf.org/html/rfc6749 + /// UseJwtBearerAuthentication is obsolete. Configure JwtBearer authentication with AddAuthentication().AddJwtBearer in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. + /// The to add the handler to. /// A reference to this instance after the operation has completed. + [Obsolete("UseJwtBearerAuthentication is obsolete. Configure JwtBearer authentication with AddAuthentication().AddJwtBearer in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseJwtBearerAuthentication(this IApplicationBuilder app) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware(); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } /// - /// Adds the middleware to the specified , which enables Bearer token processing capabilities. - /// This middleware understands appropriately - /// formatted and secured tokens which appear in the request header. If the Options.AuthenticationMode is Active, the - /// claims within the bearer token are added to the current request's IPrincipal User. If the Options.AuthenticationMode - /// is Passive, then the current request is not modified, but IAuthenticationManager AuthenticateAsync may be used at - /// any time to obtain the claims from the request's bearer token. - /// See also http://tools.ietf.org/html/rfc6749 + /// UseJwtBearerAuthentication is obsolete. Configure JwtBearer authentication with AddAuthentication().AddJwtBearer in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. - /// A that specifies options for the middleware. + /// The to add the handler to. + /// A that specifies options for the handler. /// A reference to this instance after the operation has completed. + [Obsolete("UseJwtBearerAuthentication is obsolete. Configure JwtBearer authentication with AddAuthentication().AddJwtBearer in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseJwtBearerAuthentication(this IApplicationBuilder app, JwtBearerOptions options) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - return app.UseMiddleware(Options.Create(options)); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerExtensions.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerExtensions.cs new file mode 100644 index 000000000..334407c0d --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class JwtBearerExtensions + { + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder) + => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, _ => { }); + + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddJwtBearer(authenticationScheme, displayName: null, configureOptions: configureOptions); + + public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, JwtBearerPostConfigureOptions>()); + return builder.AddScheme(authenticationScheme, displayName, configureOptions); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs index 40a0b2efd..4f03ccd69 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs @@ -5,22 +5,39 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Authentication.JwtBearer { - internal class JwtBearerHandler : AuthenticationHandler + public class JwtBearerHandler : AuthenticationHandler { private OpenIdConnectConfiguration _configuration; + public JwtBearerHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new JwtBearerEvents Events + { + get => (JwtBearerEvents)base.Events; + set => base.Events = value; + } + + protected override Task CreateEventsAsync() => Task.FromResult(new JwtBearerEvents()); + /// /// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using set in the options. /// @@ -28,17 +45,16 @@ internal class JwtBearerHandler : AuthenticationHandler protected override async Task HandleAuthenticateAsync() { string token = null; - AuthenticateResult result = null; try { // Give application opportunity to find from a different location, adjust, or reject token - var messageReceivedContext = new MessageReceivedContext(Context, Options); + var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options); // event can set the token - await Options.Events.MessageReceived(messageReceivedContext); - if (messageReceivedContext.CheckEventResult(out result)) + await Events.MessageReceived(messageReceivedContext); + if (messageReceivedContext.Result != null) { - return result; + return messageReceivedContext.Result; } // If application retrieved token from somewhere else, use that. @@ -51,7 +67,7 @@ protected override async Task HandleAuthenticateAsync() // If no authorization header found, nothing to process further if (string.IsNullOrEmpty(authorization)) { - return AuthenticateResult.Skip(); + return AuthenticateResult.NoResult(); } if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) @@ -62,7 +78,7 @@ protected override async Task HandleAuthenticateAsync() // If no token found, no further work possible if (string.IsNullOrEmpty(token)) { - return AuthenticateResult.Skip(); + return AuthenticateResult.NoResult(); } } @@ -74,17 +90,11 @@ protected override async Task HandleAuthenticateAsync() var validationParameters = Options.TokenValidationParameters.Clone(); if (_configuration != null) { - if (validationParameters.ValidIssuer == null && !string.IsNullOrEmpty(_configuration.Issuer)) - { - validationParameters.ValidIssuer = _configuration.Issuer; - } - else - { - var issuers = new[] { _configuration.Issuer }; - validationParameters.ValidIssuers = (validationParameters.ValidIssuers == null ? issuers : validationParameters.ValidIssuers.Concat(issuers)); - } + var issuers = new[] { _configuration.Issuer }; + validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers; - validationParameters.IssuerSigningKeys = (validationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : validationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys)); + validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) + ?? _configuration.SigningKeys; } List validationFailures = null; @@ -100,10 +110,11 @@ protected override async Task HandleAuthenticateAsync() } catch (Exception ex) { - Logger.TokenValidationFailed(token, ex); + Logger.TokenValidationFailed(ex); // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event. - if (Options.RefreshOnIssuerKeyNotFound && ex.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException))) + if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null + && ex is SecurityTokenSignatureKeyNotFoundException) { Options.ConfigurationManager.RequestRefresh(); } @@ -118,43 +129,42 @@ protected override async Task HandleAuthenticateAsync() Logger.TokenValidationSucceeded(); - var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), Options.AuthenticationScheme); - var tokenValidatedContext = new TokenValidatedContext(Context, Options) + var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options) { - Ticket = ticket, - SecurityToken = validatedToken, + Principal = principal, + SecurityToken = validatedToken }; - await Options.Events.TokenValidated(tokenValidatedContext); - if (tokenValidatedContext.CheckEventResult(out result)) + await Events.TokenValidated(tokenValidatedContext); + if (tokenValidatedContext.Result != null) { - return result; + return tokenValidatedContext.Result; } - ticket = tokenValidatedContext.Ticket; if (Options.SaveToken) { - ticket.Properties.StoreTokens(new[] + tokenValidatedContext.Properties.StoreTokens(new[] { new AuthenticationToken { Name = "access_token", Value = token } }); } - return AuthenticateResult.Success(ticket); + tokenValidatedContext.Success(); + return tokenValidatedContext.Result; } } if (validationFailures != null) { - var authenticationFailedContext = new AuthenticationFailedContext(Context, Options) + var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options) { Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures) }; - await Options.Events.AuthenticationFailed(authenticationFailedContext); - if (authenticationFailedContext.CheckEventResult(out result)) + await Events.AuthenticationFailed(authenticationFailedContext); + if (authenticationFailedContext.Result != null) { - return result; + return authenticationFailedContext.Result; } return AuthenticateResult.Fail(authenticationFailedContext.Exception); @@ -166,48 +176,142 @@ protected override async Task HandleAuthenticateAsync() { Logger.ErrorProcessingMessage(ex); - var authenticationFailedContext = new AuthenticationFailedContext(Context, Options) + var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options) { Exception = ex }; - await Options.Events.AuthenticationFailed(authenticationFailedContext); - if (authenticationFailedContext.CheckEventResult(out result)) + await Events.AuthenticationFailed(authenticationFailedContext); + if (authenticationFailedContext.Result != null) { - return result; + return authenticationFailedContext.Result; } throw; } } - protected override async Task HandleUnauthorizedAsync(ChallengeContext context) + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { - var eventContext = new JwtBearerChallengeContext(Context, Options, new AuthenticationProperties(context.Properties)); - await Options.Events.Challenge(eventContext); - if (eventContext.HandledResponse) + var authResult = await HandleAuthenticateOnceSafeAsync(); + var eventContext = new JwtBearerChallengeContext(Context, Scheme, Options, properties) { - return true; + AuthenticateFailure = authResult?.Failure + }; + + // Avoid returning error=invalid_token if the error is not caused by an authentication failure (e.g missing token). + if (Options.IncludeErrorDetails && eventContext.AuthenticateFailure != null) + { + eventContext.Error = "invalid_token"; + eventContext.ErrorDescription = CreateErrorDescription(eventContext.AuthenticateFailure); } - if (eventContext.Skipped) + + await Events.Challenge(eventContext); + if (eventContext.Handled) { - return false; + return; } Response.StatusCode = 401; - Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge); - return false; - } + if (string.IsNullOrEmpty(eventContext.Error) && + string.IsNullOrEmpty(eventContext.ErrorDescription) && + string.IsNullOrEmpty(eventContext.ErrorUri)) + { + Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge); + } + else + { + // https://tools.ietf.org/html/rfc6750#section-3.1 + // WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired" + var builder = new StringBuilder(Options.Challenge); + if (Options.Challenge.IndexOf(" ", StringComparison.Ordinal) > 0) + { + // Only add a comma after the first param, if any + builder.Append(','); + } + if (!string.IsNullOrEmpty(eventContext.Error)) + { + builder.Append(" error=\""); + builder.Append(eventContext.Error); + builder.Append("\""); + } + if (!string.IsNullOrEmpty(eventContext.ErrorDescription)) + { + if (!string.IsNullOrEmpty(eventContext.Error)) + { + builder.Append(","); + } - protected override Task HandleSignOutAsync(SignOutContext context) - { - throw new NotSupportedException(); + builder.Append(" error_description=\""); + builder.Append(eventContext.ErrorDescription); + builder.Append('\"'); + } + if (!string.IsNullOrEmpty(eventContext.ErrorUri)) + { + if (!string.IsNullOrEmpty(eventContext.Error) || + !string.IsNullOrEmpty(eventContext.ErrorDescription)) + { + builder.Append(","); + } + + builder.Append(" error_uri=\""); + builder.Append(eventContext.ErrorUri); + builder.Append('\"'); + } + + Response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString()); + } } - protected override Task HandleSignInAsync(SignInContext context) + private static string CreateErrorDescription(Exception authFailure) { - throw new NotSupportedException(); + IEnumerable exceptions; + if (authFailure is AggregateException agEx) + { + exceptions = agEx.InnerExceptions; + } + else + { + exceptions = new[] { authFailure }; + } + + var messages = new List(); + + foreach (var ex in exceptions) + { + // Order sensitive, some of these exceptions derive from others + // and we want to display the most specific message possible. + switch (ex) + { + case SecurityTokenInvalidAudienceException _: + messages.Add("The audience is invalid"); + break; + case SecurityTokenInvalidIssuerException _: + messages.Add("The issuer is invalid"); + break; + case SecurityTokenNoExpirationException _: + messages.Add("The token has no expiration"); + break; + case SecurityTokenInvalidLifetimeException _: + messages.Add("The token lifetime is invalid"); + break; + case SecurityTokenNotYetValidException _: + messages.Add("The token is not valid yet"); + break; + case SecurityTokenExpiredException _: + messages.Add("The token is expired"); + break; + case SecurityTokenSignatureKeyNotFoundException _: + messages.Add("The signature key was not found"); + break; + case SecurityTokenInvalidSignatureException _: + messages.Add("The signature is invalid"); + break; + } + } + + return string.Join("; ", messages); } } } diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerMiddleware.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerMiddleware.cs deleted file mode 100644 index bfb38793f..000000000 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerMiddleware.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Net.Http; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Protocols; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; - -namespace Microsoft.AspNetCore.Authentication.JwtBearer -{ - /// - /// Bearer authentication middleware component which is added to an HTTP pipeline. This class is not - /// created by application code directly, instead it is added by calling the the IAppBuilder UseJwtBearerAuthentication - /// extension method. - /// - public class JwtBearerMiddleware : AuthenticationMiddleware - { - /// - /// Bearer authentication component which is added to an HTTP pipeline. This constructor is not - /// called by application code directly, instead it is added by calling the the IAppBuilder UseJwtBearerAuthentication - /// extension method. - /// - public JwtBearerMiddleware( - RequestDelegate next, - ILoggerFactory loggerFactory, - UrlEncoder encoder, - IOptions options) - : base(next, options, loggerFactory, encoder) - { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (Options.Events == null) - { - Options.Events = new JwtBearerEvents(); - } - - if (string.IsNullOrEmpty(Options.TokenValidationParameters.ValidAudience) && !string.IsNullOrEmpty(Options.Audience)) - { - Options.TokenValidationParameters.ValidAudience = Options.Audience; - } - - if (Options.ConfigurationManager == null) - { - if (Options.Configuration != null) - { - Options.ConfigurationManager = new StaticConfigurationManager(Options.Configuration); - } - else if (!(string.IsNullOrEmpty(Options.MetadataAddress) && string.IsNullOrEmpty(Options.Authority))) - { - if (string.IsNullOrEmpty(Options.MetadataAddress) && !string.IsNullOrEmpty(Options.Authority)) - { - Options.MetadataAddress = Options.Authority; - if (!Options.MetadataAddress.EndsWith("/", StringComparison.Ordinal)) - { - Options.MetadataAddress += "/"; - } - - Options.MetadataAddress += ".well-known/openid-configuration"; - } - - if (Options.RequireHttpsMetadata && !Options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false."); - } - - var httpClient = new HttpClient(Options.BackchannelHttpHandler ?? new HttpClientHandler()); - httpClient.Timeout = Options.BackchannelTimeout; - httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB - - Options.ConfigurationManager = new ConfigurationManager(Options.MetadataAddress, new OpenIdConnectConfigurationRetriever(), - new HttpDocumentRetriever(httpClient) { RequireHttps = Options.RequireHttpsMetadata }); - } - } - } - - /// - /// Called by the AuthenticationMiddleware base class to create a per-request handler. - /// - /// A new instance of the request handler - protected override AuthenticationHandler CreateHandler() - { - return new JwtBearerHandler(); - } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs index 837928e77..0d0a88e24 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs @@ -3,32 +3,19 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.IdentityModel.Tokens.Jwt; using System.Net.Http; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Authentication.JwtBearer { /// - /// Options class provides information needed to control Bearer Authentication middleware behavior + /// Options class provides information needed to control Bearer Authentication handler behavior /// - public class JwtBearerOptions : AuthenticationOptions + public class JwtBearerOptions : AuthenticationSchemeOptions { - /// - /// Creates an instance of bearer authentication options with default values. - /// - public JwtBearerOptions() : base() - { - AuthenticationScheme = JwtBearerDefaults.AuthenticationScheme; - AutomaticAuthenticate = true; - AutomaticChallenge = true; - } - /// /// Gets or sets if HTTPS is required for the metadata address or authority. /// The default is true. This should be disabled only in development environments. @@ -59,11 +46,15 @@ public JwtBearerOptions() : base() public string Challenge { get; set; } = JwtBearerDefaults.AuthenticationScheme; /// - /// The object provided by the application to process events raised by the bearer authentication middleware. - /// The application may implement the interface fully, or it may create an instance of JwtBearerAuthenticationEvents + /// The object provided by the application to process events raised by the bearer authentication handler. + /// The application may implement the interface fully, or it may create an instance of JwtBearerEvents /// and assign delegates only to the events it wants to process. /// - public IJwtBearerEvents Events { get; set; } = new JwtBearerEvents(); + public new JwtBearerEvents Events + { + get { return (JwtBearerEvents)base.Events; } + set { base.Events = value; } + } /// /// The HttpMessageHandler used to retrieve metadata. @@ -95,12 +86,6 @@ public JwtBearerOptions() : base() /// public bool RefreshOnIssuerKeyNotFound { get; set; } = true; - /// - /// For testing purposes only. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public ISystemClock SystemClock { get; set; } = new SystemClock(); - /// /// Gets the ordered list of used to validate access tokens. /// @@ -118,5 +103,12 @@ public JwtBearerOptions() : base() /// after a successful authorization. /// public bool SaveToken { get; set; } = true; + + /// + /// Defines whether the token validation errors should be returned to the caller. + /// Enabled by default, this option can be disabled to prevent the JWT handler + /// from returning an error and an error_description in the WWW-Authenticate header. + /// + public bool IncludeErrorDetails { get; set; } = true; } } diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerPostConfigureOptions.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerPostConfigureOptions.cs new file mode 100644 index 000000000..8829bfac0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerPostConfigureOptions.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer +{ + /// + /// Used to setup defaults for all . + /// + public class JwtBearerPostConfigureOptions : IPostConfigureOptions + { + /// + /// Invoked to post configure a JwtBearerOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configure. + public void PostConfigure(string name, JwtBearerOptions options) + { + if (string.IsNullOrEmpty(options.TokenValidationParameters.ValidAudience) && !string.IsNullOrEmpty(options.Audience)) + { + options.TokenValidationParameters.ValidAudience = options.Audience; + } + + if (options.ConfigurationManager == null) + { + if (options.Configuration != null) + { + options.ConfigurationManager = new StaticConfigurationManager(options.Configuration); + } + else if (!(string.IsNullOrEmpty(options.MetadataAddress) && string.IsNullOrEmpty(options.Authority))) + { + if (string.IsNullOrEmpty(options.MetadataAddress) && !string.IsNullOrEmpty(options.Authority)) + { + options.MetadataAddress = options.Authority; + if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal)) + { + options.MetadataAddress += "/"; + } + + options.MetadataAddress += ".well-known/openid-configuration"; + } + + if (options.RequireHttpsMetadata && !options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false."); + } + + var httpClient = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler()); + httpClient.Timeout = options.BackchannelTimeout; + httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + + options.ConfigurationManager = new ConfigurationManager(options.MetadataAddress, new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever(httpClient) { RequireHttps = options.RequireHttpsMetadata }); + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/LoggingExtensions.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/LoggingExtensions.cs index 643da9290..5c6ca088a 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/LoggingExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/LoggingExtensions.cs @@ -7,17 +7,17 @@ namespace Microsoft.Extensions.Logging { internal static class LoggingExtensions { - private static Action _tokenValidationFailed; - private static Action _tokenValidationSucceeded; + private static Action _tokenValidationFailed; + private static Action _tokenValidationSucceeded; private static Action _errorProcessingMessage; static LoggingExtensions() { - _tokenValidationFailed = LoggerMessage.Define( + _tokenValidationFailed = LoggerMessage.Define( eventId: 1, logLevel: LogLevel.Information, - formatString: "Failed to validate the token {Token}."); - _tokenValidationSucceeded = LoggerMessage.Define( + formatString: "Failed to validate the token."); + _tokenValidationSucceeded = LoggerMessage.Define( eventId: 2, logLevel: LogLevel.Information, formatString: "Successfully validated the token."); @@ -27,19 +27,13 @@ static LoggingExtensions() formatString: "Exception occurred while processing message."); } - public static void TokenValidationFailed(this ILogger logger, string token, Exception ex) - { - _tokenValidationFailed(logger, token, ex); - } + public static void TokenValidationFailed(this ILogger logger, Exception ex) + => _tokenValidationFailed(logger, ex); public static void TokenValidationSucceeded(this ILogger logger) - { - _tokenValidationSucceeded(logger, null, null); - } + => _tokenValidationSucceeded(logger, null); public static void ErrorProcessingMessage(this ILogger logger, Exception ex) - { - _errorProcessingMessage(logger, ex); - } + => _errorProcessingMessage(logger, ex); } } diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj new file mode 100644 index 000000000..8a26c58f3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj @@ -0,0 +1,19 @@ + + + + ASP.NET Core middleware that enables an application to receive an OpenID Connect bearer token. + netcoreapp3.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.xproj b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.xproj deleted file mode 100644 index 8d87a04b7..000000000 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.xproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 2755BFE5-7421-4A31-A644-F817DF5CAA98 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/AssemblyInfo.cs deleted file mode 100644 index 76feceeff..000000000 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Reflection; -using System.Resources; - -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: AssemblyCompany("Microsoft Corporation.")] -[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/Resources.Designer.cs index ef1d784f2..e95b8e061 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Properties/Resources.Designer.cs @@ -15,32 +15,28 @@ private static readonly ResourceManager _resourceManager /// internal static string Exception_OptionMustBeProvided { - get { return GetString("Exception_OptionMustBeProvided"); } + get => GetString("Exception_OptionMustBeProvided"); } /// /// The '{0}' option must be provided. /// internal static string FormatException_OptionMustBeProvided(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); - } + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); /// /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. /// internal static string Exception_ValidatorHandlerMismatch { - get { return GetString("Exception_ValidatorHandlerMismatch"); } + get => GetString("Exception_ValidatorHandlerMismatch"); } /// /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. /// internal static string FormatException_ValidatorHandlerMismatch() - { - return GetString("Exception_ValidatorHandlerMismatch"); - } + => GetString("Exception_ValidatorHandlerMismatch"); private static string GetString(string name, params string[] formatterNames) { diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json new file mode 100644 index 000000000..d3839022b --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json @@ -0,0 +1,1064 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.JwtBearer, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.JwtBearerExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddJwtBearer", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddJwtBearer", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddJwtBearer", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddJwtBearer", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.JwtBearerAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseJwtBearerAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseJwtBearerAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.AuthenticationFailedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Exception", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Exception", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerChallengeContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AuthenticateFailure", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthenticateFailure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Error", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Error", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ErrorDescription", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ErrorDescription", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ErrorUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ErrorUri", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Handled", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleResponse", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnAuthenticationFailed", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnAuthenticationFailed", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnMessageReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnMessageReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnTokenValidated", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnTokenValidated", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnChallenge", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnChallenge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticationFailed", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.AuthenticationFailedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MessageReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.MessageReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TokenValidated", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Challenge", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerChallengeContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.MessageReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Token", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Token", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_SecurityToken", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Tokens.SecurityToken", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SecurityToken", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Tokens.SecurityToken" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Bearer\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "dataProtection", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequireHttpsMetadata", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequireHttpsMetadata", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MetadataAddress", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MetadataAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Authority", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Authority", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Audience", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Audience", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Challenge", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Challenge", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BackchannelHttpHandler", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpMessageHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BackchannelHttpHandler", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.Http.HttpMessageHandler" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BackchannelTimeout", + "Parameters": [], + "ReturnType": "System.TimeSpan", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BackchannelTimeout", + "Parameters": [ + { + "Name": "value", + "Type": "System.TimeSpan" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Configuration", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Configuration", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConfigurationManager", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.IConfigurationManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConfigurationManager", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.IConfigurationManager" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RefreshOnIssuerKeyNotFound", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RefreshOnIssuerKeyNotFound", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SecurityTokenValidators", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenValidationParameters", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Tokens.TokenValidationParameters", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenValidationParameters", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Tokens.TokenValidationParameters" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SaveToken", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SaveToken", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IncludeErrorDetails", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IncludeErrorDetails", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Options.IPostConfigureOptions" + ], + "Members": [ + { + "Kind": "Method", + "Name": "PostConfigure", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/breakingchanges.netcore.json b/src/Microsoft.AspNetCore.Authentication.JwtBearer/breakingchanges.netcore.json new file mode 100644 index 000000000..d17003a80 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/breakingchanges.netcore.json @@ -0,0 +1,7 @@ + [ + { + "TypeId": "public class Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler : Microsoft.AspNetCore.Authentication.AuthenticationHandler", + "MemberId": "public .ctor(Microsoft.Extensions.Options.IOptionsMonitor options, Microsoft.Extensions.Logging.ILoggerFactory logger, System.Text.Encodings.Web.UrlEncoder encoder, Microsoft.AspNetCore.DataProtection.IDataProtectionProvider dataProtection, Microsoft.AspNetCore.Authentication.ISystemClock clock)", + "Kind": "Removal" + } + ] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/project.json b/src/Microsoft.AspNetCore.Authentication.JwtBearer/project.json deleted file mode 100644 index 204ad1ace..000000000 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/project.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "ASP.NET Core middleware that enables an application to receive an OpenID Connect bearer token.", - "tags": [ - "aspnetcore", - "authentication", - "security" - ], - "repository": { - "type": "git", - "url": "git://github.com/aspnet/security" - }, - "compilationOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk", - "nowarn": [ - "CS1591" - ], - "xmlDoc": true - }, - "dependencies": { - "Microsoft.AspNetCore.Authentication": "1.0.0-*", - "Microsoft.IdentityModel.Protocols.OpenIdConnect": "2.0.0-*" - }, - "frameworks": { - "net451": {}, - "netstandard1.4": { - "imports": [ - "portable-net451+win8" - ] - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj new file mode 100644 index 000000000..1841157c9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj @@ -0,0 +1,15 @@ + + + + ASP.NET Core middleware that enables an application to support the Microsoft Account authentication workflow. + netcoreapp3.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.xproj b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.xproj deleted file mode 100644 index 52dd6cb67..000000000 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.xproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - acb45e19-f520-4d0c-8916-b0ceb9c017fe - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs index 660dd2e81..7fd71d7a9 100644 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs @@ -1,9 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using Microsoft.AspNetCore.Authentication.MicrosoftAccount; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { @@ -13,38 +12,26 @@ namespace Microsoft.AspNetCore.Builder public static class MicrosoftAccountAppBuilderExtensions { /// - /// Adds the middleware to the specified , which enables Microsoft Account authentication capabilities. + /// UseMicrosoftAccountAuthentication is obsolete. Configure MicrosoftAccount authentication with AddAuthentication().AddMicrosoftAccount in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. + /// The to add the handler to. /// A reference to this instance after the operation has completed. + [Obsolete("UseMicrosoftAccountAuthentication is obsolete. Configure MicrosoftAccount authentication with AddAuthentication().AddMicrosoftAccount in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseMicrosoftAccountAuthentication(this IApplicationBuilder app) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware(); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } /// - /// Adds the middleware to the specified , which enables Microsoft Account authentication capabilities. + /// UseMicrosoftAccountAuthentication is obsolete. Configure MicrosoftAccount authentication with AddAuthentication().AddMicrosoftAccount in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. - /// A that specifies options for the middleware. + /// The to add the handler to. + /// A that specifies options for the handler. /// A reference to this instance after the operation has completed. + [Obsolete("UseMicrosoftAccountAuthentication is obsolete. Configure MicrosoftAccount authentication with AddAuthentication().AddMicrosoftAccount in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseMicrosoftAccountAuthentication(this IApplicationBuilder app, MicrosoftAccountOptions options) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - return app.UseMiddleware(Options.Create(options)); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs index 0d272f979..0421fa14b 100644 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs +++ b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs @@ -7,6 +7,9 @@ public static class MicrosoftAccountDefaults { public const string AuthenticationScheme = "Microsoft"; + public static readonly string DisplayName = "Microsoft"; + + // https://developer.microsoft.com/en-us/graph/docs/concepts/auth_v2_user public static readonly string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; public static readonly string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountExtensions.cs b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountExtensions.cs new file mode 100644 index 000000000..7f24e5af7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.MicrosoftAccount; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class MicrosoftAccountExtensions + { + public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder) + => builder.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, _ => { }); + + public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddMicrosoftAccount(authenticationScheme, MicrosoftAccountDefaults.DisplayName, configureOptions); + + public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + => builder.AddOAuth(authenticationScheme, displayName, configureOptions); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs index 8b9177625..bba547277 100644 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs @@ -4,20 +4,20 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; +using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.OAuth; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount { - internal class MicrosoftAccountHandler : OAuthHandler + public class MicrosoftAccountHandler : OAuthHandler { - public MicrosoftAccountHandler(HttpClient httpClient) - : base(httpClient) - { - } + public MicrosoftAccountHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } protected override async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) { @@ -25,48 +25,18 @@ protected override async Task CreateTicketAsync(ClaimsIden request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); var response = await Backchannel.SendAsync(request, Context.RequestAborted); - response.EnsureSuccessStatusCode(); - - var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); - - var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), properties, Options.AuthenticationScheme); - var context = new OAuthCreatingTicketContext(ticket, Context, Options, Backchannel, tokens, payload); - var identifier = MicrosoftAccountHelper.GetId(payload); - if (!string.IsNullOrEmpty(identifier)) - { - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, identifier, ClaimValueTypes.String, Options.ClaimsIssuer)); - identity.AddClaim(new Claim("urn:microsoftaccount:id", identifier, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var name = MicrosoftAccountHelper.GetDisplayName(payload); - if (!string.IsNullOrEmpty(name)) + if (!response.IsSuccessStatusCode) { - identity.AddClaim(new Claim(ClaimTypes.Name, name, ClaimValueTypes.String, Options.ClaimsIssuer)); - identity.AddClaim(new Claim("urn:microsoftaccount:name", name, ClaimValueTypes.String, Options.ClaimsIssuer)); + throw new HttpRequestException($"An error occurred when retrieving Microsoft user information ({response.StatusCode}). Please check if the authentication information is correct and the corresponding Microsoft Account API is enabled."); } - var givenName = MicrosoftAccountHelper.GetGivenName(payload); - if (!string.IsNullOrEmpty(givenName)) - { - identity.AddClaim(new Claim(ClaimTypes.GivenName, givenName, ClaimValueTypes.String, Options.ClaimsIssuer)); - identity.AddClaim(new Claim("urn:microsoftaccount:givenname", givenName, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var surname = MicrosoftAccountHelper.GetSurname(payload); - if (!string.IsNullOrEmpty(surname)) - { - identity.AddClaim(new Claim(ClaimTypes.Surname, surname, ClaimValueTypes.String, Options.ClaimsIssuer)); - identity.AddClaim(new Claim("urn:microsoftaccount:surname", surname, ClaimValueTypes.String, Options.ClaimsIssuer)); - } + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); - var email = MicrosoftAccountHelper.GetEmail(payload); - if (!string.IsNullOrEmpty(email)) - { - identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, Options.ClaimsIssuer)); - } + var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload); + context.RunClaimActions(); - await Options.Events.CreatingTicket(context); - return context.Ticket; + await Events.CreatingTicket(context); + return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); } } } diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHelper.cs b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHelper.cs deleted file mode 100644 index cce8dcc73..000000000 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHelper.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Newtonsoft.Json.Linq; - -namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount -{ - /// - /// Contains static methods that allow to extract user's information from a - /// instance retrieved from Microsoft after a successful authentication process. - /// http://graph.microsoft.io/en-us/docs/api-reference/v1.0/resources/user - /// - public static class MicrosoftAccountHelper - { - /// - /// Gets the Microsoft Account user ID. - /// - public static string GetId(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("id"); - } - - /// - /// Gets the user's name. - /// - public static string GetDisplayName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("displayName"); - } - - /// - /// Gets the user's given name. - /// - public static string GetGivenName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("givenName"); - } - - /// - /// Gets the user's surname. - /// - public static string GetSurname(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("surname"); - } - - /// - /// Gets the user's email address. - /// - public static string GetEmail(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("mail") ?? user.Value("userPrincipalName"); - } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountMiddleware.cs b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountMiddleware.cs deleted file mode 100644 index 3ad1bf557..000000000 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountMiddleware.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Authentication.OAuth; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount -{ - /// - /// An ASP.NET Core middleware for authenticating users using the Microsoft Account service. - /// - public class MicrosoftAccountMiddleware : OAuthMiddleware - { - /// - /// Initializes a new . - /// - /// The next middleware in the HTTP pipeline to invoke. - /// - /// - /// - /// - /// Configuration options for the middleware. - public MicrosoftAccountMiddleware( - RequestDelegate next, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - UrlEncoder encoder, - IOptions sharedOptions, - IOptions options) - : base(next, dataProtectionProvider, loggerFactory, encoder, sharedOptions, options) - { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - - if (dataProtectionProvider == null) - { - throw new ArgumentNullException(nameof(dataProtectionProvider)); - } - - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - if (sharedOptions == null) - { - throw new ArgumentNullException(nameof(sharedOptions)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - } - - /// - /// Provides the object for processing authentication-related requests. - /// - /// An configured with the supplied to the constructor. - protected override AuthenticationHandler CreateHandler() - { - return new MicrosoftAccountHandler(Backchannel); - } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs index 625d4baf9..dbca3507e 100644 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs @@ -1,13 +1,16 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Http; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.MicrosoftAccount; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authentication.OAuth; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount { /// - /// Configuration options for . + /// Configuration options for . /// public class MicrosoftAccountOptions : OAuthOptions { @@ -16,13 +19,17 @@ public class MicrosoftAccountOptions : OAuthOptions /// public MicrosoftAccountOptions() { - AuthenticationScheme = MicrosoftAccountDefaults.AuthenticationScheme; - DisplayName = AuthenticationScheme; CallbackPath = new PathString("/signin-microsoft"); AuthorizationEndpoint = MicrosoftAccountDefaults.AuthorizationEndpoint; TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint; UserInformationEndpoint = MicrosoftAccountDefaults.UserInformationEndpoint; Scope.Add("https://graph.microsoft.com/user.read"); + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName"); + ClaimActions.MapJsonKey(ClaimTypes.GivenName, "givenName"); + ClaimActions.MapJsonKey(ClaimTypes.Surname, "surname"); + ClaimActions.MapCustomJson(ClaimTypes.Email, user => user.Value("mail") ?? user.Value("userPrincipalName")); } } } diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/AssemblyInfo.cs deleted file mode 100644 index 76feceeff..000000000 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Reflection; -using System.Resources; - -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: AssemblyCompany("Microsoft Corporation.")] -[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/Resources.Designer.cs index 618d143ee..7ef5acecb 100644 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Properties/Resources.Designer.cs @@ -15,48 +15,42 @@ private static readonly ResourceManager _resourceManager /// internal static string Exception_MissingId { - get { return GetString("Exception_MissingId"); } + get => GetString("Exception_MissingId"); } /// /// The user does not have an id. /// internal static string FormatException_MissingId() - { - return GetString("Exception_MissingId"); - } + => GetString("Exception_MissingId"); /// /// The '{0}' option must be provided. /// internal static string Exception_OptionMustBeProvided { - get { return GetString("Exception_OptionMustBeProvided"); } + get => GetString("Exception_OptionMustBeProvided"); } /// /// The '{0}' option must be provided. /// internal static string FormatException_OptionMustBeProvided(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); - } + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); /// /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. /// internal static string Exception_ValidatorHandlerMismatch { - get { return GetString("Exception_ValidatorHandlerMismatch"); } + get => GetString("Exception_ValidatorHandlerMismatch"); } /// /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. /// internal static string FormatException_ValidatorHandlerMismatch() - { - return GetString("Exception_ValidatorHandlerMismatch"); - } + => GetString("Exception_ValidatorHandlerMismatch"); private static string GetString(string name, params string[] formatterNames) { diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/baseline.netcore.json new file mode 100644 index 000000000..877e9035a --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/baseline.netcore.json @@ -0,0 +1,284 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.MicrosoftAccount, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.MicrosoftAccountExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddMicrosoftAccount", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddMicrosoftAccount", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddMicrosoftAccount", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddMicrosoftAccount", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthorizationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "TokenEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "UserInformationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Microsoft\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CreateTicketAsync", + "Parameters": [ + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokens", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.MicrosoftAccountAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseMicrosoftAccountAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseMicrosoftAccountAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.MicrosoftAccount.MicrosoftAccountOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/project.json b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/project.json deleted file mode 100644 index be1c9099e..000000000 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/project.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "ASP.NET Core middleware that enables an application to support the Microsoft Account authentication workflow.", - "tags": [ - "aspnetcore", - "authentication", - "security" - ], - "repository": { - "type": "git", - "url": "git://github.com/aspnet/security" - }, - "compilationOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk", - "nowarn": [ - "CS1591" - ], - "xmlDoc": true - }, - "dependencies": { - "Microsoft.AspNetCore.Authentication.OAuth": "1.0.0-*" - }, - "frameworks": { - "net451": {}, - "netstandard1.3": { - "imports": [ - "portable-net451+win8" - ] - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimAction.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimAction.cs new file mode 100644 index 000000000..78b63bb38 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimAction.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// Infrastructure for mapping user data from a json structure to claims on the ClaimsIdentity. + /// + public abstract class ClaimAction + { + /// + /// Create a new claim manipulation action. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + public ClaimAction(string claimType, string valueType) + { + ClaimType = claimType; + ValueType = valueType; + } + + /// + /// The value to use for Claim.Type when creating a Claim. + /// + public string ClaimType { get; } + + // The value to use for Claim.ValueType when creating a Claim. + public string ValueType { get; } + + /// + /// Examine the given userData json, determine if the requisite data is present, and optionally add it + /// as a new Claim on the ClaimsIdentity. + /// + /// The source data to examine. This value may be null. + /// The identity to add Claims to. + /// The value to use for Claim.Issuer when creating a Claim. + public abstract void Run(JObject userData, ClaimsIdentity identity, string issuer); + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollection.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollection.cs new file mode 100644 index 000000000..63da155d7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollection.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A collection of ClaimActions used when mapping user data to Claims. + /// + public class ClaimActionCollection : IEnumerable + { + private IList Actions { get; } = new List(); + + /// + /// Remove all claim actions. + /// + public void Clear() => Actions.Clear(); + + /// + /// Remove all claim actions for the given ClaimType. + /// + /// The ClaimType of maps to remove. + public void Remove(string claimType) + { + var itemsToRemove = Actions.Where(map => string.Equals(claimType, map.ClaimType, StringComparison.OrdinalIgnoreCase)).ToList(); + itemsToRemove.ForEach(map => Actions.Remove(map)); + } + + /// + /// Add a claim action to the collection. + /// + /// The claim action to add. + public void Add(ClaimAction action) + { + Actions.Add(action); + } + + public IEnumerator GetEnumerator() + { + return Actions.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return Actions.GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs new file mode 100644 index 000000000..5a178957a --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs @@ -0,0 +1,139 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication +{ + public static class ClaimActionCollectionMapExtensions + { + /// + /// Select a top level value from the json user data with the given key name and add it as a Claim. + /// This no-ops if the key is not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + public static void MapJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey) + { + collection.MapJsonKey(claimType, jsonKey, ClaimValueTypes.String); + } + + /// + /// Select a top level value from the json user data with the given key name and add it as a Claim. + /// This no-ops if the key is not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + /// The value to use for Claim.ValueType when creating a Claim. + public static void MapJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey, string valueType) + { + collection.Add(new JsonKeyClaimAction(claimType, valueType, jsonKey)); + } + + /// + /// Select a second level value from the json user data with the given top level key name and second level sub key name and add it as a Claim. + /// This no-ops if the keys are not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + /// The second level key to look for in the json user data. + public static void MapJsonSubKey(this ClaimActionCollection collection, string claimType, string jsonKey, string subKey) + { + collection.MapJsonSubKey(claimType, jsonKey, subKey, ClaimValueTypes.String); + } + + /// + /// Select a second level value from the json user data with the given top level key name and second level sub key name and add it as a Claim. + /// This no-ops if the keys are not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + /// The second level key to look for in the json user data. + /// The value to use for Claim.ValueType when creating a Claim. + public static void MapJsonSubKey(this ClaimActionCollection collection, string claimType, string jsonKey, string subKey, string valueType) + { + collection.Add(new JsonSubKeyClaimAction(claimType, valueType, jsonKey, subKey)); + } + + /// + /// Run the given resolver to select a value from the json user data to add as a claim. + /// This no-ops if the returned value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The Func that will be called to select value from the given json user data. + public static void MapCustomJson(this ClaimActionCollection collection, string claimType, Func resolver) + { + collection.MapCustomJson(claimType, ClaimValueTypes.String, resolver); + } + + /// + /// Run the given resolver to select a value from the json user data to add as a claim. + /// This no-ops if the returned value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The Func that will be called to select value from the given json user data. + public static void MapCustomJson(this ClaimActionCollection collection, string claimType, string valueType, Func resolver) + { + collection.Add(new CustomJsonClaimAction(claimType, valueType, resolver)); + } + + /// + /// Clears any current ClaimsActions and maps all values from the json user data as claims, excluding duplicates. + /// + /// + public static void MapAll(this ClaimActionCollection collection) + { + collection.Clear(); + collection.Add(new MapAllClaimsAction()); + } + + /// + /// Clears any current ClaimsActions and maps all values from the json user data as claims, excluding the specified types. + /// + /// + /// + public static void MapAllExcept(this ClaimActionCollection collection, params string[] exclusions) + { + collection.MapAll(); + collection.DeleteClaims(exclusions); + } + + /// + /// Delete all claims from the given ClaimsIdentity with the given ClaimType. + /// + /// + /// + public static void DeleteClaim(this ClaimActionCollection collection, string claimType) + { + collection.Add(new DeleteClaimAction(claimType)); + } + + /// + /// Delete all claims from the ClaimsIdentity with the given claimTypes. + /// + /// + /// + public static void DeleteClaims(this ClaimActionCollection collection, params string[] claimTypes) + { + if (claimTypes == null) + { + throw new ArgumentNullException(nameof(claimTypes)); + } + + foreach (var claimType in claimTypes) + { + collection.Add(new DeleteClaimAction(claimType)); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/CustomJsonClaimAction.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/CustomJsonClaimAction.cs new file mode 100644 index 000000000..21a4f70e1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/CustomJsonClaimAction.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that selects the value from the json user data by running the given Func resolver. + /// + public class CustomJsonClaimAction : ClaimAction + { + /// + /// Creates a new CustomJsonClaimAction. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The Func that will be called to select value from the given json user data. + public CustomJsonClaimAction(string claimType, string valueType, Func resolver) + : base(claimType, valueType) + { + Resolver = resolver; + } + + /// + /// The Func that will be called to select value from the given json user data. + /// + public Func Resolver { get; } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + if (userData == null) + { + return; + } + var value = Resolver(userData); + if (!string.IsNullOrEmpty(value)) + { + identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer)); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/DeleteClaimAction.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/DeleteClaimAction.cs new file mode 100644 index 000000000..75167cabc --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/DeleteClaimAction.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that deletes all claims from the given ClaimsIdentity with the given ClaimType. + /// + public class DeleteClaimAction : ClaimAction + { + /// + /// Creates a new DeleteClaimAction. + /// + /// The ClaimType of Claims to delete. + public DeleteClaimAction(string claimType) + : base(claimType, ClaimValueTypes.String) + { + } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + foreach (var claim in identity.FindAll(ClaimType).ToList()) + { + identity.TryRemoveClaim(claim); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs new file mode 100644 index 000000000..ccd1a965d --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that selects a top level value from the json user data with the given key name and adds it as a Claim. + /// This no-ops if the key is not found or the value is empty. + /// + public class JsonKeyClaimAction : ClaimAction + { + /// + /// Creates a new JsonKeyClaimAction. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The top level key to look for in the json user data. + public JsonKeyClaimAction(string claimType, string valueType, string jsonKey) + : base(claimType, valueType) + { + JsonKey = jsonKey; + } + + /// + /// The top level key to look for in the json user data. + /// + public string JsonKey { get; } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + var value = userData?[JsonKey]; + if (value is JValue) + { + AddClaim(value?.ToString(), identity, issuer); + } + else if (value is JArray) + { + foreach (var v in value) + { + AddClaim(v?.ToString(), identity, issuer); + } + } + } + + private void AddClaim(string value, ClaimsIdentity identity, string issuer) + { + if (!string.IsNullOrEmpty(value)) + { + identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer)); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonSubKeyClaimAction.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonSubKeyClaimAction.cs new file mode 100644 index 000000000..bc29672d0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonSubKeyClaimAction.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Newtonsoft.Json.Linq; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that selects a second level value from the json user data with the given top level key + /// name and second level sub key name and add it as a Claim. + /// This no-ops if the keys are not found or the value is empty. + /// + public class JsonSubKeyClaimAction : JsonKeyClaimAction + { + /// + /// Creates a new JsonSubKeyClaimAction. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The top level key to look for in the json user data. + /// The second level key to look for in the json user data. + public JsonSubKeyClaimAction(string claimType, string valueType, string jsonKey, string subKey) + : base(claimType, valueType, jsonKey) + { + SubKey = subKey; + } + + /// + /// The second level key to look for in the json user data. + /// + public string SubKey { get; } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + var value = GetValue(userData, JsonKey, SubKey); + if (!string.IsNullOrEmpty(value)) + { + identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer)); + } + } + + // Get the given subProperty from a property. + private static string GetValue(JObject userData, string propertyName, string subProperty) + { + if (userData != null && userData.TryGetValue(propertyName, out var value)) + { + var subObject = JObject.Parse(value.ToString()); + if (subObject != null && subObject.TryGetValue(subProperty, out value)) + { + return value.ToString(); + } + } + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/MapAllClaimsAction.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/MapAllClaimsAction.cs new file mode 100644 index 000000000..b3bf5d99f --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/MapAllClaimsAction.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that selects all top level values from the json user data and adds them as Claims. + /// This excludes duplicate sets of names and values. + /// + public class MapAllClaimsAction : ClaimAction + { + public MapAllClaimsAction() : base("All", ClaimValueTypes.String) + { + } + + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + if (userData == null) + { + return; + } + foreach (var pair in userData) + { + var claimValue = userData.TryGetValue(pair.Key, out var value) ? value.ToString() : null; + + // Avoid adding a claim if there's a duplicate name and value. This often happens in OIDC when claims are + // retrieved both from the id_token and from the user-info endpoint. + var duplicate = identity.FindFirst(c => string.Equals(c.Type, pair.Key, StringComparison.OrdinalIgnoreCase) + && string.Equals(c.Value, claimValue, StringComparison.Ordinal)) != null; + + if (!duplicate) + { + identity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, issuer)); + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Events/IOAuthEvents.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Events/IOAuthEvents.cs deleted file mode 100644 index 29316732c..000000000 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/Events/IOAuthEvents.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Authentication.OAuth -{ - /// - /// Specifies callback methods which the invokes to enable developer control over the authentication process. - /// - public interface IOAuthEvents : IRemoteAuthenticationEvents - { - /// - /// Invoked after the provider successfully authenticates a user. This can be used to retrieve user information. - /// This event may not be invoked by sub-classes of OAuthAuthenticationHandler if they override CreateTicketAsync. - /// - /// Contains information about the login session. - /// A representing the completed operation. - Task CreatingTicket(OAuthCreatingTicketContext context); - - /// - /// Called when a Challenge causes a redirect to the authorize endpoint. - /// - /// Contains redirect URI and of the challenge. - Task RedirectToAuthorizationEndpoint(OAuthRedirectToAuthorizationContext context); - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs index 15f9c91c7..f660dd224 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.Net.Http; using System.Security.Claims; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Newtonsoft.Json.Linq; @@ -14,54 +13,51 @@ namespace Microsoft.AspNetCore.Authentication.OAuth /// /// Contains information about the login session as well as the user . /// - public class OAuthCreatingTicketContext : BaseContext + public class OAuthCreatingTicketContext : ResultContext { /// /// Initializes a new . /// - /// The . + /// The . + /// The . /// The HTTP environment. + /// The authentication scheme. /// The options used by the authentication middleware. /// The HTTP client used by the authentication middleware /// The tokens returned from the token endpoint. public OAuthCreatingTicketContext( - AuthenticationTicket ticket, + ClaimsPrincipal principal, + AuthenticationProperties properties, HttpContext context, + AuthenticationScheme scheme, OAuthOptions options, HttpClient backchannel, OAuthTokenResponse tokens) - : this(ticket, context, options, backchannel, tokens, user: new JObject()) - { - } + : this(principal, properties, context, scheme, options, backchannel, tokens, user: new JObject()) + { } /// /// Initializes a new . /// - /// The . + /// The . + /// The . /// The HTTP environment. + /// The authentication scheme. /// The options used by the authentication middleware. /// The HTTP client used by the authentication middleware /// The tokens returned from the token endpoint. /// The JSON-serialized user. public OAuthCreatingTicketContext( - AuthenticationTicket ticket, + ClaimsPrincipal principal, + AuthenticationProperties properties, HttpContext context, + AuthenticationScheme scheme, OAuthOptions options, HttpClient backchannel, OAuthTokenResponse tokens, JObject user) - : base(context) + : base(context, scheme, options) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - if (backchannel == null) { throw new ArgumentNullException(nameof(backchannel)); @@ -80,12 +76,10 @@ public OAuthCreatingTicketContext( TokenResponse = tokens; Backchannel = backchannel; User = user; - Options = options; - Ticket = ticket; + Principal = principal; + Properties = properties; } - public OAuthOptions Options { get; } - /// /// Gets the JSON-serialized user or an empty /// if it is not available. @@ -135,14 +129,24 @@ public TimeSpan? ExpiresIn public HttpClient Backchannel { get; } /// - /// The that will be created. + /// Gets the main identity exposed by the authentication ticket. + /// This property returns null when the ticket is null. /// - public AuthenticationTicket Ticket { get; set; } + public ClaimsIdentity Identity => Principal?.Identity as ClaimsIdentity; - /// - /// Gets the main identity exposed by . - /// This property returns null when is null. - /// - public ClaimsIdentity Identity => Ticket?.Principal.Identity as ClaimsIdentity; + public void RunClaimActions() => RunClaimActions(User); + + public void RunClaimActions(JObject userData) + { + if (userData == null) + { + throw new ArgumentNullException(nameof(userData)); + } + + foreach (var action in Options.ClaimActions) + { + action.Run(userData, Identity, Options.ClaimsIssuer ?? Scheme.Name); + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthEvents.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthEvents.cs index 44c426051..9e194491b 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthEvents.cs +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthEvents.cs @@ -7,22 +7,22 @@ namespace Microsoft.AspNetCore.Authentication.OAuth { /// - /// Default implementation. + /// Default implementation. /// - public class OAuthEvents : RemoteAuthenticationEvents, IOAuthEvents + public class OAuthEvents : RemoteAuthenticationEvents { /// /// Gets or sets the function that is invoked when the CreatingTicket method is invoked. /// - public Func OnCreatingTicket { get; set; } = context => Task.FromResult(0); + public Func OnCreatingTicket { get; set; } = context => Task.CompletedTask; /// /// Gets or sets the delegate that is invoked when the RedirectToAuthorizationEndpoint method is invoked. /// - public Func OnRedirectToAuthorizationEndpoint { get; set; } = context => + public Func, Task> OnRedirectToAuthorizationEndpoint { get; set; } = context => { context.Response.Redirect(context.RedirectUri); - return Task.FromResult(0); + return Task.CompletedTask; }; /// @@ -33,9 +33,9 @@ public class OAuthEvents : RemoteAuthenticationEvents, IOAuthEvents public virtual Task CreatingTicket(OAuthCreatingTicketContext context) => OnCreatingTicket(context); /// - /// Called when a Challenge causes a redirect to authorize endpoint in the OAuth middleware. + /// Called when a Challenge causes a redirect to authorize endpoint in the OAuth handler. /// - /// Contains redirect URI and of the challenge. - public virtual Task RedirectToAuthorizationEndpoint(OAuthRedirectToAuthorizationContext context) => OnRedirectToAuthorizationEndpoint(context); + /// Contains redirect URI and of the challenge. + public virtual Task RedirectToAuthorizationEndpoint(RedirectContext context) => OnRedirectToAuthorizationEndpoint(context); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthRedirectToAuthorizationContext.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthRedirectToAuthorizationContext.cs deleted file mode 100644 index 63eaa3537..000000000 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthRedirectToAuthorizationContext.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; - -namespace Microsoft.AspNetCore.Authentication.OAuth -{ - /// - /// Context passed when a Challenge causes a redirect to authorize endpoint in the middleware. - /// - public class OAuthRedirectToAuthorizationContext : BaseContext - { - /// - /// Creates a new context object. - /// - /// The HTTP request context. - /// The . - /// The authentication properties of the challenge. - /// The initial redirect URI. - public OAuthRedirectToAuthorizationContext(HttpContext context, OAuthOptions options, AuthenticationProperties properties, string redirectUri) - : base(context) - { - RedirectUri = redirectUri; - Properties = properties; - Options = options; - } - - public OAuthOptions Options { get; } - - /// - /// Gets the URI used for the redirect operation. - /// - public string RedirectUri { get; private set; } - - /// - /// Gets the authentication properties of the challenge. - /// - public AuthenticationProperties Properties { get; private set; } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.csproj b/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.csproj new file mode 100644 index 000000000..97d4a9703 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.csproj @@ -0,0 +1,19 @@ + + + + ASP.NET Core middleware that enables an application to support any standard OAuth 2.0 authentication workflow. + netcoreapp3.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.xproj b/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.xproj deleted file mode 100644 index 34a397bd5..000000000 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.xproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 1657c79e-7755-4aee-9d61-571295b69a30 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthAppBuilderExtensions.cs index eebeaf7a3..d55f311f7 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthAppBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthAppBuilderExtensions.cs @@ -1,9 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using Microsoft.AspNetCore.Authentication.OAuth; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { @@ -13,38 +12,26 @@ namespace Microsoft.AspNetCore.Builder public static class OAuthAppBuilderExtensions { /// - /// Adds the middleware to the specified , which enables OAuth 2.0 authentication capabilities. + /// UseOAuthAuthentication is obsolete. Configure OAuth authentication with AddAuthentication().AddOAuth in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. + /// The to add the handler to. /// A reference to this instance after the operation has completed. + [Obsolete("UseOAuthAuthentication is obsolete. Configure OAuth authentication with AddAuthentication().AddOAuth in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseOAuthAuthentication(this IApplicationBuilder app) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware>(); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } /// - /// Adds the middleware to the specified , which enables OAuth 2.0 authentication capabilities. + /// UseOAuthAuthentication is obsolete. Configure OAuth authentication with AddAuthentication().AddOAuth in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. - /// A that specifies options for the middleware. + /// The to add the handler to. + /// A that specifies options for the handler. /// A reference to this instance after the operation has completed. + [Obsolete("UseOAuthAuthentication is obsolete. Configure OAuth authentication with AddAuthentication().AddOAuth in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseOAuthAuthentication(this IApplicationBuilder app, OAuthOptions options) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - return app.UseMiddleware>(Options.Create(options)); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthChallengeProperties.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthChallengeProperties.cs new file mode 100644 index 000000000..fc768a8ac --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthChallengeProperties.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Authentication.OAuth +{ + public class OAuthChallengeProperties : AuthenticationProperties + { + /// + /// The parameter key for the "scope" argument being used for a challenge request. + /// + public static readonly string ScopeKey = "scope"; + + public OAuthChallengeProperties() + { } + + public OAuthChallengeProperties(IDictionary items) + : base(items) + { } + + public OAuthChallengeProperties(IDictionary items, IDictionary parameters) + : base(items, parameters) + { } + + /// + /// The "scope" parameter value being used for a challenge request. + /// + public ICollection Scope + { + get => GetParameter>(ScopeKey); + set => SetParameter(ScopeKey, value); + } + + /// + /// Set the "scope" parameter value. + /// + /// List of scopes. + public virtual void SetScope(params string[] scopes) + { + Scope = scopes; + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthDefaults.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthDefaults.cs new file mode 100644 index 000000000..376f8ab01 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthDefaults.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.OAuth +{ + public static class OAuthDefaults + { + public static readonly string DisplayName = "OAuth"; + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthExtensions.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthExtensions.cs new file mode 100644 index 000000000..22c541a0a --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class OAuthExtensions + { + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddOAuth>(authenticationScheme, configureOptions); + + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + => builder.AddOAuth>(authenticationScheme, displayName, configureOptions); + + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + where TOptions : OAuthOptions, new() + where THandler : OAuthHandler + => builder.AddOAuth(authenticationScheme, OAuthDefaults.DisplayName, configureOptions); + + public static AuthenticationBuilder AddOAuth(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + where TOptions : OAuthOptions, new() + where THandler : OAuthHandler + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, OAuthPostConfigureOptions>()); + return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs index ddd26d9f0..05ba4df68 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs @@ -8,33 +8,71 @@ using System.Net.Http.Headers; using System.Security.Claims; using System.Text; +using System.Text.Encodings.Web; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.Authentication.OAuth { - public class OAuthHandler : RemoteAuthenticationHandler where TOptions : OAuthOptions + public class OAuthHandler : RemoteAuthenticationHandler where TOptions : OAuthOptions, new() { - public OAuthHandler(HttpClient backchannel) + protected HttpClient Backchannel => Options.Backchannel; + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new OAuthEvents Events { - Backchannel = backchannel; + get { return (OAuthEvents)base.Events; } + set { base.Events = value; } } - protected HttpClient Backchannel { get; private set; } + public OAuthHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } + + /// + /// Creates a new instance of the events instance. + /// + /// A new instance of the events instance. + protected override Task CreateEventsAsync() => Task.FromResult(new OAuthEvents()); - protected override async Task HandleRemoteAuthenticateAsync() + protected override async Task HandleRemoteAuthenticateAsync() { - AuthenticationProperties properties = null; var query = Request.Query; + var state = query["state"]; + var properties = Options.StateDataFormat.Unprotect(state); + + if (properties == null) + { + return HandleRequestResult.Fail("The oauth state was missing or invalid."); + } + + // OAuth2 10.12 CSRF + if (!ValidateCorrelationId(properties)) + { + return HandleRequestResult.Fail("Correlation failed.", properties); + } + var error = query["error"]; if (!StringValues.IsNullOrEmpty(error)) { + // Note: access_denied errors are special protocol errors indicating the user didn't + // approve the authorization demand requested by the remote authorization server. + // Since it's a frequent scenario (that is not caused by incorrect configuration), + // denied errors are handled differently using HandleAccessDeniedErrorAsync(). + // Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information. + if (StringValues.Equals(error, "access_denied")) + { + return await HandleAccessDeniedErrorAsync(properties); + } + var failureMessage = new StringBuilder(); failureMessage.Append(error); var errorDescription = query["error_description"]; @@ -48,42 +86,29 @@ protected override async Task HandleRemoteAuthenticateAsync( failureMessage.Append(";Uri=").Append(errorUri); } - return AuthenticateResult.Fail(failureMessage.ToString()); + return HandleRequestResult.Fail(failureMessage.ToString(), properties); } var code = query["code"]; - var state = query["state"]; - - properties = Options.StateDataFormat.Unprotect(state); - if (properties == null) - { - return AuthenticateResult.Fail("The oauth state was missing or invalid."); - } - - // OAuth2 10.12 CSRF - if (!ValidateCorrelationId(properties)) - { - return AuthenticateResult.Fail("Correlation failed."); - } if (StringValues.IsNullOrEmpty(code)) { - return AuthenticateResult.Fail("Code was not found."); + return HandleRequestResult.Fail("Code was not found.", properties); } var tokens = await ExchangeCodeAsync(code, BuildRedirectUri(Options.CallbackPath)); if (tokens.Error != null) { - return AuthenticateResult.Fail(tokens.Error); + return HandleRequestResult.Fail(tokens.Error, properties); } if (string.IsNullOrEmpty(tokens.AccessToken)) { - return AuthenticateResult.Fail("Failed to retrieve access token."); + return HandleRequestResult.Fail("Failed to retrieve access token.", properties); } - var identity = new ClaimsIdentity(Options.ClaimsIssuer); + var identity = new ClaimsIdentity(ClaimsIssuer); if (Options.SaveTokens) { @@ -107,7 +132,7 @@ protected override async Task HandleRemoteAuthenticateAsync( { // https://www.w3.org/TR/xmlschema-2/#dateTime // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx - var expiresAt = Options.SystemClock.UtcNow + TimeSpan.FromSeconds(value); + var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value); authTokens.Add(new AuthenticationToken { Name = "expires_at", @@ -119,7 +144,15 @@ protected override async Task HandleRemoteAuthenticateAsync( properties.StoreTokens(authTokens); } - return AuthenticateResult.Success(await CreateTicketAsync(identity, properties, tokens)); + var ticket = await CreateTicketAsync(identity, properties, tokens); + if (ticket != null) + { + return HandleRequestResult.Success(ticket); + } + else + { + return HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties); + } } protected virtual async Task ExchangeCodeAsync(string code, string redirectUri) @@ -162,47 +195,35 @@ private static async Task Display(HttpResponseMessage response) protected virtual async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) { - var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), properties, Options.AuthenticationScheme); - var context = new OAuthCreatingTicketContext(ticket, Context, Options, Backchannel, tokens); - await Options.Events.CreatingTicket(context); - return context.Ticket; + var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens); + await Events.CreatingTicket(context); + return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); } - protected override async Task HandleUnauthorizedAsync(ChallengeContext context) + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - var properties = new AuthenticationProperties(context.Properties) - { - ExpiresUtc = Options.SystemClock.UtcNow.Add(Options.RemoteAuthenticationTimeout) - }; - if (string.IsNullOrEmpty(properties.RedirectUri)) { - properties.RedirectUri = CurrentUri; + properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString; } // OAuth2 10.12 CSRF GenerateCorrelationId(properties); var authorizationEndpoint = BuildChallengeUrl(properties, BuildRedirectUri(Options.CallbackPath)); - var redirectContext = new OAuthRedirectToAuthorizationContext( - Context, Options, + var redirectContext = new RedirectContext( + Context, Scheme, Options, properties, authorizationEndpoint); - await Options.Events.RedirectToAuthorizationEndpoint(redirectContext); - return true; + await Events.RedirectToAuthorizationEndpoint(redirectContext); } protected virtual string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) { - var scope = FormatScope(); + var scopeParameter = properties.GetParameter>(OAuthChallengeProperties.ScopeKey); + var scope = scopeParameter != null ? FormatScope(scopeParameter) : FormatScope(); var state = Options.StateDataFormat.Protect(properties); - - var queryBuilder = new QueryBuilder() + var parameters = new Dictionary { { "client_id", Options.ClientId }, { "scope", scope }, @@ -210,13 +231,23 @@ protected virtual string BuildChallengeUrl(AuthenticationProperties properties, { "redirect_uri", redirectUri }, { "state", state }, }; - return Options.AuthorizationEndpoint + queryBuilder.ToString(); + return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters); } + /// + /// Format a list of OAuth scopes. + /// + /// List of scopes. + /// Formatted scopes. + protected virtual string FormatScope(IEnumerable scopes) + => string.Join(" ", scopes); // OAuth2 3.3 space separated + + /// + /// Format the property. + /// + /// Formatted scopes. + /// Subclasses should rather override . protected virtual string FormatScope() - { - // OAuth2 3.3 space separated - return string.Join(" ", Options.Scope); - } + => FormatScope(Options.Scope); } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthMiddleware.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthMiddleware.cs deleted file mode 100644 index 75139c1c8..000000000 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthMiddleware.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Globalization; -using System.Net.Http; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Authentication.OAuth -{ - /// - /// An ASP.NET Core middleware for authenticating users using OAuth services. - /// - public class OAuthMiddleware : AuthenticationMiddleware where TOptions : OAuthOptions, new() - { - /// - /// Initializes a new . - /// - /// The next middleware in the HTTP pipeline to invoke. - /// - /// - /// The . - /// The configuration options for this middleware. - /// Configuration options for the middleware. - public OAuthMiddleware( - RequestDelegate next, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - UrlEncoder encoder, - IOptions sharedOptions, - IOptions options) - : base(next, options, loggerFactory, encoder) - { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - - if (dataProtectionProvider == null) - { - throw new ArgumentNullException(nameof(dataProtectionProvider)); - } - - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - if (sharedOptions == null) - { - throw new ArgumentNullException(nameof(sharedOptions)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - // todo: review error handling - if (string.IsNullOrEmpty(Options.AuthenticationScheme)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.AuthenticationScheme))); - } - - if (string.IsNullOrEmpty(Options.ClientId)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.ClientId))); - } - - if (string.IsNullOrEmpty(Options.ClientSecret)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.ClientSecret))); - } - - if (string.IsNullOrEmpty(Options.AuthorizationEndpoint)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.AuthorizationEndpoint))); - } - - if (string.IsNullOrEmpty(Options.TokenEndpoint)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.TokenEndpoint))); - } - - if (!Options.CallbackPath.HasValue) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.CallbackPath))); - } - - if (Options.Events == null) - { - Options.Events = new OAuthEvents(); - } - - if (Options.StateDataFormat == null) - { - var dataProtector = dataProtectionProvider.CreateProtector( - GetType().FullName, Options.AuthenticationScheme, "v1"); - Options.StateDataFormat = new PropertiesDataFormat(dataProtector); - } - - Backchannel = new HttpClient(Options.BackchannelHttpHandler ?? new HttpClientHandler()); - Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OAuth middleware"); - Backchannel.Timeout = Options.BackchannelTimeout; - Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB - - if (string.IsNullOrEmpty(Options.SignInScheme)) - { - Options.SignInScheme = sharedOptions.Value.SignInScheme; - } - if (string.IsNullOrEmpty(Options.SignInScheme)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.SignInScheme))); - } - } - - protected HttpClient Backchannel { get; private set; } - - /// - /// Provides the object for processing authentication-related requests. - /// - /// An configured with the supplied to the constructor. - protected override AuthenticationHandler CreateHandler() - { - return new OAuthHandler(Backchannel); - } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs index 57ecba2f4..4eacbd0b3 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs @@ -1,16 +1,16 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; -using System.ComponentModel; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.OAuth; -using Microsoft.AspNetCore.Http.Authentication; +using System.Globalization; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Authentication.OAuth { /// - /// Configuration options for . + /// Configuration options OAuth. /// public class OAuthOptions : RemoteAuthenticationOptions { @@ -19,6 +19,39 @@ public OAuthOptions() Events = new OAuthEvents(); } + /// + /// Check that the options are valid. Should throw an exception if things are not ok. + /// + public override void Validate() + { + base.Validate(); + + if (string.IsNullOrEmpty(ClientId)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ClientId)), nameof(ClientId)); + } + + if (string.IsNullOrEmpty(ClientSecret)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ClientSecret)), nameof(ClientSecret)); + } + + if (string.IsNullOrEmpty(AuthorizationEndpoint)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(AuthorizationEndpoint)), nameof(AuthorizationEndpoint)); + } + + if (string.IsNullOrEmpty(TokenEndpoint)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(TokenEndpoint)), nameof(TokenEndpoint)); + } + + if (!CallbackPath.HasValue) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(CallbackPath)), nameof(CallbackPath)); + } + } + /// /// Gets or sets the provider-assigned client id. /// @@ -47,14 +80,19 @@ public OAuthOptions() public string UserInformationEndpoint { get; set; } /// - /// Gets or sets the used to handle authentication events. + /// Gets or sets the used to handle authentication events. /// - public new IOAuthEvents Events + public new OAuthEvents Events { - get { return (IOAuthEvents)base.Events; } + get { return (OAuthEvents)base.Events; } set { base.Events = value; } } + /// + /// A collection of claim actions used to select values from the json user data and create Claims. + /// + public ClaimActionCollection ClaimActions { get; } = new ClaimActionCollection(); + /// /// Gets the list of permissions to request. /// @@ -64,11 +102,5 @@ public OAuthOptions() /// Gets or sets the type used to secure data handled by the middleware. /// public ISecureDataFormat StateDataFormat { get; set; } - - /// - /// For testing purposes only. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public ISystemClock SystemClock { get; set; } = new SystemClock(); } } diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthPostConfigureOptions.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthPostConfigureOptions.cs new file mode 100644 index 000000000..e97346413 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthPostConfigureOptions.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net.Http; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Used to setup defaults for the OAuthOptions. + /// + public class OAuthPostConfigureOptions : IPostConfigureOptions + where TOptions : OAuthOptions, new() + where THandler : OAuthHandler + { + private readonly IDataProtectionProvider _dp; + + public OAuthPostConfigureOptions(IDataProtectionProvider dataProtection) + { + _dp = dataProtection; + } + + public void PostConfigure(string name, TOptions options) + { + options.DataProtectionProvider = options.DataProtectionProvider ?? _dp; + if (options.Backchannel == null) + { + options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler()); + options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OAuth handler"); + options.Backchannel.Timeout = options.BackchannelTimeout; + options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + } + + if (options.StateDataFormat == null) + { + var dataProtector = options.DataProtectionProvider.CreateProtector( + typeof(THandler).FullName, name, "v1"); + options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/AssemblyInfo.cs deleted file mode 100644 index 76feceeff..000000000 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Reflection; -using System.Resources; - -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: AssemblyCompany("Microsoft Corporation.")] -[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/Resources.Designer.cs index 00c7b848b..5a38ade0b 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Properties/Resources.Designer.cs @@ -15,32 +15,28 @@ private static readonly ResourceManager _resourceManager /// internal static string Exception_OptionMustBeProvided { - get { return GetString("Exception_OptionMustBeProvided"); } + get => GetString("Exception_OptionMustBeProvided"); } /// /// The '{0}' option must be provided. /// internal static string FormatException_OptionMustBeProvided(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); - } + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); /// /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. /// internal static string Exception_ValidatorHandlerMismatch { - get { return GetString("Exception_ValidatorHandlerMismatch"); } + get => GetString("Exception_ValidatorHandlerMismatch"); } /// /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. /// internal static string FormatException_ValidatorHandlerMismatch() - { - return GetString("Exception_ValidatorHandlerMismatch"); - } + => GetString("Exception_ValidatorHandlerMismatch"); private static string GetString(string name, params string[] formatterNames) { diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json new file mode 100644 index 000000000..9c2394704 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json @@ -0,0 +1,1810 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.OAuth, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.OAuthExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddOAuth", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddOAuth", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddOAuth", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddOAuth", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler" + ] + } + ] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.DependencyInjection.OAuthPostConfigureOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Options.IPostConfigureOptions" + ], + "Members": [ + { + "Kind": "Method", + "Name": "PostConfigure", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "T0" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "dataProtection", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Builder.OAuthAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseOAuthAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseOAuthAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.ClaimActionCollectionMapExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "MapJsonKey", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapJsonKey", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapJsonSubKey", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + }, + { + "Name": "subKey", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapJsonSubKey", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + }, + { + "Name": "subKey", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapCustomJson", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "resolver", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapCustomJson", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + }, + { + "Name": "resolver", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapAll", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapAllExcept", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "exclusions", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DeleteClaim", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DeleteClaims", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimTypes", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthCreatingTicketContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "Newtonsoft.Json.Linq.JObject", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenResponse", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AccessToken", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RefreshToken", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ExpiresIn", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Backchannel", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpClient", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Identity", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsIdentity", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RunClaimActions", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RunClaimActions", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + }, + { + "Name": "backchannel", + "Type": "System.Net.Http.HttpClient" + }, + { + "Name": "tokens", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + }, + { + "Name": "backchannel", + "Type": "System.Net.Http.HttpClient" + }, + { + "Name": "tokens", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse" + }, + { + "Name": "user", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnCreatingTicket", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnCreatingTicket", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToAuthorizationEndpoint", + "Parameters": [], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToAuthorizationEndpoint", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreatingTicket", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthCreatingTicketContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToAuthorizationEndpoint", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthChallengeProperties", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Scope", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Scope", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.ICollection" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetScope", + "Parameters": [ + { + "Name": "scopes", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "parameters", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "ScopeKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Backchannel", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpClient", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ExchangeCodeAsync", + "Parameters": [ + { + "Name": "code", + "Type": "System.String" + }, + { + "Name": "redirectUri", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateTicketAsync", + "Parameters": [ + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "tokens", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "BuildChallengeUrl", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "redirectUri", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatScope", + "Parameters": [ + { + "Name": "scopes", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FormatScope", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClientId", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientId", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClientSecret", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientSecret", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthorizationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthorizationEndpoint", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenEndpoint", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UserInformationEndpoint", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_UserInformationEndpoint", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClaimActions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Scope", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StateDataFormat", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StateDataFormat", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Success", + "Parameters": [ + { + "Name": "response", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Failed", + "Parameters": [ + { + "Name": "error", + "Type": "System.Exception" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthTokenResponse", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Response", + "Parameters": [], + "ReturnType": "Newtonsoft.Json.Linq.JObject", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Response", + "Parameters": [ + { + "Name": "value", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AccessToken", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AccessToken", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenType", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RefreshToken", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RefreshToken", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ExpiresIn", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ExpiresIn", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Error", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Error", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ClaimType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ValueType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.Collections.Generic.IEnumerable" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Clear", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "action", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetEnumerator", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerator", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.Collections.Generic.IEnumerable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.CustomJsonClaimAction", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Resolver", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + }, + { + "Name": "resolver", + "Type": "System.Func" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.DeleteClaimAction", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.JsonKeyClaimAction", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_JsonKey", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.JsonSubKeyClaimAction", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.JsonKeyClaimAction", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_SubKey", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + }, + { + "Name": "subKey", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OAuth.Claims.MapAllClaimsAction", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimAction", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/project.json b/src/Microsoft.AspNetCore.Authentication.OAuth/project.json deleted file mode 100644 index a4ee11166..000000000 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/project.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "ASP.NET Core middleware that enables an application to support any standard OAuth 2.0 authentication workflow.", - "tags": [ - "aspnetcore", - "authentication", - "security" - ], - "repository": { - "type": "git", - "url": "git://github.com/aspnet/security" - }, - "compilationOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk", - "nowarn": [ - "CS1591" - ], - "xmlDoc": true - }, - "dependencies": { - "Microsoft.AspNetCore.Authentication": "1.0.0-*", - "Newtonsoft.Json": "8.0.3" - }, - "frameworks": { - "net451": {}, - "netstandard1.3": { - "dependencies": { - "System.Runtime.Serialization.Primitives": "4.1.1-*" - }, - "imports": [ - "portable-net451+win8" - ] - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/ClaimActionCollectionUniqueExtensions.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/ClaimActionCollectionUniqueExtensions.cs new file mode 100644 index 000000000..4e349579f --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/ClaimActionCollectionUniqueExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Microsoft.AspNetCore.Authentication.OpenIdConnect.Claims; + +namespace Microsoft.AspNetCore.Authentication +{ + public static class ClaimActionCollectionUniqueExtensions + { + /// + /// Selects a top level value from the json user data with the given key name and adds it as a Claim. + /// This no-ops if the ClaimsIdentity already contains a Claim with the given ClaimType. + /// This no-ops if the key is not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + public static void MapUniqueJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey) + { + collection.MapUniqueJsonKey(claimType, jsonKey, ClaimValueTypes.String); + } + + /// + /// Selects a top level value from the json user data with the given key name and adds it as a Claim. + /// This no-ops if the ClaimsIdentity already contains a Claim with the given ClaimType. + /// This no-ops if the key is not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + /// The value to use for Claim.ValueType when creating a Claim. + public static void MapUniqueJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey, string valueType) + { + collection.Add(new UniqueJsonKeyClaimAction(claimType, valueType, jsonKey)); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/UniqueJsonKeyClaimAction.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/UniqueJsonKeyClaimAction.cs new file mode 100644 index 000000000..132885b3c --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/UniqueJsonKeyClaimAction.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect.Claims +{ + /// + /// A ClaimAction that selects a top level value from the json user data with the given key name and adds it as a Claim. + /// This no-ops if the ClaimsIdentity already contains a Claim with the given ClaimType. + /// This no-ops if the key is not found or the value is empty. + /// + public class UniqueJsonKeyClaimAction : JsonKeyClaimAction + { + /// + /// Creates a new UniqueJsonKeyClaimAction. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The top level key to look for in the json user data. + public UniqueJsonKeyClaimAction(string claimType, string valueType, string jsonKey) + : base(claimType, valueType, jsonKey) + { + } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + var value = userData?.Value(JsonKey); + if (string.IsNullOrEmpty(value)) + { + // Not found + return; + } + + var claim = identity.FindFirst(c => string.Equals(c.Type, JsonKey, System.StringComparison.OrdinalIgnoreCase)); + if (claim != null && string.Equals(claim.Value, value, System.StringComparison.Ordinal)) + { + // Duplicate + return; + } + + claim = identity.FindFirst(c => + { + // If this claimType is mapped by the JwtSeurityTokenHandler, then this property will be set + return c.Properties.TryGetValue(JwtSecurityTokenHandler.ShortClaimTypeProperty, out var shortType) + && string.Equals(shortType, JsonKey, System.StringComparison.OrdinalIgnoreCase); + }); + if (claim != null && string.Equals(claim.Value, value, System.StringComparison.Ordinal)) + { + // Duplicate with an alternate name. + return; + } + + identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer)); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs index 776f78d6e..203da93c5 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthenticationFailedContext.cs @@ -2,17 +2,18 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace Microsoft.AspNetCore.Authentication.OpenIdConnect { - public class AuthenticationFailedContext : BaseOpenIdConnectContext + public class AuthenticationFailedContext : RemoteAuthenticationContext { - public AuthenticationFailedContext(HttpContext context, OpenIdConnectOptions options) - : base(context, options) - { - } + public AuthenticationFailedContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options) + : base(context, scheme, options, new AuthenticationProperties()) + { } + + public OpenIdConnectMessage ProtocolMessage { get; set; } public Exception Exception { get; set; } } diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs index 49c863e4b..bdf6e4a7f 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/AuthorizationCodeReceivedContext.cs @@ -3,9 +3,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Net.Http; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace Microsoft.AspNetCore.Authentication.OpenIdConnect @@ -13,17 +11,19 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect /// /// This Context can be used to be informed when an 'AuthorizationCode' is received over the OpenIdConnect protocol. /// - public class AuthorizationCodeReceivedContext : BaseOpenIdConnectContext + public class AuthorizationCodeReceivedContext : RemoteAuthenticationContext { /// /// Creates a /// - public AuthorizationCodeReceivedContext(HttpContext context, OpenIdConnectOptions options) - : base(context, options) - { - } + public AuthorizationCodeReceivedContext( + HttpContext context, + AuthenticationScheme scheme, + OpenIdConnectOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) { } - public AuthenticationProperties Properties { get; set; } + public OpenIdConnectMessage ProtocolMessage { get; set; } /// /// Gets or sets the that was received in the authentication response, if any. @@ -42,23 +42,23 @@ public AuthorizationCodeReceivedContext(HttpContext context, OpenIdConnectOption /// /// If the developer chooses to redeem the code themselves then they can provide the resulting tokens here. This is the - /// same as calling HandleCodeRedemption. If set then the middleware will not attempt to redeem the code. An IdToken + /// same as calling HandleCodeRedemption. If set then the handler will not attempt to redeem the code. An IdToken /// is required if one had not been previously received in the authorization response. An access token is optional - /// if the middleware is to contact the user-info endpoint. + /// if the handler is to contact the user-info endpoint. /// public OpenIdConnectMessage TokenEndpointResponse { get; set; } /// - /// Indicates if the developer choose to handle (or skip) the code redemption. If true then the middleware will not attempt + /// Indicates if the developer choose to handle (or skip) the code redemption. If true then the handler will not attempt /// to redeem the code. See HandleCodeRedemption and TokenEndpointResponse. /// public bool HandledCodeRedemption => TokenEndpointResponse != null; /// - /// Tells the middleware to skip the code redemption process. The developer may have redeemed the code themselves, or + /// Tells the handler to skip the code redemption process. The developer may have redeemed the code themselves, or /// decided that the redemption was not required. If tokens were retrieved that are needed for further processing then /// call one of the overloads that allows providing tokens. An IdToken is required if one had not been previously received - /// in the authorization response. An access token can optionally be provided for the middleware to contact the + /// in the authorization response. An access token can optionally be provided for the handler to contact the /// user-info endpoint. Calling this is the same as setting TokenEndpointResponse. /// public void HandleCodeRedemption() @@ -67,10 +67,10 @@ public void HandleCodeRedemption() } /// - /// Tells the middleware to skip the code redemption process. The developer may have redeemed the code themselves, or + /// Tells the handler to skip the code redemption process. The developer may have redeemed the code themselves, or /// decided that the redemption was not required. If tokens were retrieved that are needed for further processing then /// call one of the overloads that allows providing tokens. An IdToken is required if one had not been previously received - /// in the authorization response. An access token can optionally be provided for the middleware to contact the + /// in the authorization response. An access token can optionally be provided for the handler to contact the /// user-info endpoint. Calling this is the same as setting TokenEndpointResponse. /// public void HandleCodeRedemption(string accessToken, string idToken) @@ -79,10 +79,10 @@ public void HandleCodeRedemption(string accessToken, string idToken) } /// - /// Tells the middleware to skip the code redemption process. The developer may have redeemed the code themselves, or + /// Tells the handler to skip the code redemption process. The developer may have redeemed the code themselves, or /// decided that the redemption was not required. If tokens were retrieved that are needed for further processing then /// call one of the overloads that allows providing tokens. An IdToken is required if one had not been previously received - /// in the authorization response. An access token can optionally be provided for the middleware to contact the + /// in the authorization response. An access token can optionally be provided for the handler to contact the /// user-info endpoint. Calling this is the same as setting TokenEndpointResponse. /// public void HandleCodeRedemption(OpenIdConnectMessage tokenEndpointResponse) diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/BaseOpenIdConnectContext.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/BaseOpenIdConnectContext.cs deleted file mode 100644 index d2f56a4ce..000000000 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/BaseOpenIdConnectContext.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; - -namespace Microsoft.AspNetCore.Authentication.OpenIdConnect -{ - public class BaseOpenIdConnectContext : BaseControlContext - { - public BaseOpenIdConnectContext(HttpContext context, OpenIdConnectOptions options) - : base(context) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - Options = options; - } - - public OpenIdConnectOptions Options { get; } - - public OpenIdConnectMessage ProtocolMessage { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/IOpenIdConnectEvents.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/IOpenIdConnectEvents.cs deleted file mode 100644 index 57600cee8..000000000 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/IOpenIdConnectEvents.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Authentication.OpenIdConnect -{ - /// - /// Specifies events which the invokes to enable developer control over the authentication process. - /// - public interface IOpenIdConnectEvents : IRemoteAuthenticationEvents - { - /// - /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. - /// - Task AuthenticationFailed(AuthenticationFailedContext context); - - /// - /// Invoked after security token validation if an authorization code is present in the protocol message. - /// - Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context); - - /// - /// Invoked when a protocol message is first received. - /// - Task MessageReceived(MessageReceivedContext context); - - /// - /// Invoked before redirecting to the identity provider to authenticate. - /// - Task RedirectToIdentityProvider(RedirectContext context); - - /// - /// Invoked before redirecting to the identity provider to sign out. - /// - Task RedirectToIdentityProviderForSignOut(RedirectContext context); - - /// - /// Invoked after "authorization code" is redeemed for tokens at the token endpoint. - /// - Task TokenResponseReceived(TokenResponseReceivedContext context); - - /// - /// Invoked when an IdToken has been validated and produced an AuthenticationTicket. - /// - Task TokenValidated(TokenValidatedContext context); - - /// - /// Invoked when user information is retrieved from the UserInfoEndpoint. - /// - Task UserInformationReceived(UserInformationReceivedContext context); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs index b2554969c..7d06e4479 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/MessageReceivedContext.cs @@ -1,24 +1,25 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace Microsoft.AspNetCore.Authentication.OpenIdConnect { - public class MessageReceivedContext : BaseOpenIdConnectContext + public class MessageReceivedContext : RemoteAuthenticationContext { - public MessageReceivedContext(HttpContext context, OpenIdConnectOptions options) - : base(context, options) - { - } + public MessageReceivedContext( + HttpContext context, + AuthenticationScheme scheme, + OpenIdConnectOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) { } + + public OpenIdConnectMessage ProtocolMessage { get; set; } /// - /// Bearer Token. This will give application an opportunity to retrieve token from an alternation location. + /// Bearer Token. This will give the application an opportunity to retrieve a token from an alternative location. /// public string Token { get; set; } - - public AuthenticationProperties Properties { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs index 9893b7207..2a48d250b 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs @@ -7,49 +7,61 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect { /// - /// Specifies events which the invokes to enable developer control over the authentication process. + /// Specifies events which the invokes to enable developer control over the authentication process. /// - public class OpenIdConnectEvents : RemoteAuthenticationEvents, IOpenIdConnectEvents + public class OpenIdConnectEvents : RemoteAuthenticationEvents { /// /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. /// - public Func OnAuthenticationFailed { get; set; } = context => Task.FromResult(0); + public Func OnAuthenticationFailed { get; set; } = context => Task.CompletedTask; /// /// Invoked after security token validation if an authorization code is present in the protocol message. /// - public Func OnAuthorizationCodeReceived { get; set; } = context => Task.FromResult(0); + public Func OnAuthorizationCodeReceived { get; set; } = context => Task.CompletedTask; /// /// Invoked when a protocol message is first received. /// - public Func OnMessageReceived { get; set; } = context => Task.FromResult(0); + public Func OnMessageReceived { get; set; } = context => Task.CompletedTask; /// - /// Invoked before redirecting to the identity provider to authenticate. + /// Invoked before redirecting to the identity provider to authenticate. This can be used to set ProtocolMessage.State + /// that will be persisted through the authentication process. The ProtocolMessage can also be used to add or customize + /// parameters sent to the identity provider. /// - public Func OnRedirectToIdentityProvider { get; set; } = context => Task.FromResult(0); + public Func OnRedirectToIdentityProvider { get; set; } = context => Task.CompletedTask; /// /// Invoked before redirecting to the identity provider to sign out. /// - public Func OnRedirectToIdentityProviderForSignOut { get; set; } = context => Task.FromResult(0); + public Func OnRedirectToIdentityProviderForSignOut { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked before redirecting to the at the end of a remote sign-out flow. + /// + public Func OnSignedOutCallbackRedirect { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked when a request is received on the RemoteSignOutPath. + /// + public Func OnRemoteSignOut { get; set; } = context => Task.CompletedTask; /// /// Invoked after "authorization code" is redeemed for tokens at the token endpoint. /// - public Func OnTokenResponseReceived { get; set; } = context => Task.FromResult(0); + public Func OnTokenResponseReceived { get; set; } = context => Task.CompletedTask; /// /// Invoked when an IdToken has been validated and produced an AuthenticationTicket. /// - public Func OnTokenValidated { get; set; } = context => Task.FromResult(0); + public Func OnTokenValidated { get; set; } = context => Task.CompletedTask; /// /// Invoked when user information is retrieved from the UserInfoEndpoint. /// - public Func OnUserInformationReceived { get; set; } = context => Task.FromResult(0); + public Func OnUserInformationReceived { get; set; } = context => Task.CompletedTask; public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context); @@ -61,6 +73,10 @@ public class OpenIdConnectEvents : RemoteAuthenticationEvents, IOpenIdConnectEve public virtual Task RedirectToIdentityProviderForSignOut(RedirectContext context) => OnRedirectToIdentityProviderForSignOut(context); + public virtual Task SignedOutCallbackRedirect(RemoteSignOutContext context) => OnSignedOutCallbackRedirect(context); + + public virtual Task RemoteSignOut(RemoteSignOutContext context) => OnRemoteSignOut(context); + public virtual Task TokenResponseReceived(TokenResponseReceivedContext context) => OnTokenResponseReceived(context); public virtual Task TokenValidated(TokenValidatedContext context) => OnTokenValidated(context); diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RedirectContext.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RedirectContext.cs index 59b1c0efd..9961c237d 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RedirectContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RedirectContext.cs @@ -1,24 +1,34 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace Microsoft.AspNetCore.Authentication.OpenIdConnect { /// - /// When a user configures the to be notified prior to redirecting to an IdentityProvider + /// When a user configures the to be notified prior to redirecting to an IdentityProvider /// an instance of is passed to the 'RedirectToAuthenticationEndpoint' or 'RedirectToEndSessionEndpoint' events. /// - public class RedirectContext : BaseOpenIdConnectContext + public class RedirectContext : PropertiesContext { - public RedirectContext(HttpContext context, OpenIdConnectOptions options, AuthenticationProperties properties) - : base(context, options) - { - Properties = properties; - } + public RedirectContext( + HttpContext context, + AuthenticationScheme scheme, + OpenIdConnectOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) { } - public AuthenticationProperties Properties { get; } + public OpenIdConnectMessage ProtocolMessage { get; set; } + + /// + /// If true, will skip any default logic for this redirect. + /// + public bool Handled { get; private set; } + + /// + /// Skips any default logic for this redirect. + /// + public void HandleResponse() => Handled = true; } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs new file mode 100644 index 000000000..26720a58f --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/RemoteSignoutContext.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + public class RemoteSignOutContext : RemoteAuthenticationContext + { + public RemoteSignOutContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, OpenIdConnectMessage message) + : base(context, scheme, options, new AuthenticationProperties()) + => ProtocolMessage = message; + + public OpenIdConnectMessage ProtocolMessage { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs index b4a9ad6d1..2bebdb8dc 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenResponseReceivedContext.cs @@ -1,9 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Builder; +using System.Security.Claims; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace Microsoft.AspNetCore.Authentication.OpenIdConnect @@ -11,18 +10,16 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect /// /// This Context can be used to be informed when an 'AuthorizationCode' is redeemed for tokens at the token endpoint. /// - public class TokenResponseReceivedContext : BaseOpenIdConnectContext + public class TokenResponseReceivedContext : RemoteAuthenticationContext { /// /// Creates a /// - public TokenResponseReceivedContext(HttpContext context, OpenIdConnectOptions options, AuthenticationProperties properties) - : base(context, options) - { - Properties = properties; - } + public TokenResponseReceivedContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, ClaimsPrincipal user, AuthenticationProperties properties) + : base(context, scheme, options, properties) + => Principal = user; - public AuthenticationProperties Properties { get; } + public OpenIdConnectMessage ProtocolMessage { get; set; } /// /// Gets or sets the that contains the tokens received after redeeming the code at the token endpoint. diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenValidatedContext.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenValidatedContext.cs index 130a4d987..853857dc7 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenValidatedContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/TokenValidatedContext.cs @@ -2,26 +2,23 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.IdentityModel.Tokens.Jwt; -using System.Net.Http; -using Microsoft.AspNetCore.Builder; +using System.Security.Claims; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace Microsoft.AspNetCore.Authentication.OpenIdConnect { - public class TokenValidatedContext : BaseOpenIdConnectContext + public class TokenValidatedContext : RemoteAuthenticationContext { /// /// Creates a /// - public TokenValidatedContext(HttpContext context, OpenIdConnectOptions options) - : base(context, options) - { - } + public TokenValidatedContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, ClaimsPrincipal principal, AuthenticationProperties properties) + : base(context, scheme, options, properties) + => Principal = principal; + + public OpenIdConnectMessage ProtocolMessage { get; set; } - public AuthenticationProperties Properties { get; set; } - public JwtSecurityToken SecurityToken { get; set; } public OpenIdConnectMessage TokenEndpointResponse { get; set; } diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs index c0a53db44..0b855eaf3 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/UserInformationReceivedContext.cs @@ -1,18 +1,20 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Builder; +using System.Security.Claims; using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.Authentication.OpenIdConnect { - public class UserInformationReceivedContext : BaseOpenIdConnectContext + public class UserInformationReceivedContext : RemoteAuthenticationContext { - public UserInformationReceivedContext(HttpContext context, OpenIdConnectOptions options) - : base(context, options) - { - } + public UserInformationReceivedContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, ClaimsPrincipal principal, AuthenticationProperties properties) + : base(context, scheme, options, properties) + => Principal = principal; + + public OpenIdConnectMessage ProtocolMessage { get; set; } public JObject User { get; set; } } diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs index ff580ff26..224af87b6 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/LoggingExtensions.cs @@ -8,14 +8,15 @@ namespace Microsoft.Extensions.Logging internal static class LoggingExtensions { private static Action _redirectToIdentityProviderForSignOutHandledResponse; - private static Action _redirectToIdentityProviderForSignOutSkipped; private static Action _redirectToIdentityProviderHandledResponse; - private static Action _redirectToIdentityProviderSkipped; + private static Action _signoutCallbackRedirectHandledResponse; + private static Action _signoutCallbackRedirectSkipped; private static Action _updatingConfiguration; private static Action _receivedIdToken; private static Action _redeemingCodeForTokens; private static Action _enteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync; private static Action _enteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync; + private static Action _enteringOpenIdAuthenticationHandlerHandleSignOutAsync; private static Action _messageReceived; private static Action _messageReceivedContextHandledResponse; private static Action _messageReceivedContextSkipped; @@ -36,7 +37,8 @@ internal static class LoggingExtensions private static Action _invalidLogoutQueryStringRedirectUrl; private static Action _nullOrEmptyAuthorizationResponseState; private static Action _unableToReadAuthorizationResponseState; - private static Action _authorizationResponseError; + private static Action _responseError; + private static Action _responseErrorWithStatusCode; private static Action _exceptionProcessingMessage; private static Action _accessTokenNotAvailable; private static Action _retrievingClaims; @@ -47,6 +49,13 @@ internal static class LoggingExtensions private static Action _invalidSecurityTokenType; private static Action _unableToValidateIdToken; private static Action _postAuthenticationLocalRedirect; + private static Action _postSignOutRedirect; + private static Action _remoteSignOutHandledResponse; + private static Action _remoteSignOutSkipped; + private static Action _remoteSignOut; + private static Action _remoteSignOutSessionIdMissing; + private static Action _remoteSignOutSessionIdInvalid; + private static Action _signOut; static LoggingExtensions() { @@ -55,10 +64,6 @@ static LoggingExtensions() eventId: 1, logLevel: LogLevel.Debug, formatString: "RedirectToIdentityProviderForSignOut.HandledResponse"); - _redirectToIdentityProviderForSignOutSkipped = LoggerMessage.Define( - eventId: 2, - logLevel: LogLevel.Debug, - formatString: "RedirectToIdentityProviderForSignOut.Skipped"); _invalidLogoutQueryStringRedirectUrl = LoggerMessage.Define( eventId: 3, logLevel: LogLevel.Warning, @@ -67,6 +72,10 @@ static LoggingExtensions() eventId: 4, logLevel: LogLevel.Trace, formatString: "Entering {OpenIdConnectHandlerType}'s HandleUnauthorizedAsync."); + _enteringOpenIdAuthenticationHandlerHandleSignOutAsync = LoggerMessage.Define( + eventId: 14, + logLevel: LogLevel.Trace, + formatString: "Entering {OpenIdConnectHandlerType}'s HandleSignOutAsync."); _postAuthenticationLocalRedirect = LoggerMessage.Define( eventId: 5, logLevel: LogLevel.Trace, @@ -75,10 +84,6 @@ static LoggingExtensions() eventId: 6, logLevel: LogLevel.Debug, formatString: "RedirectToIdentityProvider.HandledResponse"); - _redirectToIdentityProviderSkipped = LoggerMessage.Define( - eventId: 7, - logLevel: LogLevel.Debug, - formatString: "RedirectToIdentityProvider.Skipped"); _invalidAuthenticationRequestUrl = LoggerMessage.Define( eventId: 8, logLevel: LogLevel.Warning, @@ -95,10 +100,14 @@ static LoggingExtensions() eventId: 11, logLevel: LogLevel.Debug, formatString: "Unable to read the message.State."); - _authorizationResponseError = LoggerMessage.Define( + _responseError = LoggerMessage.Define( eventId: 12, logLevel: LogLevel.Error, formatString: "Message contains error: '{Error}', error_description: '{ErrorDescription}', error_uri: '{ErrorUri}'."); + _responseErrorWithStatusCode = LoggerMessage.Define( + eventId: 49, + logLevel: LogLevel.Error, + formatString: "Message contains error: '{Error}', error_description: '{ErrorDescription}', error_uri: '{ErrorUri}', status code '{StatusCode}'."); _updatingConfiguration = LoggerMessage.Define( eventId: 13, logLevel: LogLevel.Debug, @@ -175,6 +184,10 @@ static LoggingExtensions() eventId: 32, logLevel: LogLevel.Debug, formatString: "TokenResponseReceived.Skipped"); + _postSignOutRedirect = LoggerMessage.Define( + eventId: 33, + logLevel: LogLevel.Trace, + formatString: "Using properties.RedirectUri for redirect post authentication: '{RedirectUri}'."); _userInformationReceived = LoggerMessage.Define( eventId: 35, logLevel: LogLevel.Trace, @@ -211,6 +224,40 @@ static LoggingExtensions() eventId: 43, logLevel: LogLevel.Error, formatString: "Unable to read the 'id_token', no suitable ISecurityTokenValidator was found for: '{IdToken}'."); + _remoteSignOutHandledResponse = LoggerMessage.Define( + eventId: 44, + logLevel: LogLevel.Debug, + formatString: "RemoteSignOutContext.HandledResponse"); + _remoteSignOutSkipped = LoggerMessage.Define( + eventId: 45, + logLevel: LogLevel.Debug, + formatString: "RemoteSignOutContext.Skipped"); + _remoteSignOut = LoggerMessage.Define( + eventId: 46, + logLevel: LogLevel.Information, + formatString: "Remote signout request processed."); + _remoteSignOutSessionIdMissing = LoggerMessage.Define( + eventId: 47, + logLevel: LogLevel.Error, + formatString: "The remote signout request was ignored because the 'sid' parameter " + + "was missing, which may indicate an unsolicited logout."); + _remoteSignOutSessionIdInvalid = LoggerMessage.Define( + eventId: 48, + logLevel: LogLevel.Error, + formatString: "The remote signout request was ignored because the 'sid' parameter didn't match " + + "the expected value, which may indicate an unsolicited logout."); + _signOut = LoggerMessage.Define( + eventId: 49, + logLevel: LogLevel.Information, + formatString: "AuthenticationScheme: {AuthenticationScheme} signed out."); + _signoutCallbackRedirectHandledResponse = LoggerMessage.Define( + eventId: 50, + logLevel: LogLevel.Debug, + formatString: "RedirectToSignedOutRedirectUri.HandledResponse"); + _signoutCallbackRedirectSkipped = LoggerMessage.Define( + eventId: 51, + logLevel: LogLevel.Debug, + formatString: "RedirectToSignedOutRedirectUri.Skipped"); } public static void UpdatingConfiguration(this ILogger logger) @@ -303,19 +350,19 @@ public static void RedirectToIdentityProviderForSignOutHandledResponse(this ILog _redirectToIdentityProviderForSignOutHandledResponse(logger, null); } - public static void RedirectToIdentityProviderForSignOutSkipped(this ILogger logger) + public static void RedirectToIdentityProviderHandledResponse(this ILogger logger) { - _redirectToIdentityProviderForSignOutSkipped(logger, null); + _redirectToIdentityProviderHandledResponse(logger, null); } - public static void RedirectToIdentityProviderHandledResponse(this ILogger logger) + public static void SignoutCallbackRedirectHandledResponse(this ILogger logger) { - _redirectToIdentityProviderHandledResponse(logger, null); + _signoutCallbackRedirectHandledResponse(logger, null); } - public static void RedirectToIdentityProviderSkipped(this ILogger logger) + public static void SignoutCallbackRedirectSkipped(this ILogger logger) { - _redirectToIdentityProviderSkipped(logger, null); + _signoutCallbackRedirectSkipped(logger, null); } public static void UserInformationReceivedHandledResponse(this ILogger logger) @@ -343,9 +390,14 @@ public static void UnableToReadAuthorizationResponseState(this ILogger logger) _unableToReadAuthorizationResponseState(logger, null); } - public static void AuthorizationResponseError(this ILogger logger, string error, string errorDescription, string errorUri) + public static void ResponseError(this ILogger logger, string error, string errorDescription, string errorUri) + { + _responseError(logger, error, errorDescription, errorUri, null); + } + + public static void ResponseErrorWithStatusCode(this ILogger logger, string error, string errorDescription, string errorUri, int statusCode) { - _authorizationResponseError(logger, error, errorDescription, errorUri, null); + _responseErrorWithStatusCode(logger, error, errorDescription, errorUri, statusCode, null); } public static void ExceptionProcessingMessage(this ILogger logger, Exception ex) @@ -403,6 +455,11 @@ public static void EnteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(th _enteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(logger, openIdConnectHandlerTypeName, null); } + public static void EnteringOpenIdAuthenticationHandlerHandleSignOutAsync(this ILogger logger, string openIdConnectHandlerTypeName) + { + _enteringOpenIdAuthenticationHandlerHandleSignOutAsync(logger, openIdConnectHandlerTypeName, null); + } + public static void UserInformationReceived(this ILogger logger, string user) { _userInformationReceived(logger, user, null); @@ -412,5 +469,40 @@ public static void PostAuthenticationLocalRedirect(this ILogger logger, string r { _postAuthenticationLocalRedirect(logger, redirectUri, null); } + + public static void PostSignOutRedirect(this ILogger logger, string redirectUri) + { + _postSignOutRedirect(logger, redirectUri, null); + } + + public static void RemoteSignOutHandledResponse(this ILogger logger) + { + _remoteSignOutHandledResponse(logger, null); + } + + public static void RemoteSignOutSkipped(this ILogger logger) + { + _remoteSignOutSkipped(logger, null); + } + + public static void RemoteSignOut(this ILogger logger) + { + _remoteSignOut(logger, null); + } + + public static void RemoteSignOutSessionIdMissing(this ILogger logger) + { + _remoteSignOutSessionIdMissing(logger, null); + } + + public static void RemoteSignOutSessionIdInvalid(this ILogger logger) + { + _remoteSignOutSessionIdInvalid(logger, null); + } + + public static void SignedOut(this ILogger logger, string authenticationScheme) + { + _signOut(logger, authenticationScheme, null); + } } } diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj new file mode 100644 index 000000000..65ca6b842 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj @@ -0,0 +1,19 @@ + + + + ASP.NET Core middleware that enables an application to support the OpenID Connect authentication workflow. + netcoreapp3.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.xproj b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.xproj deleted file mode 100644 index 2f970b513..000000000 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.xproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 35115d55-b69e-46d4-bb33-c9e9e6ec5e7a - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectAppBuilderExtensions.cs index dde12494d..0746ae3fd 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectAppBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectAppBuilderExtensions.cs @@ -1,9 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { @@ -13,38 +12,26 @@ namespace Microsoft.AspNetCore.Builder public static class OpenIdConnectAppBuilderExtensions { /// - /// Adds the middleware to the specified , which enables OpenID Connect authentication capabilities. + /// UseOpenIdConnectAuthentication is obsolete. Configure OpenIdConnect authentication with AddAuthentication().AddOpenIdConnect in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. + /// The to add the handler to. /// A reference to this instance after the operation has completed. + [Obsolete("UseOpenIdConnectAuthentication is obsolete. Configure OpenIdConnect authentication with AddAuthentication().AddOpenIdConnect in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseOpenIdConnectAuthentication(this IApplicationBuilder app) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware(); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } /// - /// Adds the middleware to the specified , which enables OpenID Connect authentication capabilities. + /// UseOpenIdConnectAuthentication is obsolete. Configure OpenIdConnect authentication with AddAuthentication().AddOpenIdConnect in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. - /// A that specifies options for the middleware. + /// The to add the handler to. + /// A that specifies options for the handler. /// A reference to this instance after the operation has completed. + [Obsolete("UseOpenIdConnectAuthentication is obsolete. Configure OpenIdConnect authentication with AddAuthentication().AddOpenIdConnect in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseOpenIdConnectAuthentication(this IApplicationBuilder app, OpenIdConnectOptions options) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - return app.UseMiddleware(Options.Create(options)); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectChallengeProperties.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectChallengeProperties.cs new file mode 100644 index 000000000..0ced488de --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectChallengeProperties.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + public class OpenIdConnectChallengeProperties : OAuthChallengeProperties + { + /// + /// The parameter key for the "max_age" argument being used for a challenge request. + /// + public static readonly string MaxAgeKey = OpenIdConnectParameterNames.MaxAge; + + /// + /// The parameter key for the "prompt" argument being used for a challenge request. + /// + public static readonly string PromptKey = OpenIdConnectParameterNames.Prompt; + + public OpenIdConnectChallengeProperties() + { } + + public OpenIdConnectChallengeProperties(IDictionary items) + : base(items) + { } + + public OpenIdConnectChallengeProperties(IDictionary items, IDictionary parameters) + : base(items, parameters) + { } + + /// + /// The "max_age" parameter value being used for a challenge request. + /// + public TimeSpan? MaxAge + { + get => GetParameter(MaxAgeKey); + set => SetParameter(MaxAgeKey, value); + } + + /// + /// The "prompt" parameter value being used for a challenge request. + /// + public string Prompt + { + get => GetParameter(PromptKey); + set => SetParameter(PromptKey, value); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectDefaults.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectDefaults.cs index a099a7276..f98ba87e0 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectDefaults.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectDefaults.cs @@ -4,7 +4,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect { /// - /// Default values related to OpenIdConnect authentication middleware + /// Default values related to OpenIdConnect authentication handler /// public static class OpenIdConnectDefaults { @@ -19,9 +19,9 @@ public static class OpenIdConnectDefaults public const string AuthenticationScheme = "OpenIdConnect"; /// - /// The default value for OpenIdConnectOptions.Caption. + /// The default value for the display name. /// - public static readonly string Caption = "OpenIdConnect"; + public static readonly string DisplayName = "OpenIdConnect"; /// /// The prefix used to for the nonce in the cookie. diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectExtensions.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectExtensions.cs new file mode 100644 index 000000000..f427bebaf --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class OpenIdConnectExtensions + { + public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder) + => builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, _ => { }); + + public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddOpenIdConnect(authenticationScheme, OpenIdConnectDefaults.DisplayName, configureOptions); + + public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdConnectPostConfigureOptions>()); + return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs index f85ba9c14..8b0b72428 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs @@ -13,11 +13,9 @@ using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using Microsoft.Net.Http.Headers; @@ -28,167 +26,293 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect /// /// A per-request authentication handler for the OpenIdConnectAuthenticationMiddleware. /// - public class OpenIdConnectHandler : RemoteAuthenticationHandler + public class OpenIdConnectHandler : RemoteAuthenticationHandler, IAuthenticationSignOutHandler { private const string NonceProperty = "N"; - private const string UriSchemeDelimiter = "://"; - - private const string InputTagFormat = @""; - private const string HtmlFormFormat = @" - - - Please wait while you're being redirected to the identity provider - - -
- {1} - -
- - -"; + + private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT"; private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create(); private OpenIdConnectConfiguration _configuration; - protected HttpClient Backchannel { get; private set; } + protected HttpClient Backchannel => Options.Backchannel; - protected HtmlEncoder HtmlEncoder { get; private set; } + protected HtmlEncoder HtmlEncoder { get; } - public OpenIdConnectHandler(HttpClient backchannel, HtmlEncoder htmlEncoder) + public OpenIdConnectHandler(IOptionsMonitor options, ILoggerFactory logger, HtmlEncoder htmlEncoder, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) { - Backchannel = backchannel; HtmlEncoder = htmlEncoder; } /// - /// Handles Signout + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. /// - /// - protected override async Task HandleSignOutAsync(SignOutContext signout) + protected new OpenIdConnectEvents Events { - if (signout != null) + get { return (OpenIdConnectEvents)base.Events; } + set { base.Events = value; } + } + + protected override Task CreateEventsAsync() => Task.FromResult(new OpenIdConnectEvents()); + + public override Task HandleRequestAsync() + { + if (Options.RemoteSignOutPath.HasValue && Options.RemoteSignOutPath == Request.Path) { - if (_configuration == null && Options.ConfigurationManager != null) - { - _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); - } + return HandleRemoteSignOutAsync(); + } + else if (Options.SignedOutCallbackPath.HasValue && Options.SignedOutCallbackPath == Request.Path) + { + return HandleSignOutCallbackAsync(); + } + + return base.HandleRequestAsync(); + } + + protected virtual async Task HandleRemoteSignOutAsync() + { + OpenIdConnectMessage message = null; + + if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) + { + message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair(pair.Key, pair.Value))); + } + + // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small. + else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) + && !string.IsNullOrEmpty(Request.ContentType) + // May have media/type; charset=utf-8, allow partial match. + && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) + && Request.Body.CanRead) + { + var form = await Request.ReadFormAsync(); + message = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair(pair.Key, pair.Value))); + } + + var remoteSignOutContext = new RemoteSignOutContext(Context, Scheme, Options, message); + await Events.RemoteSignOut(remoteSignOutContext); - var message = new OpenIdConnectMessage() + if (remoteSignOutContext.Result != null) + { + if (remoteSignOutContext.Result.Handled) { - IssuerAddress = _configuration == null ? string.Empty : (_configuration.EndSessionEndpoint ?? string.Empty), - }; - - // Set End_Session_Endpoint in order: - // 1. properties.Redirect - // 2. Options.PostLogoutRedirectUri - var properties = new AuthenticationProperties(signout.Properties); - var logoutRedirectUri = properties.RedirectUri; - if (!string.IsNullOrEmpty(logoutRedirectUri)) + Logger.RemoteSignOutHandledResponse(); + return true; + } + if (remoteSignOutContext.Result.Skipped) { - // Relative to PathBase - if (logoutRedirectUri.StartsWith("/", StringComparison.Ordinal)) - { - logoutRedirectUri = BuildRedirectUri(logoutRedirectUri); - } - message.PostLogoutRedirectUri = logoutRedirectUri; + Logger.RemoteSignOutSkipped(); + return false; } - else if (!string.IsNullOrEmpty(Options.PostLogoutRedirectUri)) + if (remoteSignOutContext.Result.Failure != null) { - logoutRedirectUri = Options.PostLogoutRedirectUri; - // Relative to PathBase - if (logoutRedirectUri.StartsWith("/", StringComparison.Ordinal)) - { - logoutRedirectUri = BuildRedirectUri(logoutRedirectUri); - } - message.PostLogoutRedirectUri = logoutRedirectUri; + throw new InvalidOperationException("An error was returned from the RemoteSignOut event.", remoteSignOutContext.Result.Failure); } + } - message.IdTokenHint = await Context.Authentication.GetTokenAsync(OpenIdConnectParameterNames.IdToken); - var redirectContext = new RedirectContext(Context, Options, properties) - { - ProtocolMessage = message - }; + if (message == null) + { + return false; + } - await Options.Events.RedirectToIdentityProviderForSignOut(redirectContext); - if (redirectContext.HandledResponse) + // Try to extract the session identifier from the authentication ticket persisted by the sign-in handler. + // If the identifier cannot be found, bypass the session identifier checks: this may indicate that the + // authentication cookie was already cleared, that the session identifier was lost because of a lossy + // external/application cookie conversion or that the identity provider doesn't support sessions. + var sid = (await Context.AuthenticateAsync(Options.SignOutScheme)) + ?.Principal + ?.FindFirst(JwtRegisteredClaimNames.Sid) + ?.Value; + if (!string.IsNullOrEmpty(sid)) + { + // Ensure a 'sid' parameter was sent by the identity provider. + if (string.IsNullOrEmpty(message.Sid)) { - Logger.RedirectToIdentityProviderForSignOutHandledResponse(); - return; + Logger.RemoteSignOutSessionIdMissing(); + return true; } - else if (redirectContext.Skipped) + // Ensure the 'sid' parameter corresponds to the 'sid' stored in the authentication ticket. + if (!string.Equals(sid, message.Sid, StringComparison.Ordinal)) { - Logger.RedirectToIdentityProviderForSignOutSkipped(); - return; + Logger.RemoteSignOutSessionIdInvalid(); + return true; } + } - message = redirectContext.ProtocolMessage; + Logger.RemoteSignOut(); - if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet) - { - var redirectUri = message.CreateLogoutRequestUrl(); - if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) - { - Logger.InvalidLogoutQueryStringRedirectUrl(redirectUri); - } + // We've received a remote sign-out request + await Context.SignOutAsync(Options.SignOutScheme); + return true; + } - Response.Redirect(redirectUri); - } - else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost) + /// + /// Redirect user to the identity provider for sign out + /// + /// A task executing the sign out procedure + public async virtual Task SignOutAsync(AuthenticationProperties properties) + { + var target = ResolveTarget(Options.ForwardSignOut); + if (target != null) + { + await Context.SignOutAsync(target, properties); + return; + } + + properties = properties ?? new AuthenticationProperties(); + + Logger.EnteringOpenIdAuthenticationHandlerHandleSignOutAsync(GetType().FullName); + + if (_configuration == null && Options.ConfigurationManager != null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + var message = new OpenIdConnectMessage() + { + EnableTelemetryParameters = !Options.DisableTelemetry, + IssuerAddress = _configuration?.EndSessionEndpoint ?? string.Empty, + + // Redirect back to SigneOutCallbackPath first before user agent is redirected to actual post logout redirect uri + PostLogoutRedirectUri = BuildRedirectUriIfRelative(Options.SignedOutCallbackPath) + }; + + // Get the post redirect URI. + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = BuildRedirectUriIfRelative(Options.SignedOutRedirectUri); + if (string.IsNullOrWhiteSpace(properties.RedirectUri)) { - var inputs = new StringBuilder(); - foreach (var parameter in message.Parameters) - { - var name = HtmlEncoder.Encode(parameter.Key); - var value = HtmlEncoder.Encode(parameter.Value); + properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString; + } + } + Logger.PostSignOutRedirect(properties.RedirectUri); - var input = string.Format(CultureInfo.InvariantCulture, InputTagFormat, name, value); - inputs.AppendLine(input); - } + // Attach the identity token to the logout request when possible. + message.IdTokenHint = await Context.GetTokenAsync(Options.SignOutScheme, OpenIdConnectParameterNames.IdToken); + + var redirectContext = new RedirectContext(Context, Scheme, Options, properties) + { + ProtocolMessage = message + }; - var issuer = HtmlEncoder.Encode(message.IssuerAddress); + await Events.RedirectToIdentityProviderForSignOut(redirectContext); + if (redirectContext.Handled) + { + Logger.RedirectToIdentityProviderForSignOutHandledResponse(); + return; + } + + message = redirectContext.ProtocolMessage; - var content = string.Format(CultureInfo.InvariantCulture, HtmlFormFormat, issuer, inputs); - var buffer = Encoding.UTF8.GetBytes(content); + if (!string.IsNullOrEmpty(message.State)) + { + properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State; + } - Response.ContentLength = buffer.Length; - Response.ContentType = "text/html;charset=UTF-8"; + message.State = Options.StateDataFormat.Protect(properties); - // Emit Cache-Control=no-cache to prevent client caching. - Response.Headers[HeaderNames.CacheControl] = "no-cache"; - Response.Headers[HeaderNames.Pragma] = "no-cache"; - Response.Headers[HeaderNames.Expires] = "-1"; + if (string.IsNullOrEmpty(message.IssuerAddress)) + { + throw new InvalidOperationException("Cannot redirect to the end session endpoint, the configuration may be missing or invalid."); + } - await Response.Body.WriteAsync(buffer, 0, buffer.Length); + if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet) + { + var redirectUri = message.CreateLogoutRequestUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + Logger.InvalidLogoutQueryStringRedirectUrl(redirectUri); } + + Response.Redirect(redirectUri); + } + else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost) + { + var content = message.BuildFormPost(); + var buffer = Encoding.UTF8.GetBytes(content); + + Response.ContentLength = buffer.Length; + Response.ContentType = "text/html;charset=UTF-8"; + + // Emit Cache-Control=no-cache to prevent client caching. + Response.Headers[HeaderNames.CacheControl] = "no-cache"; + Response.Headers[HeaderNames.Pragma] = "no-cache"; + Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate; + + await Response.Body.WriteAsync(buffer, 0, buffer.Length); } + else + { + throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}"); + } + + Logger.SignedOut(Scheme.Name); } /// - /// Responds to a 401 Challenge. Sends an OpenIdConnect message to the 'identity authority' to obtain an identity. + /// Response to the callback from OpenId provider after session ended. /// - /// - protected override async Task HandleUnauthorizedAsync(ChallengeContext context) + /// A task executing the callback procedure + protected async virtual Task HandleSignOutCallbackAsync() { - if (context == null) + var message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair(pair.Key, pair.Value))); + AuthenticationProperties properties = null; + if (!string.IsNullOrEmpty(message.State)) { - throw new ArgumentNullException(nameof(context)); + properties = Options.StateDataFormat.Unprotect(message.State); } + var signOut = new RemoteSignOutContext(Context, Scheme, Options, message) + { + Properties = properties, + }; + + await Events.SignedOutCallbackRedirect(signOut); + if (signOut.Result != null) + { + if (signOut.Result.Handled) + { + Logger.SignoutCallbackRedirectHandledResponse(); + return true; + } + if (signOut.Result.Skipped) + { + Logger.SignoutCallbackRedirectSkipped(); + return false; + } + if (signOut.Result.Failure != null) + { + throw new InvalidOperationException("An error was returned from the SignedOutCallbackRedirect event.", signOut.Result.Failure); + } + } + + properties = signOut.Properties; + if (!string.IsNullOrEmpty(properties?.RedirectUri)) + { + Response.Redirect(properties.RedirectUri); + } + + return true; + } + + /// + /// Responds to a 401 Challenge. Sends an OpenIdConnect message to the 'identity authority' to obtain an identity. + /// + /// + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { Logger.EnteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(GetType().FullName); // order for local RedirectUri // 1. challenge.Properties.RedirectUri // 2. CurrentUri if RedirectUri is not set) - var properties = new AuthenticationProperties(context.Properties) - { - ExpiresUtc = Options.SystemClock.UtcNow.Add(Options.RemoteAuthenticationTimeout) - }; - if (string.IsNullOrEmpty(properties.RedirectUri)) { - properties.RedirectUri = CurrentUri; + properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString; } Logger.PostAuthenticationLocalRedirect(properties.RedirectUri); @@ -200,18 +324,29 @@ protected override async Task HandleUnauthorizedAsync(ChallengeContext con var message = new OpenIdConnectMessage { ClientId = Options.ClientId, + EnableTelemetryParameters = !Options.DisableTelemetry, IssuerAddress = _configuration?.AuthorizationEndpoint ?? string.Empty, RedirectUri = BuildRedirectUri(Options.CallbackPath), Resource = Options.Resource, ResponseType = Options.ResponseType, - Scope = string.Join(" ", Options.Scope) + Prompt = properties.GetParameter(OpenIdConnectParameterNames.Prompt) ?? Options.Prompt, + Scope = string.Join(" ", properties.GetParameter>(OpenIdConnectParameterNames.Scope) ?? Options.Scope), }; + // Add the 'max_age' parameter to the authentication request if MaxAge is not null. + // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + var maxAge = properties.GetParameter(OpenIdConnectParameterNames.MaxAge) ?? Options.MaxAge; + if (maxAge.HasValue) + { + message.MaxAge = Convert.ToInt64(Math.Floor((maxAge.Value).TotalSeconds)) + .ToString(CultureInfo.InvariantCulture); + } + // Omitting the response_mode parameter when it already corresponds to the default // response_mode used for the specified response_type is recommended by the specifications. // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes - if (!string.Equals(Options.ResponseType, OpenIdConnectResponseTypes.Code, StringComparison.Ordinal) || - !string.Equals(Options.ResponseMode, OpenIdConnectResponseModes.Query, StringComparison.Ordinal)) + if (!string.Equals(Options.ResponseType, OpenIdConnectResponseType.Code, StringComparison.Ordinal) || + !string.Equals(Options.ResponseMode, OpenIdConnectResponseMode.Query, StringComparison.Ordinal)) { message.ResponseMode = Options.ResponseMode; } @@ -224,21 +359,16 @@ protected override async Task HandleUnauthorizedAsync(ChallengeContext con GenerateCorrelationId(properties); - var redirectContext = new RedirectContext(Context, Options, properties) + var redirectContext = new RedirectContext(Context, Scheme, Options, properties) { ProtocolMessage = message }; - await Options.Events.RedirectToIdentityProvider(redirectContext); - if (redirectContext.HandledResponse) + await Events.RedirectToIdentityProvider(redirectContext); + if (redirectContext.Handled) { Logger.RedirectToIdentityProviderHandledResponse(); - return true; - } - else if (redirectContext.Skipped) - { - Logger.RedirectToIdentityProviderSkipped(); - return false; + return; } message = redirectContext.ProtocolMessage; @@ -253,6 +383,12 @@ protected override async Task HandleUnauthorizedAsync(ChallengeContext con message.State = Options.StateDataFormat.Protect(properties); + if (string.IsNullOrEmpty(message.IssuerAddress)) + { + throw new InvalidOperationException( + "Cannot redirect to the authorization endpoint, the configuration may be missing or invalid."); + } + if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet) { var redirectUri = message.CreateAuthenticationRequestUrl(); @@ -262,23 +398,11 @@ protected override async Task HandleUnauthorizedAsync(ChallengeContext con } Response.Redirect(redirectUri); - return true; + return; } else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost) { - var inputs = new StringBuilder(); - foreach (var parameter in message.Parameters) - { - var name = HtmlEncoder.Encode(parameter.Key); - var value = HtmlEncoder.Encode(parameter.Value); - - var input = string.Format(CultureInfo.InvariantCulture, InputTagFormat, name, value); - inputs.AppendLine(input); - } - - var issuer = HtmlEncoder.Encode(message.IssuerAddress); - - var content = string.Format(CultureInfo.InvariantCulture, HtmlFormFormat, issuer, inputs); + var content = message.BuildFormPost(); var buffer = Encoding.UTF8.GetBytes(content); Response.ContentLength = buffer.Length; @@ -287,10 +411,10 @@ protected override async Task HandleUnauthorizedAsync(ChallengeContext con // Emit Cache-Control=no-cache to prevent client caching. Response.Headers[HeaderNames.CacheControl] = "no-cache"; Response.Headers[HeaderNames.Pragma] = "no-cache"; - Response.Headers[HeaderNames.Expires] = "-1"; + Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate; await Response.Body.WriteAsync(buffer, 0, buffer.Length); - return true; + return; } throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}"); @@ -299,8 +423,8 @@ protected override async Task HandleUnauthorizedAsync(ChallengeContext con /// /// Invoked to process incoming OpenIdConnect messages. /// - /// An if successful. - protected override async Task HandleRemoteAuthenticateAsync() + /// An . + protected override async Task HandleRemoteAuthenticateAsync() { Logger.EnteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync(GetType().FullName); @@ -318,9 +442,9 @@ protected override async Task HandleRemoteAuthenticateAsync( if (Options.SkipUnrecognizedRequests) { // Not for us? - return AuthenticateResult.Skip(); + return HandleRequestResult.SkipHandler(); } - return AuthenticateResult.Fail("An OpenID Connect response cannot contain an " + + return HandleRequestResult.Fail("An OpenID Connect response cannot contain an " + "identity token or an access token when using response_mode=query"); } } @@ -340,25 +464,20 @@ protected override async Task HandleRemoteAuthenticateAsync( if (Options.SkipUnrecognizedRequests) { // Not for us? - return AuthenticateResult.Skip(); + return HandleRequestResult.SkipHandler(); } - return AuthenticateResult.Fail("No message."); + return HandleRequestResult.Fail("No message."); } - AuthenticateResult result; - + AuthenticationProperties properties = null; try { - AuthenticationProperties properties = null; - if (!string.IsNullOrEmpty(authorizationResponse.State)) - { - properties = Options.StateDataFormat.Unprotect(authorizationResponse.State); - } + properties = ReadPropertiesAndClearState(authorizationResponse); var messageReceivedContext = await RunMessageReceivedEventAsync(authorizationResponse, properties); - if (messageReceivedContext.CheckEventResult(out result)) + if (messageReceivedContext.Result != null) { - return result; + return messageReceivedContext.Result; } authorizationResponse = messageReceivedContext.ProtocolMessage; properties = messageReceivedContext.Properties; @@ -372,13 +491,12 @@ protected override async Task HandleRemoteAuthenticateAsync( Logger.NullOrEmptyAuthorizationResponseState(); if (Options.SkipUnrecognizedRequests) { - return AuthenticateResult.Skip(); + return HandleRequestResult.SkipHandler(); } - return AuthenticateResult.Fail(Resources.MessageStateIsNullOrEmpty); + return HandleRequestResult.Fail(Resources.MessageStateIsNullOrEmpty); } - // if state exists and we failed to 'unprotect' this is not a message we should process. - properties = Options.StateDataFormat.Unprotect(authorizationResponse.State); + properties = ReadPropertiesAndClearState(authorizationResponse); } if (properties == null) @@ -387,31 +505,32 @@ protected override async Task HandleRemoteAuthenticateAsync( if (Options.SkipUnrecognizedRequests) { // Not for us? - return AuthenticateResult.Skip(); + return HandleRequestResult.SkipHandler(); } - return AuthenticateResult.Fail(Resources.MessageStateIsInvalid); - } - string userstate = null; - properties.Items.TryGetValue(OpenIdConnectDefaults.UserstatePropertiesKey, out userstate); - authorizationResponse.State = userstate; + // if state exists and we failed to 'unprotect' this is not a message we should process. + return HandleRequestResult.Fail(Resources.MessageStateIsInvalid); + } if (!ValidateCorrelationId(properties)) { - return AuthenticateResult.Fail("Correlation failed."); + return HandleRequestResult.Fail("Correlation failed.", properties); } // if any of the error fields are set, throw error null if (!string.IsNullOrEmpty(authorizationResponse.Error)) { - Logger.AuthorizationResponseError( - authorizationResponse.Error, - authorizationResponse.ErrorDescription ?? "ErrorDecription null", - authorizationResponse.ErrorUri ?? "ErrorUri null"); - - return AuthenticateResult.Fail(new OpenIdConnectProtocolException( - string.Format(CultureInfo.InvariantCulture, Resources.MessageContainsError, authorizationResponse.Error, - authorizationResponse.ErrorDescription ?? "ErrorDecription null", authorizationResponse.ErrorUri ?? "ErrorUri null"))); + // Note: access_denied errors are special protocol errors indicating the user didn't + // approve the authorization demand requested by the remote authorization server. + // Since it's a frequent scenario (that is not caused by incorrect configuration), + // denied errors are handled differently using HandleAccessDeniedErrorAsync(). + // Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information. + if (string.Equals(authorizationResponse.Error, "access_denied", StringComparison.Ordinal)) + { + return await HandleAccessDeniedErrorAsync(properties); + } + + return HandleRequestResult.Fail(CreateOpenIdConnectProtocolException(authorizationResponse, response: null), properties); } if (_configuration == null && Options.ConfigurationManager != null) @@ -422,7 +541,7 @@ protected override async Task HandleRemoteAuthenticateAsync( PopulateSessionProperties(authorizationResponse, properties); - AuthenticationTicket ticket = null; + ClaimsPrincipal user = null; JwtSecurityToken jwt = null; string nonce = null; var validationParameters = Options.TokenValidationParameters.Clone(); @@ -431,22 +550,22 @@ protected override async Task HandleRemoteAuthenticateAsync( if (!string.IsNullOrEmpty(authorizationResponse.IdToken)) { Logger.ReceivedIdToken(); - ticket = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt); + user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt); - nonce = jwt?.Payload.Nonce; + nonce = jwt.Payload.Nonce; if (!string.IsNullOrEmpty(nonce)) { nonce = ReadNonceCookie(nonce); } - var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, null, properties, ticket, jwt, nonce); - if (tokenValidatedContext.CheckEventResult(out result)) + var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, null, user, properties, jwt, nonce); + if (tokenValidatedContext.Result != null) { - return result; + return tokenValidatedContext.Result; } authorizationResponse = tokenValidatedContext.ProtocolMessage; + user = tokenValidatedContext.Principal; properties = tokenValidatedContext.Properties; - ticket = tokenValidatedContext.Ticket; jwt = tokenValidatedContext.SecurityToken; nonce = tokenValidatedContext.Nonce; } @@ -464,17 +583,17 @@ protected override async Task HandleRemoteAuthenticateAsync( // Authorization Code or Hybrid flow if (!string.IsNullOrEmpty(authorizationResponse.Code)) { - var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(authorizationResponse, properties, ticket, jwt); - if (authorizationCodeReceivedContext.CheckEventResult(out result)) + var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(authorizationResponse, user, properties, jwt); + if (authorizationCodeReceivedContext.Result != null) { - return result; + return authorizationCodeReceivedContext.Result; } authorizationResponse = authorizationCodeReceivedContext.ProtocolMessage; + user = authorizationCodeReceivedContext.Principal; properties = authorizationCodeReceivedContext.Properties; var tokenEndpointRequest = authorizationCodeReceivedContext.TokenEndpointRequest; // If the developer redeemed the code themselves... tokenEndpointResponse = authorizationCodeReceivedContext.TokenEndpointResponse; - ticket = authorizationCodeReceivedContext.Ticket; jwt = authorizationCodeReceivedContext.JwtSecurityToken; if (!authorizationCodeReceivedContext.HandledCodeRedemption) @@ -482,41 +601,55 @@ protected override async Task HandleRemoteAuthenticateAsync( tokenEndpointResponse = await RedeemAuthorizationCodeAsync(tokenEndpointRequest); } - var tokenResponseReceivedContext = await RunTokenResponseReceivedEventAsync(authorizationResponse, tokenEndpointResponse, properties); - if (tokenResponseReceivedContext.CheckEventResult(out result)) + var tokenResponseReceivedContext = await RunTokenResponseReceivedEventAsync(authorizationResponse, tokenEndpointResponse, user, properties); + if (tokenResponseReceivedContext.Result != null) { - return result; + return tokenResponseReceivedContext.Result; } + authorizationResponse = tokenResponseReceivedContext.ProtocolMessage; tokenEndpointResponse = tokenResponseReceivedContext.TokenEndpointResponse; + user = tokenResponseReceivedContext.Principal; + properties = tokenResponseReceivedContext.Properties; - // We only have to process the IdToken if we didn't already get one in the AuthorizationResponse - if (ticket == null) - { - // no need to validate signature when token is received using "code flow" as per spec - // [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation]. - validationParameters.RequireSignedTokens = false; + // no need to validate signature when token is received using "code flow" as per spec + // [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation]. + validationParameters.RequireSignedTokens = false; - ticket = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out jwt); + // At least a cursory validation is required on the new IdToken, even if we've already validated the one from the authorization response. + // And we'll want to validate the new JWT in ValidateTokenResponse. + var tokenEndpointUser = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out var tokenEndpointJwt); - nonce = jwt?.Payload.Nonce; + // Avoid reading & deleting the nonce cookie, running the event, etc, if it was already done as part of the authorization response validation. + if (user == null) + { + nonce = tokenEndpointJwt.Payload.Nonce; if (!string.IsNullOrEmpty(nonce)) { nonce = ReadNonceCookie(nonce); } - var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, tokenEndpointResponse, properties, ticket, jwt, nonce); - if (tokenValidatedContext.CheckEventResult(out result)) + var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, tokenEndpointResponse, tokenEndpointUser, properties, tokenEndpointJwt, nonce); + if (tokenValidatedContext.Result != null) { - return result; + return tokenValidatedContext.Result; } authorizationResponse = tokenValidatedContext.ProtocolMessage; tokenEndpointResponse = tokenValidatedContext.TokenEndpointResponse; + user = tokenValidatedContext.Principal; properties = tokenValidatedContext.Properties; - ticket = tokenValidatedContext.Ticket; jwt = tokenValidatedContext.SecurityToken; nonce = tokenValidatedContext.Nonce; } + else + { + if (!string.Equals(jwt.Subject, tokenEndpointJwt.Subject, StringComparison.Ordinal)) + { + throw new SecurityTokenException("The sub claim does not match in the id_token's from the authorization and token endpoints."); + } + + jwt = tokenEndpointJwt; + } // Validate the token response if it wasn't provided manually if (!authorizationCodeReceivedContext.HandledCodeRedemption) @@ -533,22 +666,30 @@ protected override async Task HandleRemoteAuthenticateAsync( if (Options.SaveTokens) { - SaveTokens(ticket.Properties, tokenEndpointResponse ?? authorizationResponse); + SaveTokens(properties, tokenEndpointResponse ?? authorizationResponse); } if (Options.GetClaimsFromUserInfoEndpoint) { - return await GetUserInformationAsync(tokenEndpointResponse ?? authorizationResponse, jwt, ticket); + return await GetUserInformationAsync(tokenEndpointResponse ?? authorizationResponse, jwt, user, properties); + } + else + { + var identity = (ClaimsIdentity)user.Identity; + foreach (var action in Options.ClaimActions) + { + action.Run(null, identity, ClaimsIssuer); + } } - return AuthenticateResult.Success(ticket); + return HandleRequestResult.Success(new AuthenticationTicket(user, properties, Scheme.Name)); } catch (Exception exception) { Logger.ExceptionProcessingMessage(exception); // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event. - if (Options.RefreshOnIssuerKeyNotFound && exception.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException))) + if (Options.RefreshOnIssuerKeyNotFound && exception is SecurityTokenSignatureKeyNotFoundException) { if (Options.ConfigurationManager != null) { @@ -558,13 +699,30 @@ protected override async Task HandleRemoteAuthenticateAsync( } var authenticationFailedContext = await RunAuthenticationFailedEventAsync(authorizationResponse, exception); - if (authenticationFailedContext.CheckEventResult(out result)) + if (authenticationFailedContext.Result != null) { - return result; + return authenticationFailedContext.Result; } - return AuthenticateResult.Fail(exception); + return HandleRequestResult.Fail(exception, properties); + } + } + + private AuthenticationProperties ReadPropertiesAndClearState(OpenIdConnectMessage message) + { + AuthenticationProperties properties = null; + if (!string.IsNullOrEmpty(message.State)) + { + properties = Options.StateDataFormat.Unprotect(message.State); + + if (properties != null) + { + // If properties can be decoded from state, clear the message state. + properties.Items.TryGetValue(OpenIdConnectDefaults.UserstatePropertiesKey, out var userstate); + message.State = userstate; + } } + return properties; } private void PopulateSessionProperties(OpenIdConnectMessage message, AuthenticationProperties properties) @@ -581,20 +739,50 @@ private void PopulateSessionProperties(OpenIdConnectMessage message, Authenticat } /// - /// Redeems the authorization code for tokens at the token endpoint + /// Redeems the authorization code for tokens at the token endpoint. /// /// The request that will be sent to the token endpoint and is available for customization. /// OpenIdConnect message that has tokens inside it. protected virtual async Task RedeemAuthorizationCodeAsync(OpenIdConnectMessage tokenEndpointRequest) { Logger.RedeemingCodeForTokens(); + var requestMessage = new HttpRequestMessage(HttpMethod.Post, _configuration.TokenEndpoint); requestMessage.Content = new FormUrlEncodedContent(tokenEndpointRequest.Parameters); + var responseMessage = await Backchannel.SendAsync(requestMessage); - responseMessage.EnsureSuccessStatusCode(); - var tokenResonse = await responseMessage.Content.ReadAsStringAsync(); - var jsonTokenResponse = JObject.Parse(tokenResonse); - return new OpenIdConnectMessage(jsonTokenResponse); + + var contentMediaType = responseMessage.Content.Headers.ContentType?.MediaType; + if (string.IsNullOrEmpty(contentMediaType)) + { + Logger.LogDebug($"Unexpected token response format. Status Code: {(int)responseMessage.StatusCode}. Content-Type header is missing."); + } + else if (!string.Equals(contentMediaType, "application/json", StringComparison.OrdinalIgnoreCase)) + { + Logger.LogDebug($"Unexpected token response format. Status Code: {(int)responseMessage.StatusCode}. Content-Type {responseMessage.Content.Headers.ContentType}."); + } + + // Error handling: + // 1. If the response body can't be parsed as json, throws. + // 2. If the response's status code is not in 2XX range, throw OpenIdConnectProtocolException. If the body is correct parsed, + // pass the error information from body to the exception. + OpenIdConnectMessage message; + try + { + var responseContent = await responseMessage.Content.ReadAsStringAsync(); + message = new OpenIdConnectMessage(responseContent); + } + catch (Exception ex) + { + throw new OpenIdConnectProtocolException($"Failed to parse token response body as JSON. Status Code: {(int)responseMessage.StatusCode}. Content-Type: {responseMessage.Content.Headers.ContentType}", ex); + } + + if (!responseMessage.IsSuccessStatusCode) + { + throw CreateOpenIdConnectProtocolException(message, responseMessage); + } + + return message; } /// @@ -602,21 +790,24 @@ protected virtual async Task RedeemAuthorizationCodeAsync( /// /// message that is being processed /// The . - /// authentication ticket with claims principal and identities - /// Authentication ticket with identity with additional claims, if any. - protected virtual async Task GetUserInformationAsync(OpenIdConnectMessage message, JwtSecurityToken jwt, AuthenticationTicket ticket) + /// The claims principal and identities. + /// The authentication properties. + /// which is used to determine if the remote authentication was successful. + protected virtual async Task GetUserInformationAsync( + OpenIdConnectMessage message, JwtSecurityToken jwt, + ClaimsPrincipal principal, AuthenticationProperties properties) { var userInfoEndpoint = _configuration?.UserInfoEndpoint; if (string.IsNullOrEmpty(userInfoEndpoint)) { Logger.UserInfoEndpointNotSet(); - return AuthenticateResult.Success(ticket); + return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name)); } if (string.IsNullOrEmpty(message.AccessToken)) { Logger.AccessTokenNotAvailable(); - return AuthenticateResult.Success(ticket); + return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name)); } Logger.RetrievingClaims(); var requestMessage = new HttpRequestMessage(HttpMethod.Get, userInfoEndpoint); @@ -638,16 +829,16 @@ protected virtual async Task GetUserInformationAsync(OpenIdC } else { - return AuthenticateResult.Fail("Unknown response type: " + contentType.MediaType); + return HandleRequestResult.Fail("Unknown response type: " + contentType.MediaType, properties); } - var userInformationReceivedContext = await RunUserInformationReceivedEventAsync(ticket, message, user); - AuthenticateResult result; - if (userInformationReceivedContext.CheckEventResult(out result)) + var userInformationReceivedContext = await RunUserInformationReceivedEventAsync(principal, properties, message, user); + if (userInformationReceivedContext.Result != null) { - return result; + return userInformationReceivedContext.Result; } - ticket = userInformationReceivedContext.Ticket; + principal = userInformationReceivedContext.Principal; + properties = userInformationReceivedContext.Properties; user = userInformationReceivedContext.User; Options.ProtocolValidator.ValidateUserInfoResponse(new OpenIdConnectProtocolValidationContext() @@ -656,37 +847,14 @@ protected virtual async Task GetUserInformationAsync(OpenIdC ValidatedIdToken = jwt, }); - var identity = (ClaimsIdentity)ticket.Principal.Identity; - - foreach (var claim in identity.Claims) - { - // If this claimType is mapped by the JwtSeurityTokenHandler, then this property will be set - var shortClaimTypeName = claim.Properties.ContainsKey(JwtSecurityTokenHandler.ShortClaimTypeProperty) ? - claim.Properties[JwtSecurityTokenHandler.ShortClaimTypeProperty] : string.Empty; - - // checking if claim in the identity (generated from id_token) has the same type as a claim retrieved from userinfo endpoint - JToken value; - var isClaimIncluded = user.TryGetValue(claim.Type, out value) || user.TryGetValue(shortClaimTypeName, out value); - - // if a same claim exists (matching both type and value) both in id_token identity and userinfo response, remove the json entry from the userinfo response - if (isClaimIncluded && claim.Value.Equals(value.ToString(), StringComparison.Ordinal)) - { - if (!user.Remove(claim.Type)) - { - user.Remove(shortClaimTypeName); - } - } - } + var identity = (ClaimsIdentity)principal.Identity; - // adding remaining unique claims from userinfo endpoint to the identity - foreach (var pair in user) + foreach (var action in Options.ClaimActions) { - JToken value; - var claimValue = user.TryGetValue(pair.Key, out value) ? value.ToString() : null; - identity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, jwt.Issuer)); + action.Run(user, identity, ClaimsIssuer); } - return AuthenticateResult.Success(ticket); + return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name)); } /// @@ -720,10 +888,9 @@ private void SaveTokens(AuthenticationProperties properties, OpenIdConnectMessag if (!string.IsNullOrEmpty(message.ExpiresIn)) { - int value; - if (int.TryParse(message.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) + if (int.TryParse(message.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value)) { - var expiresAt = Options.SystemClock.UtcNow + TimeSpan.FromSeconds(value); + var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value); // https://www.w3.org/TR/xmlschema-2/#dateTime // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx tokens.Add(new AuthenticationToken { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) }); @@ -746,15 +913,12 @@ private void WriteNonceCookie(string nonce) throw new ArgumentNullException(nameof(nonce)); } + var cookieOptions = Options.NonceCookie.Build(Context, Clock.UtcNow); + Response.Cookies.Append( - OpenIdConnectDefaults.CookieNoncePrefix + Options.StringDataFormat.Protect(nonce), + Options.NonceCookie.Name + Options.StringDataFormat.Protect(nonce), NonceProperty, - new CookieOptions - { - HttpOnly = true, - Secure = Request.IsHttps, - Expires = Options.SystemClock.UtcNow.Add(Options.ProtocolValidator.NonceLifetime) - }); + cookieOptions); } /// @@ -762,7 +926,7 @@ private void WriteNonceCookie(string nonce) /// /// the nonce that we are looking for. /// echos 'nonce' if a cookie is found that matches, null otherwise. - /// Examine of that start with the prefix: 'OpenIdConnectAuthenticationDefaults.Nonce'. + /// Examine of that start with the prefix: 'OpenIdConnectAuthenticationDefaults.Nonce'. /// of is used to obtain the actual 'nonce'. If the nonce is found, then of is called. private string ReadNonceCookie(string nonce) { @@ -773,19 +937,14 @@ private string ReadNonceCookie(string nonce) foreach (var nonceKey in Request.Cookies.Keys) { - if (nonceKey.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix)) + if (nonceKey.StartsWith(Options.NonceCookie.Name)) { try { - var nonceDecodedValue = Options.StringDataFormat.Unprotect(nonceKey.Substring(OpenIdConnectDefaults.CookieNoncePrefix.Length, nonceKey.Length - OpenIdConnectDefaults.CookieNoncePrefix.Length)); + var nonceDecodedValue = Options.StringDataFormat.Unprotect(nonceKey.Substring(Options.NonceCookie.Name.Length, nonceKey.Length - Options.NonceCookie.Name.Length)); if (nonceDecodedValue == nonce) { - var cookieOptions = new CookieOptions - { - HttpOnly = true, - Secure = Request.IsHttps - }; - + var cookieOptions = Options.NonceCookie.Build(Context, Clock.UtcNow); Response.Cookies.Delete(nonceKey, cookieOptions); return nonce; } @@ -833,51 +992,54 @@ private AuthenticationProperties GetPropertiesFromState(string state) private async Task RunMessageReceivedEventAsync(OpenIdConnectMessage message, AuthenticationProperties properties) { Logger.MessageReceived(message.BuildRedirectUrl()); - var messageReceivedContext = new MessageReceivedContext(Context, Options) + var context = new MessageReceivedContext(Context, Scheme, Options, properties) { ProtocolMessage = message, - Properties = properties, }; - await Options.Events.MessageReceived(messageReceivedContext); - if (messageReceivedContext.HandledResponse) + await Events.MessageReceived(context); + if (context.Result != null) { - Logger.MessageReceivedContextHandledResponse(); - } - else if (messageReceivedContext.Skipped) - { - Logger.MessageReceivedContextSkipped(); + if (context.Result.Handled) + { + Logger.MessageReceivedContextHandledResponse(); + } + else if (context.Result.Skipped) + { + Logger.MessageReceivedContextSkipped(); + } } - return messageReceivedContext; + return context; } - private async Task RunTokenValidatedEventAsync(OpenIdConnectMessage authorizationResponse, OpenIdConnectMessage tokenEndpointResponse, AuthenticationProperties properties, AuthenticationTicket ticket, JwtSecurityToken jwt, string nonce) + private async Task RunTokenValidatedEventAsync(OpenIdConnectMessage authorizationResponse, OpenIdConnectMessage tokenEndpointResponse, ClaimsPrincipal user, AuthenticationProperties properties, JwtSecurityToken jwt, string nonce) { - var tokenValidatedContext = new TokenValidatedContext(Context, Options) + var context = new TokenValidatedContext(Context, Scheme, Options, user, properties) { ProtocolMessage = authorizationResponse, TokenEndpointResponse = tokenEndpointResponse, - Properties = properties, - Ticket = ticket, SecurityToken = jwt, Nonce = nonce, }; - await Options.Events.TokenValidated(tokenValidatedContext); - if (tokenValidatedContext.HandledResponse) - { - Logger.TokenValidatedHandledResponse(); - } - else if (tokenValidatedContext.Skipped) + await Events.TokenValidated(context); + if (context.Result != null) { - Logger.TokenValidatedSkipped(); + if (context.Result.Handled) + { + Logger.TokenValidatedHandledResponse(); + } + else if (context.Result.Skipped) + { + Logger.TokenValidatedSkipped(); + } } - return tokenValidatedContext; + return context; } - private async Task RunAuthorizationCodeReceivedEventAsync(OpenIdConnectMessage authorizationResponse, AuthenticationProperties properties, AuthenticationTicket ticket, JwtSecurityToken jwt) + private async Task RunAuthorizationCodeReceivedEventAsync(OpenIdConnectMessage authorizationResponse, ClaimsPrincipal user, AuthenticationProperties properties, JwtSecurityToken jwt) { Logger.AuthorizationCodeReceived(); @@ -887,100 +1049,116 @@ private async Task RunAuthorizationCodeReceive ClientSecret = Options.ClientSecret, Code = authorizationResponse.Code, GrantType = OpenIdConnectGrantTypes.AuthorizationCode, + EnableTelemetryParameters = !Options.DisableTelemetry, RedirectUri = properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey] }; - var authorizationCodeReceivedContext = new AuthorizationCodeReceivedContext(Context, Options) + var context = new AuthorizationCodeReceivedContext(Context, Scheme, Options, properties) { ProtocolMessage = authorizationResponse, - Properties = properties, TokenEndpointRequest = tokenEndpointRequest, - Ticket = ticket, + Principal = user, JwtSecurityToken = jwt, - Backchannel = Backchannel, + Backchannel = Backchannel }; - await Options.Events.AuthorizationCodeReceived(authorizationCodeReceivedContext); - if (authorizationCodeReceivedContext.HandledResponse) - { - Logger.AuthorizationCodeReceivedContextHandledResponse(); - } - else if (authorizationCodeReceivedContext.Skipped) + await Events.AuthorizationCodeReceived(context); + if (context.Result != null) { - Logger.AuthorizationCodeReceivedContextSkipped(); + if (context.Result.Handled) + { + Logger.AuthorizationCodeReceivedContextHandledResponse(); + } + else if (context.Result.Skipped) + { + Logger.AuthorizationCodeReceivedContextSkipped(); + } } - return authorizationCodeReceivedContext; + return context; } - private async Task RunTokenResponseReceivedEventAsync(OpenIdConnectMessage message, OpenIdConnectMessage tokenEndpointResponse, AuthenticationProperties properties) + private async Task RunTokenResponseReceivedEventAsync( + OpenIdConnectMessage message, + OpenIdConnectMessage tokenEndpointResponse, + ClaimsPrincipal user, + AuthenticationProperties properties) { Logger.TokenResponseReceived(); - var eventContext = new TokenResponseReceivedContext(Context, Options, properties) + var context = new TokenResponseReceivedContext(Context, Scheme, Options, user, properties) { ProtocolMessage = message, - TokenEndpointResponse = tokenEndpointResponse + TokenEndpointResponse = tokenEndpointResponse, }; - await Options.Events.TokenResponseReceived(eventContext); - if (eventContext.HandledResponse) + await Events.TokenResponseReceived(context); + if (context.Result != null) { - Logger.TokenResponseReceivedHandledResponse(); - } - else if (eventContext.Skipped) - { - Logger.TokenResponseReceivedSkipped(); + if (context.Result.Handled) + { + Logger.TokenResponseReceivedHandledResponse(); + } + else if (context.Result.Skipped) + { + Logger.TokenResponseReceivedSkipped(); + } } - return eventContext; + return context; } - private async Task RunUserInformationReceivedEventAsync(AuthenticationTicket ticket, OpenIdConnectMessage message, JObject user) + private async Task RunUserInformationReceivedEventAsync(ClaimsPrincipal principal, AuthenticationProperties properties, OpenIdConnectMessage message, JObject user) { Logger.UserInformationReceived(user.ToString()); - var userInformationReceivedContext = new UserInformationReceivedContext(Context, Options) + var context = new UserInformationReceivedContext(Context, Scheme, Options, principal, properties) { - Ticket = ticket, ProtocolMessage = message, User = user, }; - await Options.Events.UserInformationReceived(userInformationReceivedContext); - if (userInformationReceivedContext.HandledResponse) - { - Logger.UserInformationReceivedHandledResponse(); - } - else if (userInformationReceivedContext.Skipped) + await Events.UserInformationReceived(context); + if (context.Result != null) { - Logger.UserInformationReceivedSkipped(); + if (context.Result.Handled) + { + Logger.UserInformationReceivedHandledResponse(); + } + else if (context.Result.Skipped) + { + Logger.UserInformationReceivedSkipped(); + } } - return userInformationReceivedContext; + return context; } private async Task RunAuthenticationFailedEventAsync(OpenIdConnectMessage message, Exception exception) { - var authenticationFailedContext = new AuthenticationFailedContext(Context, Options) + var context = new AuthenticationFailedContext(Context, Scheme, Options) { ProtocolMessage = message, Exception = exception }; - await Options.Events.AuthenticationFailed(authenticationFailedContext); - if (authenticationFailedContext.HandledResponse) - { - Logger.AuthenticationFailedContextHandledResponse(); - } - else if (authenticationFailedContext.Skipped) + await Events.AuthenticationFailed(context); + if (context.Result != null) { - Logger.AuthenticationFailedContextSkipped(); + if (context.Result.Handled) + { + Logger.AuthenticationFailedContextHandledResponse(); + } + else if (context.Result.Skipped) + { + Logger.AuthenticationFailedContextSkipped(); + } } - return authenticationFailedContext; + return context; } - private AuthenticationTicket ValidateToken(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters, out JwtSecurityToken jwt) + // Note this modifies properties if Options.UseTokenLifetime + private ClaimsPrincipal ValidateToken(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters, out JwtSecurityToken jwt) { if (!Options.SecurityTokenValidator.CanReadToken(idToken)) { @@ -990,20 +1168,14 @@ private AuthenticationTicket ValidateToken(string idToken, AuthenticationPropert if (_configuration != null) { - if (string.IsNullOrEmpty(validationParameters.ValidIssuer)) - { - validationParameters.ValidIssuer = _configuration.Issuer; - } - else if (!string.IsNullOrEmpty(_configuration.Issuer)) - { - validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(new[] { _configuration.Issuer }) ?? new[] { _configuration.Issuer }; - } + var issuer = new[] { _configuration.Issuer }; + validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuer) ?? issuer; - validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys; + validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) + ?? _configuration.SigningKeys; } - SecurityToken validatedToken = null; - var principal = Options.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out validatedToken); + var principal = Options.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out SecurityToken validatedToken); jwt = validatedToken as JwtSecurityToken; if (jwt == null) { @@ -1017,24 +1189,62 @@ private AuthenticationTicket ValidateToken(string idToken, AuthenticationPropert throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken)); } - var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); - if (Options.UseTokenLifetime) { var issued = validatedToken.ValidFrom; if (issued != DateTime.MinValue) { - ticket.Properties.IssuedUtc = issued; + properties.IssuedUtc = issued; } var expires = validatedToken.ValidTo; if (expires != DateTime.MinValue) { - ticket.Properties.ExpiresUtc = expires; + properties.ExpiresUtc = expires; } } - return ticket; + return principal; + } + + /// + /// Build a redirect path if the given path is a relative path. + /// + private string BuildRedirectUriIfRelative(string uri) + { + if (string.IsNullOrEmpty(uri)) + { + return uri; + } + + if (!uri.StartsWith("/", StringComparison.Ordinal)) + { + return uri; + } + + return BuildRedirectUri(uri); + } + + private OpenIdConnectProtocolException CreateOpenIdConnectProtocolException(OpenIdConnectMessage message, HttpResponseMessage response) + { + var description = message.ErrorDescription ?? "error_description is null"; + var errorUri = message.ErrorUri ?? "error_uri is null"; + + if (response != null) + { + Logger.ResponseErrorWithStatusCode(message.Error, description, errorUri, (int)response.StatusCode); + } + else + { + Logger.ResponseError(message.Error, description, errorUri); + } + + return new OpenIdConnectProtocolException(string.Format( + CultureInfo.InvariantCulture, + Resources.MessageContainsError, + message.Error, + description, + errorUri)); } } } diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectMiddleware.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectMiddleware.cs deleted file mode 100644 index 80f2ca2ba..000000000 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectMiddleware.cs +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Net.Http; -using System.Text; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Protocols; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; - -namespace Microsoft.AspNetCore.Authentication.OpenIdConnect -{ - /// - /// ASP.NET Core middleware for obtaining identities using OpenIdConnect protocol. - /// - public class OpenIdConnectMiddleware : AuthenticationMiddleware - { - /// - /// Initializes a - /// - /// The next middleware in the middleware pipeline to invoke. - /// provider for creating a data protector. - /// factory for creating a . - /// - /// - /// - /// - /// The . - public OpenIdConnectMiddleware( - RequestDelegate next, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - UrlEncoder encoder, - IServiceProvider services, - IOptions sharedOptions, - IOptions options, - HtmlEncoder htmlEncoder) - : base(next, options, loggerFactory, encoder) - { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - - if (dataProtectionProvider == null) - { - throw new ArgumentNullException(nameof(dataProtectionProvider)); - } - - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - if (sharedOptions == null) - { - throw new ArgumentNullException(nameof(sharedOptions)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (htmlEncoder == null) - { - throw new ArgumentNullException(nameof(htmlEncoder)); - } - - if (!Options.CallbackPath.HasValue) - { - throw new ArgumentException("Options.CallbackPath must be provided."); - } - - if (string.IsNullOrEmpty(Options.SignInScheme)) - { - Options.SignInScheme = sharedOptions.Value.SignInScheme; - } - if (string.IsNullOrEmpty(Options.SignInScheme)) - { - throw new ArgumentException("Options.SignInScheme is required."); - } - - HtmlEncoder = htmlEncoder; - - if (Options.StateDataFormat == null) - { - var dataProtector = dataProtectionProvider.CreateProtector( - typeof(OpenIdConnectMiddleware).FullName, - typeof(string).FullName, - Options.AuthenticationScheme, - "v1"); - - Options.StateDataFormat = new PropertiesDataFormat(dataProtector); - } - - if (Options.StringDataFormat == null) - { - var dataProtector = dataProtectionProvider.CreateProtector( - typeof(OpenIdConnectMiddleware).FullName, - typeof(string).FullName, - Options.AuthenticationScheme, - "v1"); - - Options.StringDataFormat = new SecureDataFormat(new StringSerializer(), dataProtector); - } - - - if (Options.Events == null) - { - Options.Events = new OpenIdConnectEvents(); - } - - if (string.IsNullOrEmpty(Options.TokenValidationParameters.ValidAudience) && !string.IsNullOrEmpty(Options.ClientId)) - { - Options.TokenValidationParameters.ValidAudience = Options.ClientId; - } - - Backchannel = new HttpClient(Options.BackchannelHttpHandler ?? new HttpClientHandler()); - Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OpenIdConnect middleware"); - Backchannel.Timeout = Options.BackchannelTimeout; - Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB - - if (Options.ConfigurationManager == null) - { - if (Options.Configuration != null) - { - Options.ConfigurationManager = new StaticConfigurationManager(Options.Configuration); - } - else if (!(string.IsNullOrEmpty(Options.MetadataAddress) && string.IsNullOrEmpty(Options.Authority))) - { - if (string.IsNullOrEmpty(Options.MetadataAddress) && !string.IsNullOrEmpty(Options.Authority)) - { - Options.MetadataAddress = Options.Authority; - if (!Options.MetadataAddress.EndsWith("/", StringComparison.Ordinal)) - { - Options.MetadataAddress += "/"; - } - - Options.MetadataAddress += ".well-known/openid-configuration"; - } - - if (Options.RequireHttpsMetadata && !Options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false."); - } - - Options.ConfigurationManager = new ConfigurationManager(Options.MetadataAddress, new OpenIdConnectConfigurationRetriever(), - new HttpDocumentRetriever(Backchannel) { RequireHttps = Options.RequireHttpsMetadata }); - } - } - } - - protected HttpClient Backchannel { get; private set; } - - protected HtmlEncoder HtmlEncoder { get; private set; } - - /// - /// Provides the object for processing authentication-related requests. - /// - /// An configured with the supplied to the constructor. - protected override AuthenticationHandler CreateHandler() - { - return new OpenIdConnectHandler(Backchannel, HtmlEncoder); - } - - private class StringSerializer : IDataSerializer - { - public string Deserialize(byte[] data) - { - return Encoding.UTF8.GetString(data); - } - - public byte[] Serialize(string model) - { - return Encoding.UTF8.GetBytes(model); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs index acf9d77e4..cbf6e8eab 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs @@ -3,30 +3,22 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.IdentityModel.Tokens.Jwt; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Authentication.Internal; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect { /// - /// Configuration options for + /// Configuration options for /// public class OpenIdConnectOptions : RemoteAuthenticationOptions { - /// - /// Initializes a new - /// - public OpenIdConnectOptions() - : this(OpenIdConnectDefaults.AuthenticationScheme) - { - } + private CookieBuilder _nonceCookieBuilder; /// /// Initializes a new @@ -35,24 +27,84 @@ public OpenIdConnectOptions() /// Defaults: /// AddNonceToRequest: true. /// BackchannelTimeout: 1 minute. - /// Caption: . /// ProtocolValidator: new . /// RefreshOnIssuerKeyNotFound: true - /// ResponseType: - /// Scope: . + /// ResponseType: + /// Scope: . /// TokenValidationParameters: new with AuthenticationScheme = authenticationScheme. /// UseTokenLifetime: false. /// - /// will be used to when creating the for the AuthenticationScheme property. - public OpenIdConnectOptions(string authenticationScheme) + public OpenIdConnectOptions() { - AuthenticationScheme = authenticationScheme; - AutomaticChallenge = true; - DisplayName = OpenIdConnectDefaults.Caption; CallbackPath = new PathString("/signin-oidc"); + SignedOutCallbackPath = new PathString("/signout-callback-oidc"); + RemoteSignOutPath = new PathString("/signout-oidc"); + Events = new OpenIdConnectEvents(); Scope.Add("openid"); Scope.Add("profile"); + + ClaimActions.DeleteClaim("nonce"); + ClaimActions.DeleteClaim("aud"); + ClaimActions.DeleteClaim("azp"); + ClaimActions.DeleteClaim("acr"); + ClaimActions.DeleteClaim("amr"); + ClaimActions.DeleteClaim("iss"); + ClaimActions.DeleteClaim("iat"); + ClaimActions.DeleteClaim("nbf"); + ClaimActions.DeleteClaim("exp"); + ClaimActions.DeleteClaim("at_hash"); + ClaimActions.DeleteClaim("c_hash"); + ClaimActions.DeleteClaim("auth_time"); + ClaimActions.DeleteClaim("ipaddr"); + ClaimActions.DeleteClaim("platf"); + ClaimActions.DeleteClaim("ver"); + + // http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + ClaimActions.MapUniqueJsonKey("sub", "sub"); + ClaimActions.MapUniqueJsonKey("name", "name"); + ClaimActions.MapUniqueJsonKey("given_name", "given_name"); + ClaimActions.MapUniqueJsonKey("family_name", "family_name"); + ClaimActions.MapUniqueJsonKey("profile", "profile"); + ClaimActions.MapUniqueJsonKey("email", "email"); + + _nonceCookieBuilder = new OpenIdConnectNonceCookieBuilder(this) + { + Name = OpenIdConnectDefaults.CookieNoncePrefix, + HttpOnly = true, + SameSite = SameSiteMode.None, + SecurePolicy = CookieSecurePolicy.SameAsRequest, + IsEssential = true, + }; + } + + /// + /// Check that the options are valid. Should throw an exception if things are not ok. + /// + public override void Validate() + { + base.Validate(); + + if (MaxAge.HasValue && MaxAge.Value < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(MaxAge), MaxAge.Value, "The value must not be a negative TimeSpan."); + } + + if (string.IsNullOrEmpty(ClientId)) + { + throw new ArgumentException("Options.ClientId must be provided", nameof(ClientId)); + } + + if (!CallbackPath.HasValue) + { + throw new ArgumentException("Options.CallbackPath must be provided.", nameof(CallbackPath)); + } + + if (ConfigurationManager == null) + { + throw new InvalidOperationException($"Provide {nameof(Authority)}, {nameof(MetadataAddress)}, " + + $"{nameof(Configuration)}, or {nameof(ConfigurationManager)} to {nameof(OpenIdConnectOptions)}"); + } } /// @@ -83,10 +135,16 @@ public OpenIdConnectOptions(string authenticationScheme) public IConfigurationManager ConfigurationManager { get; set; } /// - /// Boolean to set whether the middleware should go to user info endpoint to retrieve additional claims or not after creating an identity from id_token received from token endpoint. + /// Boolean to set whether the handler should go to user info endpoint to retrieve additional claims or not after creating an identity from id_token received from token endpoint. + /// The default is 'false'. /// public bool GetClaimsFromUserInfoEndpoint { get; set; } + /// + /// A collection of claim actions used to select values from the json user data and create Claims. + /// + public ClaimActionCollection ClaimActions { get; } = new ClaimActionCollection(); + /// /// Gets or sets if HTTPS is required for the metadata address or authority. /// The default is true. This should be disabled only in development environments. @@ -99,17 +157,24 @@ public OpenIdConnectOptions(string authenticationScheme) public string MetadataAddress { get; set; } /// - /// Gets or sets the to notify when processing OpenIdConnect messages. + /// Gets or sets the to notify when processing OpenIdConnect messages. /// - public new IOpenIdConnectEvents Events + public new OpenIdConnectEvents Events { - get { return (IOpenIdConnectEvents)base.Events; } - set { base.Events = value; } + get => (OpenIdConnectEvents)base.Events; + set => base.Events = value; } + /// + /// Gets or sets the 'max_age'. If set the 'max_age' parameter will be sent with the authentication request. If the identity + /// provider has not actively authenticated the user within the length of time specified, the user will be prompted to + /// re-authenticate. By default no max_age is specified. + /// + public TimeSpan? MaxAge { get; set; } + /// /// Gets or sets the that is used to ensure that the 'id_token' received - /// is valid per: http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + /// is valid per: http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation /// /// if 'value' is null. public OpenIdConnectProtocolValidator ProtocolValidator { get; set; } = new OpenIdConnectProtocolValidator() @@ -119,10 +184,17 @@ public OpenIdConnectOptions(string authenticationScheme) }; /// - /// Gets or sets the 'post_logout_redirect_uri' + /// The request path within the application's base path where the user agent will be returned after sign out from the identity provider. + /// See post_logout_redirect_uri from http://openid.net/specs/openid-connect-session-1_0.html#RedirectionAfterLogout. /// - /// This is sent to the OP as the redirect for the user-agent. - public string PostLogoutRedirectUri { get; set; } + public PathString SignedOutCallbackPath { get; set; } + + /// + /// The uri where the user agent will be redirected to after application is signed out from the identity provider. + /// The redirect will happen after the SignedOutCallbackPath is invoked. + /// + /// This URI can be out of the application's domain. By default it points to the root. + public string SignedOutRedirectUri { get; set; } = "/"; /// /// Gets or sets if a metadata refresh should be attempted after a SecurityTokenSignatureKeyNotFoundException. This allows for automatic @@ -143,12 +215,17 @@ public OpenIdConnectOptions(string authenticationScheme) /// /// Gets or sets the 'response_mode'. /// - public string ResponseMode { get; set; } = OpenIdConnectResponseModes.FormPost; + public string ResponseMode { get; set; } = OpenIdConnectResponseMode.FormPost; /// /// Gets or sets the 'response_type'. /// - public string ResponseType { get; set; } = OpenIdConnectResponseTypes.CodeIdToken; + public string ResponseType { get; set; } = OpenIdConnectResponseType.IdToken; + + /// + /// Gets or sets the 'prompt'. + /// + public string Prompt { get; set; } /// /// Gets the list of permissions to request. @@ -156,12 +233,23 @@ public OpenIdConnectOptions(string authenticationScheme) public ICollection Scope { get; } = new HashSet(); /// - /// Gets or sets the type used to secure data handled by the middleware. + /// Requests received on this path will cause the handler to invoke SignOut using the SignInScheme. + /// + public PathString RemoteSignOutPath { get; set; } + + /// + /// The Authentication Scheme to use with SignOut on the SignOutPath. SignInScheme will be used if this + /// is not set. + /// + public string SignOutScheme { get; set; } + + /// + /// Gets or sets the type used to secure data handled by the handler. /// public ISecureDataFormat StateDataFormat { get; set; } /// - /// Gets or sets the type used to secure strings used by the middleware. + /// Gets or sets the type used to secure strings used by the handler. /// public ISecureDataFormat StringDataFormat { get; set; } @@ -184,7 +272,7 @@ public OpenIdConnectOptions(string authenticationScheme) public bool UseTokenLifetime { get; set; } /// - /// Indicates if requests to the CallbackPath may also be for other components. If enabled the middleware will pass + /// Indicates if requests to the CallbackPath may also be for other components. If enabled the handler will pass /// requests through that do not contain OpenIdConnect authentication responses. Disabling this and setting the /// CallbackPath to a dedicated endpoint may provide better error handling. /// This is disabled by default. @@ -192,9 +280,47 @@ public OpenIdConnectOptions(string authenticationScheme) public bool SkipUnrecognizedRequests { get; set; } = false; /// - /// For testing purposes only. + /// Indicates whether telemetry should be disabled. When this feature is enabled, + /// the assembly version of the Microsoft IdentityModel packages is sent to the + /// remote OpenID Connect provider as an authorization/logout request parameter. + /// + public bool DisableTelemetry { get; set; } + + /// + /// Determines the settings used to create the nonce cookie before the + /// cookie gets added to the response. /// - [EditorBrowsable(EditorBrowsableState.Never)] - public ISystemClock SystemClock { get; set; } = new SystemClock(); + /// + /// The value of is treated as the prefix to the cookie name, and defaults to . + /// + public CookieBuilder NonceCookie + { + get => _nonceCookieBuilder; + set => _nonceCookieBuilder = value ?? throw new ArgumentNullException(nameof(value)); + } + + private class OpenIdConnectNonceCookieBuilder : RequestPathBaseCookieBuilder + { + private readonly OpenIdConnectOptions _options; + + public OpenIdConnectNonceCookieBuilder(OpenIdConnectOptions oidcOptions) + { + _options = oidcOptions; + } + + protected override string AdditionalPath => _options.CallbackPath; + + public override CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom) + { + var cookieOptions = base.Build(context, expiresFrom); + + if (!Expiration.HasValue || !cookieOptions.Expires.HasValue) + { + cookieOptions.Expires = expiresFrom.Add(_options.ProtocolValidator.NonceLifetime); + } + + return cookieOptions; + } + } } } diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectPostConfigureOptions.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectPostConfigureOptions.cs new file mode 100644 index 000000000..b79f1d1ed --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectPostConfigureOptions.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using System.Text; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect +{ + /// + /// Used to setup defaults for all . + /// + public class OpenIdConnectPostConfigureOptions : IPostConfigureOptions + { + private readonly IDataProtectionProvider _dp; + + public OpenIdConnectPostConfigureOptions(IDataProtectionProvider dataProtection) + { + _dp = dataProtection; + } + + /// + /// Invoked to post configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configure. + public void PostConfigure(string name, OpenIdConnectOptions options) + { + options.DataProtectionProvider = options.DataProtectionProvider ?? _dp; + + if (string.IsNullOrEmpty(options.SignOutScheme)) + { + options.SignOutScheme = options.SignInScheme; + } + + if (options.StateDataFormat == null) + { + var dataProtector = options.DataProtectionProvider.CreateProtector( + typeof(OpenIdConnectHandler).FullName, name, "v1"); + options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + if (options.StringDataFormat == null) + { + var dataProtector = options.DataProtectionProvider.CreateProtector( + typeof(OpenIdConnectHandler).FullName, + typeof(string).FullName, + name, + "v1"); + + options.StringDataFormat = new SecureDataFormat(new StringSerializer(), dataProtector); + } + + if (string.IsNullOrEmpty(options.TokenValidationParameters.ValidAudience) && !string.IsNullOrEmpty(options.ClientId)) + { + options.TokenValidationParameters.ValidAudience = options.ClientId; + } + + if (options.Backchannel == null) + { + options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler()); + options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OpenIdConnect handler"); + options.Backchannel.Timeout = options.BackchannelTimeout; + options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + } + + if (options.ConfigurationManager == null) + { + if (options.Configuration != null) + { + options.ConfigurationManager = new StaticConfigurationManager(options.Configuration); + } + else if (!(string.IsNullOrEmpty(options.MetadataAddress) && string.IsNullOrEmpty(options.Authority))) + { + if (string.IsNullOrEmpty(options.MetadataAddress) && !string.IsNullOrEmpty(options.Authority)) + { + options.MetadataAddress = options.Authority; + if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal)) + { + options.MetadataAddress += "/"; + } + + options.MetadataAddress += ".well-known/openid-configuration"; + } + + if (options.RequireHttpsMetadata && !options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false."); + } + + options.ConfigurationManager = new ConfigurationManager(options.MetadataAddress, new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever(options.Backchannel) { RequireHttps = options.RequireHttpsMetadata }); + } + } + } + + private class StringSerializer : IDataSerializer + { + public string Deserialize(byte[] data) + { + return Encoding.UTF8.GetString(data); + } + + public byte[] Serialize(string model) + { + return Encoding.UTF8.GetBytes(model); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectRedirectBehavior .cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectRedirectBehavior.cs similarity index 100% rename from src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectRedirectBehavior .cs rename to src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectRedirectBehavior.cs diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/AssemblyInfo.cs deleted file mode 100644 index 76feceeff..000000000 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Reflection; -using System.Resources; - -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: AssemblyCompany("Microsoft Corporation.")] -[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/Resources.Designer.cs index 65a9273a0..753373ece 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Properties/Resources.Designer.cs @@ -15,96 +15,84 @@ private static readonly ResourceManager _resourceManager /// internal static string MessageStateIsNullOrEmpty { - get { return GetString("MessageStateIsNullOrEmpty"); } + get => GetString("MessageStateIsNullOrEmpty"); } /// /// OpenIdConnectAuthenticationHandler: message.State is null or empty. /// internal static string FormatMessageStateIsNullOrEmpty() - { - return GetString("MessageStateIsNullOrEmpty"); - } + => GetString("MessageStateIsNullOrEmpty"); /// /// Unable to unprotect the message.State. /// internal static string MessageStateIsInvalid { - get { return GetString("MessageStateIsInvalid"); } + get => GetString("MessageStateIsInvalid"); } /// /// Unable to unprotect the message.State. /// internal static string FormatMessageStateIsInvalid() - { - return GetString("MessageStateIsInvalid"); - } + => GetString("MessageStateIsInvalid"); /// /// Message contains error: '{0}', error_description: '{1}', error_uri: '{2}'. /// internal static string MessageContainsError { - get { return GetString("MessageContainsError"); } + get => GetString("MessageContainsError"); } /// /// Message contains error: '{0}', error_description: '{1}', error_uri: '{2}'. /// internal static string FormatMessageContainsError(object p0, object p1, object p2) - { - return string.Format(CultureInfo.CurrentCulture, GetString("MessageContainsError"), p0, p1, p2); - } + => string.Format(CultureInfo.CurrentCulture, GetString("MessageContainsError"), p0, p1, p2); /// /// The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{0}'. /// internal static string ValidatedSecurityTokenNotJwt { - get { return GetString("ValidatedSecurityTokenNotJwt"); } + get => GetString("ValidatedSecurityTokenNotJwt"); } /// /// The Validated Security Token must be of type JwtSecurityToken, but instead its type is: '{0}'. /// internal static string FormatValidatedSecurityTokenNotJwt(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("ValidatedSecurityTokenNotJwt"), p0); - } + => string.Format(CultureInfo.CurrentCulture, GetString("ValidatedSecurityTokenNotJwt"), p0); /// /// Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{0}'." /// internal static string UnableToValidateToken { - get { return GetString("UnableToValidateToken"); } + get => GetString("UnableToValidateToken"); } /// /// Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: '{0}'." /// internal static string FormatUnableToValidateToken(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("UnableToValidateToken"), p0); - } + => string.Format(CultureInfo.CurrentCulture, GetString("UnableToValidateToken"), p0); /// /// Cannot process the message. Both id_token and code are missing. /// internal static string IdTokenCodeMissing { - get { return GetString("IdTokenCodeMissing"); } + get => GetString("IdTokenCodeMissing"); } /// /// Cannot process the message. Both id_token and code are missing. /// internal static string FormatIdTokenCodeMissing() - { - return GetString("IdTokenCodeMissing"); - } + => GetString("IdTokenCodeMissing"); private static string GetString(string name, params string[] formatterNames) { diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json new file mode 100644 index 000000000..d5c10d18d --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json @@ -0,0 +1,2452 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.OpenIdConnect, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.OpenIdConnectExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddOpenIdConnect", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddOpenIdConnect", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddOpenIdConnect", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddOpenIdConnect", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.OpenIdConnectAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseOpenIdConnectAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseOpenIdConnectAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.ClaimActionCollectionUniqueExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "MapUniqueJsonKey", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MapUniqueJsonKey", + "Parameters": [ + { + "Name": "collection", + "Type": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection" + }, + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthenticationFailedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Exception", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Exception", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthorizationCodeReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_JwtSecurityToken", + "Parameters": [], + "ReturnType": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_JwtSecurityToken", + "Parameters": [ + { + "Name": "value", + "Type": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenEndpointRequest", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenEndpointRequest", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Backchannel", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpClient", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenEndpointResponse", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenEndpointResponse", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HandledCodeRedemption", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleCodeRedemption", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleCodeRedemption", + "Parameters": [ + { + "Name": "accessToken", + "Type": "System.String" + }, + { + "Name": "idToken", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleCodeRedemption", + "Parameters": [ + { + "Name": "tokenEndpointResponse", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.MessageReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Token", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Token", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnAuthenticationFailed", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnAuthenticationFailed", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnAuthorizationCodeReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnAuthorizationCodeReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnMessageReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnMessageReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToIdentityProvider", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToIdentityProvider", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToIdentityProviderForSignOut", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToIdentityProviderForSignOut", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnSignedOutCallbackRedirect", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnSignedOutCallbackRedirect", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRemoteSignOut", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRemoteSignOut", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnTokenResponseReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnTokenResponseReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnTokenValidated", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnTokenValidated", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnUserInformationReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnUserInformationReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticationFailed", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthenticationFailedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizationCodeReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthorizationCodeReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MessageReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.MessageReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToIdentityProvider", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToIdentityProviderForSignOut", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignedOutCallbackRedirect", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RemoteSignOutContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RemoteSignOut", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RemoteSignOutContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TokenResponseReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenResponseReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TokenValidated", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UserInformationReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.UserInformationReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RedirectContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Handled", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleResponse", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.RemoteSignOutContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "message", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenResponseReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenEndpointResponse", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenEndpointResponse", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SecurityToken", + "Parameters": [], + "ReturnType": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SecurityToken", + "Parameters": [ + { + "Name": "value", + "Type": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenEndpointResponse", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenEndpointResponse", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Nonce", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Nonce", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.UserInformationReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "Newtonsoft.Json.Linq.JObject", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_User", + "Parameters": [ + { + "Name": "value", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectChallengeProperties", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.OAuthChallengeProperties", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_MaxAge", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Prompt", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Prompt", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "items", + "Type": "System.Collections.Generic.IDictionary" + }, + { + "Name": "parameters", + "Type": "System.Collections.Generic.IDictionary" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "MaxAgeKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "PromptKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "AuthenticationPropertiesKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "CookieNoncePrefix", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "RedirectUriForCodePropertiesKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "UserstatePropertiesKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"OpenIdConnect\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleRequestAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Backchannel", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpClient", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HtmlEncoder", + "Parameters": [], + "ReturnType": "System.Text.Encodings.Web.HtmlEncoder", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteSignOutAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignOutCallbackAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedeemAuthorizationCodeAsync", + "Parameters": [ + { + "Name": "tokenEndpointRequest", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetUserInformationAsync", + "Parameters": [ + { + "Name": "message", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage" + }, + { + "Name": "jwt", + "Type": "System.IdentityModel.Tokens.Jwt.JwtSecurityToken" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "htmlEncoder", + "Type": "System.Text.Encodings.Web.HtmlEncoder" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Authority", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Authority", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClientId", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientId", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClientSecret", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClientSecret", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Configuration", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Configuration", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConfigurationManager", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.IConfigurationManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConfigurationManager", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.IConfigurationManager" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_GetClaimsFromUserInfoEndpoint", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_GetClaimsFromUserInfoEndpoint", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClaimActions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequireHttpsMetadata", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequireHttpsMetadata", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MetadataAddress", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MetadataAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaxAge", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaxAge", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ProtocolValidator", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolValidator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolValidator", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolValidator" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SignedOutCallbackPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SignedOutCallbackPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SignedOutRedirectUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SignedOutRedirectUri", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RefreshOnIssuerKeyNotFound", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RefreshOnIssuerKeyNotFound", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthenticationMethod", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectRedirectBehavior", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthenticationMethod", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectRedirectBehavior" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Resource", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Resource", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ResponseMode", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ResponseMode", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ResponseType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ResponseType", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Prompt", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Prompt", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Scope", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemoteSignOutPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemoteSignOutPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SignOutScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SignOutScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StateDataFormat", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StateDataFormat", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StringDataFormat", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StringDataFormat", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SecurityTokenValidator", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Tokens.ISecurityTokenValidator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SecurityTokenValidator", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Tokens.ISecurityTokenValidator" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenValidationParameters", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Tokens.TokenValidationParameters", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenValidationParameters", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Tokens.TokenValidationParameters" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UseTokenLifetime", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_UseTokenLifetime", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SkipUnrecognizedRequests", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SkipUnrecognizedRequests", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DisableTelemetry", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DisableTelemetry", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_NonceCookie", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_NonceCookie", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieBuilder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectPostConfigureOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Options.IPostConfigureOptions" + ], + "Members": [ + { + "Kind": "Method", + "Name": "PostConfigure", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "dataProtection", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectRedirectBehavior", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "RedirectGet", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "FormPost", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.OpenIdConnect.Claims.UniqueJsonKeyClaimAction", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.JsonKeyClaimAction", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Run", + "Parameters": [ + { + "Name": "userData", + "Type": "Newtonsoft.Json.Linq.JObject" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "issuer", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "valueType", + "Type": "System.String" + }, + { + "Name": "jsonKey", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/project.json b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/project.json deleted file mode 100644 index 581abf9ce..000000000 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/project.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "version": "0.1.0-*", - "description": "ASP.NET Core middleware that enables an application to support the OpenID Connect authentication workflow.", - "tags": [ - "aspnetcore", - "authentication", - "security" - ], - "repository": { - "type": "git", - "url": "git://github.com/aspnet/security" - }, - "compilationOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk", - "nowarn": [ - "CS1591" - ], - "xmlDoc": true - }, - "dependencies": { - "Microsoft.AspNetCore.Authentication": "1.0.0-*", - "Microsoft.IdentityModel.Protocols.OpenIdConnect": "2.0.0-*" - }, - "frameworks": { - "net451": {}, - "netstandard1.4": { - "imports": [ - "portable-net451+win8" - ] - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/Events/BaseTwitterContext.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/Events/BaseTwitterContext.cs deleted file mode 100644 index 5f00cb18b..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/Events/BaseTwitterContext.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Authentication.Twitter -{ - /// - /// Base class for other Twitter contexts. - /// - public class BaseTwitterContext : BaseContext - { - /// - /// Initializes a - /// - /// The HTTP environment - /// The options for Twitter - public BaseTwitterContext(HttpContext context, TwitterOptions options) - : base(context) - { - Options = options; - } - - public TwitterOptions Options { get; } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/Events/ITwitterEvents.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/Events/ITwitterEvents.cs deleted file mode 100644 index 006fafc73..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/Events/ITwitterEvents.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Authentication.Twitter -{ - /// - /// Specifies callback methods which the invokes to enable developer control over the authentication process. /> - /// - public interface ITwitterEvents : IRemoteAuthenticationEvents - { - /// - /// Invoked whenever Twitter succesfully authenticates a user - /// - /// Contains information about the login session as well as the user . - /// A representing the completed operation. - Task CreatingTicket(TwitterCreatingTicketContext context); - - /// - /// Called when a Challenge causes a redirect to authorize endpoint in the Twitter middleware - /// - /// Contains redirect URI and of the challenge - Task RedirectToAuthorizationEndpoint(TwitterRedirectToAuthorizationEndpointContext context); - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs index 04a45ac6d..67f28d529 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs @@ -1,70 +1,77 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Security.Claims; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; +using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.Authentication.Twitter { /// /// Contains information about the login session as well as the user . /// - public class TwitterCreatingTicketContext : BaseTwitterContext + public class TwitterCreatingTicketContext : ResultContext { /// /// Initializes a /// /// The HTTP environment + /// The scheme data /// The options for Twitter + /// The . + /// The . /// Twitter user ID /// Twitter screen name /// Twitter access token /// Twitter access token secret + /// User details public TwitterCreatingTicketContext( HttpContext context, + AuthenticationScheme scheme, TwitterOptions options, + ClaimsPrincipal principal, + AuthenticationProperties properties, string userId, string screenName, string accessToken, - string accessTokenSecret) - : base(context, options) + string accessTokenSecret, + JObject user) + : base(context, scheme, options) { UserId = userId; ScreenName = screenName; AccessToken = accessToken; AccessTokenSecret = accessTokenSecret; + User = user ?? new JObject(); + Principal = principal; + Properties = properties; } /// /// Gets the Twitter user ID /// - public string UserId { get; private set; } + public string UserId { get; } /// /// Gets the Twitter screen name /// - public string ScreenName { get; private set; } + public string ScreenName { get; } /// /// Gets the Twitter access token /// - public string AccessToken { get; private set; } + public string AccessToken { get; } /// /// Gets the Twitter access token secret /// - public string AccessTokenSecret { get; private set; } + public string AccessTokenSecret { get; } /// - /// Gets the representing the user + /// Gets the JSON-serialized user or an empty + /// if it is not available. /// - public ClaimsPrincipal Principal { get; set; } - - /// - /// Gets or sets a property bag for common authentication properties - /// - public AuthenticationProperties Properties { get; set; } + public JObject User { get; } } } diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterEvents.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterEvents.cs index 21c5b57a7..744c48c5f 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterEvents.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterEvents.cs @@ -7,22 +7,22 @@ namespace Microsoft.AspNetCore.Authentication.Twitter { /// - /// Default implementation. + /// Default implementation. /// - public class TwitterEvents : RemoteAuthenticationEvents, ITwitterEvents + public class TwitterEvents : RemoteAuthenticationEvents { /// /// Gets or sets the function that is invoked when the Authenticated method is invoked. /// - public Func OnCreatingTicket { get; set; } = context => Task.FromResult(0); + public Func OnCreatingTicket { get; set; } = context => Task.CompletedTask; /// /// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked. /// - public Func OnRedirectToAuthorizationEndpoint { get; set; } = context => + public Func, Task> OnRedirectToAuthorizationEndpoint { get; set; } = context => { context.Response.Redirect(context.RedirectUri); - return Task.FromResult(0); + return Task.CompletedTask; }; /// @@ -33,9 +33,9 @@ public class TwitterEvents : RemoteAuthenticationEvents, ITwitterEvents public virtual Task CreatingTicket(TwitterCreatingTicketContext context) => OnCreatingTicket(context); /// - /// Called when a Challenge causes a redirect to authorize endpoint in the Twitter middleware + /// Called when a Challenge causes a redirect to authorize endpoint in the Twitter handler /// /// Contains redirect URI and of the challenge - public virtual Task RedirectToAuthorizationEndpoint(TwitterRedirectToAuthorizationEndpointContext context) => OnRedirectToAuthorizationEndpoint(context); + public virtual Task RedirectToAuthorizationEndpoint(RedirectContext context) => OnRedirectToAuthorizationEndpoint(context); } } diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterRedirectToAuthorizationEndpointContext.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterRedirectToAuthorizationEndpointContext.cs deleted file mode 100644 index aa1da43ed..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterRedirectToAuthorizationEndpointContext.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; - -namespace Microsoft.AspNetCore.Authentication.Twitter -{ - /// - /// The Context passed when a Challenge causes a redirect to authorize endpoint in the Twitter middleware. - /// - public class TwitterRedirectToAuthorizationEndpointContext : BaseTwitterContext - { - /// - /// Creates a new context object. - /// - /// The HTTP request context. - /// The Twitter middleware options. - /// The authentication properties of the challenge. - /// The initial redirect URI. - public TwitterRedirectToAuthorizationEndpointContext(HttpContext context, TwitterOptions options, - AuthenticationProperties properties, string redirectUri) - : base(context, options) - { - RedirectUri = redirectUri; - Properties = properties; - } - - /// - /// Gets the URI used for the redirect operation. - /// - public string RedirectUri { get; private set; } - - /// - /// Gets the authentication properties of the challenge. - /// - public AuthenticationProperties Properties { get; private set; } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/LoggingExtensions.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/LoggingExtensions.cs index 21a4ac541..2a2cd5da7 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/LoggingExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/LoggingExtensions.cs @@ -9,6 +9,7 @@ internal static class LoggingExtensions { private static Action _obtainRequestToken; private static Action _obtainAccessToken; + private static Action _retrieveUserDetails; static LoggingExtensions() { @@ -20,6 +21,10 @@ static LoggingExtensions() eventId: 2, logLevel: LogLevel.Debug, formatString: "ObtainAccessToken"); + _retrieveUserDetails = LoggerMessage.Define( + eventId: 3, + logLevel: LogLevel.Debug, + formatString: "RetrieveUserDetails"); } @@ -32,5 +37,10 @@ public static void ObtainRequestToken(this ILogger logger) { _obtainRequestToken(logger, null); } + + public static void RetrieveUserDetails(this ILogger logger) + { + _retrieveUserDetails(logger, null); + } } } diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj b/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj new file mode 100644 index 000000000..4f2aae616 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj @@ -0,0 +1,15 @@ + + + + ASP.NET Core middleware that enables an application to support Twitter's OAuth 1.0 authentication workflow. + netcoreapp3.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.xproj b/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.xproj deleted file mode 100644 index 5fba7a974..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.xproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 0330fff6-b4b5-42dd-8c99-26a789569000 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/AssemblyInfo.cs deleted file mode 100644 index 76feceeff..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Reflection; -using System.Resources; - -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: AssemblyCompany("Microsoft Corporation.")] -[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/Resources.Designer.cs index d60c2fc73..2eabfff29 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/Properties/Resources.Designer.cs @@ -15,32 +15,28 @@ private static readonly ResourceManager _resourceManager /// internal static string Exception_OptionMustBeProvided { - get { return GetString("Exception_OptionMustBeProvided"); } + get => GetString("Exception_OptionMustBeProvided"); } /// /// The '{0}' option must be provided. /// internal static string FormatException_OptionMustBeProvided(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); - } + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); /// /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. /// internal static string Exception_ValidatorHandlerMismatch { - get { return GetString("Exception_ValidatorHandlerMismatch"); } + get => GetString("Exception_ValidatorHandlerMismatch"); } /// /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. /// internal static string FormatException_ValidatorHandlerMismatch() - { - return GetString("Exception_ValidatorHandlerMismatch"); - } + => GetString("Exception_ValidatorHandlerMismatch"); private static string GetString(string name, params string[] formatterNames) { diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterAppBuilderExtensions.cs index df6ca1d02..36e1111da 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterAppBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterAppBuilderExtensions.cs @@ -1,9 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using Microsoft.AspNetCore.Authentication.Twitter; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { @@ -13,38 +12,26 @@ namespace Microsoft.AspNetCore.Builder public static class TwitterAppBuilderExtensions { /// - /// Adds the middleware to the specified , which enables Twitter authentication capabilities. + /// UseTwitterAuthentication is obsolete. Configure Twitter authentication with AddAuthentication().AddTwitter in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. + /// The to add the handler to. /// A reference to this instance after the operation has completed. + [Obsolete("UseTwitterAuthentication is obsolete. Configure Twitter authentication with AddAuthentication().AddTwitter in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseTwitterAuthentication(this IApplicationBuilder app) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware(); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } /// - /// Adds the middleware to the specified , which enables Twitter authentication capabilities. + /// UseTwitterAuthentication is obsolete. Configure Twitter authentication with AddAuthentication().AddTwitter in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details. /// - /// The to add the middleware to. + /// The to add the handler to. /// An action delegate to configure the provided . /// A reference to this instance after the operation has completed. + [Obsolete("UseTwitterAuthentication is obsolete. Configure Twitter authentication with AddAuthentication().AddTwitter in ConfigureServices. See https://go.microsoft.com/fwlink/?linkid=845470 for more details.", error: true)] public static IApplicationBuilder UseTwitterAuthentication(this IApplicationBuilder app, TwitterOptions options) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - return app.UseMiddleware(Options.Create(options)); + throw new NotSupportedException("This method is no longer supported, see https://go.microsoft.com/fwlink/?linkid=845470"); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs index 0610ccfc9..bdab80e59 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs @@ -6,5 +6,16 @@ namespace Microsoft.AspNetCore.Authentication.Twitter public static class TwitterDefaults { public const string AuthenticationScheme = "Twitter"; + + public static readonly string DisplayName = "Twitter"; + + // https://developer.twitter.com/en/docs/basics/authentication/api-reference/request_token + internal const string RequestTokenEndpoint = "https://api.twitter.com/oauth/request_token"; + + // https://developer.twitter.com/en/docs/basics/authentication/api-reference/authenticate + internal const string AuthenticationEndpoint = "https://api.twitter.com/oauth/authenticate?oauth_token="; + + // https://developer.twitter.com/en/docs/basics/authentication/api-reference/access_token + internal const string AccessTokenEndpoint = "https://api.twitter.com/oauth/access_token"; } } diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterExtensions.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterExtensions.cs new file mode 100644 index 000000000..724380569 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Twitter; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class TwitterExtensions + { + public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder) + => builder.AddTwitter(TwitterDefaults.AuthenticationScheme, _ => { }); + + public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddTwitter(TwitterDefaults.AuthenticationScheme, configureOptions); + + public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddTwitter(authenticationScheme, TwitterDefaults.DisplayName, configureOptions); + + public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, TwitterPostConfigureOptions>()); + return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs index 18aa0abcb..cc8591ed3 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs @@ -8,84 +8,100 @@ using System.Security.Claims; using System.Security.Cryptography; using System.Text; +using System.Text.Encodings.Web; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; +using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.Authentication.Twitter { - internal class TwitterHandler : RemoteAuthenticationHandler + public class TwitterHandler : RemoteAuthenticationHandler { private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - private const string StateCookie = "__TwitterState"; - private const string RequestTokenEndpoint = "https://api.twitter.com/oauth/request_token"; - private const string AuthenticationEndpoint = "https://api.twitter.com/oauth/authenticate?oauth_token="; - private const string AccessTokenEndpoint = "https://api.twitter.com/oauth/access_token"; - private readonly HttpClient _httpClient; + private HttpClient Backchannel => Options.Backchannel; - public TwitterHandler(HttpClient httpClient) + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new TwitterEvents Events { - _httpClient = httpClient; + get { return (TwitterEvents)base.Events; } + set { base.Events = value; } } - protected override async Task HandleRemoteAuthenticateAsync() + public TwitterHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { } + + protected override Task CreateEventsAsync() => Task.FromResult(new TwitterEvents()); + + protected override async Task HandleRemoteAuthenticateAsync() { - AuthenticationProperties properties = null; var query = Request.Query; - var protectedRequestToken = Request.Cookies[StateCookie]; + var protectedRequestToken = Request.Cookies[Options.StateCookie.Name]; var requestToken = Options.StateDataFormat.Unprotect(protectedRequestToken); if (requestToken == null) { - return AuthenticateResult.Fail("Invalid state cookie."); + return HandleRequestResult.Fail("Invalid state cookie."); } - properties = requestToken.Properties; + var properties = requestToken.Properties; - // REVIEW: see which of these are really errors + var denied = query["denied"]; + if (!StringValues.IsNullOrEmpty(denied)) + { + // Note: denied errors are special protocol errors indicating the user didn't + // approve the authorization demand requested by the remote authorization server. + // Since it's a frequent scenario (that is not caused by incorrect configuration), + // denied errors are handled differently using HandleAccessDeniedErrorAsync(). + return await HandleAccessDeniedErrorAsync(properties); + } var returnedToken = query["oauth_token"]; if (StringValues.IsNullOrEmpty(returnedToken)) { - return AuthenticateResult.Fail("Missing oauth_token"); + return HandleRequestResult.Fail("Missing oauth_token", properties); } if (!string.Equals(returnedToken, requestToken.Token, StringComparison.Ordinal)) { - return AuthenticateResult.Fail("Unmatched token"); + return HandleRequestResult.Fail("Unmatched token", properties); } var oauthVerifier = query["oauth_verifier"]; if (StringValues.IsNullOrEmpty(oauthVerifier)) { - return AuthenticateResult.Fail("Missing or blank oauth_verifier"); + return HandleRequestResult.Fail("Missing or blank oauth_verifier", properties); } - var cookieOptions = new CookieOptions - { - HttpOnly = true, - Secure = Request.IsHttps - }; + var cookieOptions = Options.StateCookie.Build(Context, Clock.UtcNow); - Response.Cookies.Delete(StateCookie, cookieOptions); + Response.Cookies.Delete(Options.StateCookie.Name, cookieOptions); - var accessToken = await ObtainAccessTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, requestToken, oauthVerifier); + var accessToken = await ObtainAccessTokenAsync(requestToken, oauthVerifier); var identity = new ClaimsIdentity(new[] { - new Claim(ClaimTypes.NameIdentifier, accessToken.UserId, ClaimValueTypes.String, Options.ClaimsIssuer), - new Claim(ClaimTypes.Name, accessToken.ScreenName, ClaimValueTypes.String, Options.ClaimsIssuer), - new Claim("urn:twitter:userid", accessToken.UserId, ClaimValueTypes.String, Options.ClaimsIssuer), - new Claim("urn:twitter:screenname", accessToken.ScreenName, ClaimValueTypes.String, Options.ClaimsIssuer) + new Claim(ClaimTypes.NameIdentifier, accessToken.UserId, ClaimValueTypes.String, ClaimsIssuer), + new Claim(ClaimTypes.Name, accessToken.ScreenName, ClaimValueTypes.String, ClaimsIssuer), + new Claim("urn:twitter:userid", accessToken.UserId, ClaimValueTypes.String, ClaimsIssuer), + new Claim("urn:twitter:screenname", accessToken.ScreenName, ClaimValueTypes.String, ClaimsIssuer) }, - Options.ClaimsIssuer); + ClaimsIssuer); + + JObject user = null; + if (Options.RetrieveUserDetails) + { + user = await RetrieveUserDetailsAsync(accessToken, identity); + } if (Options.SaveTokens) { @@ -95,114 +111,132 @@ protected override async Task HandleRemoteAuthenticateAsync( }); } - return AuthenticateResult.Success(await CreateTicketAsync(identity, properties, accessToken)); + return HandleRequestResult.Success(await CreateTicketAsync(identity, properties, accessToken, user)); } - protected virtual async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, AccessToken token) + protected virtual async Task CreateTicketAsync( + ClaimsIdentity identity, AuthenticationProperties properties, AccessToken token, JObject user) { - var context = new TwitterCreatingTicketContext(Context, Options, token.UserId, token.ScreenName, token.Token, token.TokenSecret) - { - Principal = new ClaimsPrincipal(identity), - Properties = properties - }; - - await Options.Events.CreatingTicket(context); - - if (context.Principal?.Identity == null) + foreach (var action in Options.ClaimActions) { - return null; + action.Run(user, identity, ClaimsIssuer); } - return new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme); + var context = new TwitterCreatingTicketContext(Context, Scheme, Options, new ClaimsPrincipal(identity), properties, token.UserId, token.ScreenName, token.Token, token.TokenSecret, user); + await Events.CreatingTicket(context); + + return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); } - protected override async Task HandleUnauthorizedAsync(ChallengeContext context) + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - var properties = new AuthenticationProperties(context.Properties) - { - ExpiresUtc = Options.SystemClock.UtcNow.Add(Options.RemoteAuthenticationTimeout) - }; - if (string.IsNullOrEmpty(properties.RedirectUri)) { - properties.RedirectUri = CurrentUri; + properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString; } // If CallbackConfirmed is false, this will throw - var requestToken = await ObtainRequestTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, BuildRedirectUri(Options.CallbackPath), properties); - var twitterAuthenticationEndpoint = AuthenticationEndpoint + requestToken.Token; + var requestToken = await ObtainRequestTokenAsync(BuildRedirectUri(Options.CallbackPath), properties); + var twitterAuthenticationEndpoint = TwitterDefaults.AuthenticationEndpoint + requestToken.Token; - var cookieOptions = new CookieOptions - { - HttpOnly = true, - Secure = Request.IsHttps - }; + var cookieOptions = Options.StateCookie.Build(Context, Clock.UtcNow); - Response.Cookies.Append(StateCookie, Options.StateDataFormat.Protect(requestToken), cookieOptions); + Response.Cookies.Append(Options.StateCookie.Name, Options.StateDataFormat.Protect(requestToken), cookieOptions); - var redirectContext = new TwitterRedirectToAuthorizationEndpointContext( - Context, Options, - properties, twitterAuthenticationEndpoint); - await Options.Events.RedirectToAuthorizationEndpoint(redirectContext); - return true; + var redirectContext = new RedirectContext(Context, Scheme, Options, properties, twitterAuthenticationEndpoint); + await Events.RedirectToAuthorizationEndpoint(redirectContext); } - private async Task ObtainRequestTokenAsync(string consumerKey, string consumerSecret, string callBackUri, AuthenticationProperties properties) + private async Task ExecuteRequestAsync(string url, HttpMethod httpMethod, RequestToken accessToken = null, Dictionary extraOAuthPairs = null, Dictionary queryParameters = null, Dictionary formData = null) { - Logger.ObtainRequestToken(); - - var nonce = Guid.NewGuid().ToString("N"); - - var authorizationParts = new SortedDictionary + var authorizationParts = new SortedDictionary(extraOAuthPairs ?? new Dictionary()) { - { "oauth_callback", callBackUri }, - { "oauth_consumer_key", consumerKey }, - { "oauth_nonce", nonce }, + { "oauth_consumer_key", Options.ConsumerKey }, + { "oauth_nonce", Guid.NewGuid().ToString("N") }, { "oauth_signature_method", "HMAC-SHA1" }, { "oauth_timestamp", GenerateTimeStamp() }, { "oauth_version", "1.0" } }; + if (accessToken != null) + { + authorizationParts.Add("oauth_token", accessToken.Token); + } + + var signatureParts = new SortedDictionary(authorizationParts); + if (queryParameters != null) + { + foreach (var queryParameter in queryParameters) + { + signatureParts.Add(queryParameter.Key, queryParameter.Value); + } + } + if (formData != null) + { + foreach (var formItem in formData) + { + signatureParts.Add(formItem.Key, formItem.Value); + } + } + var parameterBuilder = new StringBuilder(); - foreach (var authorizationKey in authorizationParts) + foreach (var signaturePart in signatureParts) { - parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.Encode(authorizationKey.Key), UrlEncoder.Encode(authorizationKey.Value)); + parameterBuilder.AppendFormat("{0}={1}&", Uri.EscapeDataString(signaturePart.Key), Uri.EscapeDataString(signaturePart.Value)); } parameterBuilder.Length--; var parameterString = parameterBuilder.ToString(); var canonicalizedRequestBuilder = new StringBuilder(); - canonicalizedRequestBuilder.Append(HttpMethod.Post.Method); + canonicalizedRequestBuilder.Append(httpMethod.Method); canonicalizedRequestBuilder.Append("&"); - canonicalizedRequestBuilder.Append(UrlEncoder.Encode(RequestTokenEndpoint)); + canonicalizedRequestBuilder.Append(Uri.EscapeDataString(url)); canonicalizedRequestBuilder.Append("&"); - canonicalizedRequestBuilder.Append(UrlEncoder.Encode(parameterString)); + canonicalizedRequestBuilder.Append(Uri.EscapeDataString(parameterString)); - var signature = ComputeSignature(consumerSecret, null, canonicalizedRequestBuilder.ToString()); + var signature = ComputeSignature(Options.ConsumerSecret, accessToken?.TokenSecret, canonicalizedRequestBuilder.ToString()); authorizationParts.Add("oauth_signature", signature); + var queryString = ""; + if (queryParameters != null) + { + var queryStringBuilder = new StringBuilder("?"); + foreach (var queryParam in queryParameters) + { + queryStringBuilder.AppendFormat("{0}={1}&", queryParam.Key, queryParam.Value); + } + queryStringBuilder.Length--; + queryString = queryStringBuilder.ToString(); + } + var authorizationHeaderBuilder = new StringBuilder(); authorizationHeaderBuilder.Append("OAuth "); foreach (var authorizationPart in authorizationParts) { - authorizationHeaderBuilder.AppendFormat( - "{0}=\"{1}\", ", authorizationPart.Key, UrlEncoder.Encode(authorizationPart.Value)); + authorizationHeaderBuilder.AppendFormat("{0}=\"{1}\",", authorizationPart.Key, Uri.EscapeDataString(authorizationPart.Value)); } - authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2; + authorizationHeaderBuilder.Length--; - var request = new HttpRequestMessage(HttpMethod.Post, RequestTokenEndpoint); + var request = new HttpRequestMessage(httpMethod, url + queryString); request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString()); - var response = await _httpClient.SendAsync(request, Context.RequestAborted); + if (formData != null) + { + request.Content = new FormUrlEncodedContent(formData); + } + + return await Backchannel.SendAsync(request, Context.RequestAborted); + } + + private async Task ObtainRequestTokenAsync(string callBackUri, AuthenticationProperties properties) + { + Logger.ObtainRequestToken(); + + var response = await ExecuteRequestAsync(TwitterDefaults.RequestTokenEndpoint, HttpMethod.Post, extraOAuthPairs: new Dictionary() { { "oauth_callback", callBackUri } }); response.EnsureSuccessStatusCode(); - string responseText = await response.Content.ReadAsStringAsync(); + var responseText = await response.Content.ReadAsStringAsync(); - var responseParameters = new FormCollection(FormReader.ReadForm(responseText)); + var responseParameters = new FormCollection(new FormReader(responseText).ReadForm()); if (!string.Equals(responseParameters["oauth_callback_confirmed"], "true", StringComparison.Ordinal)) { throw new Exception("Twitter oauth_callback_confirmed is not true."); @@ -211,64 +245,14 @@ private async Task ObtainRequestTokenAsync(string consumerKey, str return new RequestToken { Token = Uri.UnescapeDataString(responseParameters["oauth_token"]), TokenSecret = Uri.UnescapeDataString(responseParameters["oauth_token_secret"]), CallbackConfirmed = true, Properties = properties }; } - private async Task ObtainAccessTokenAsync(string consumerKey, string consumerSecret, RequestToken token, string verifier) + private async Task ObtainAccessTokenAsync(RequestToken token, string verifier) { // https://dev.twitter.com/docs/api/1/post/oauth/access_token Logger.ObtainAccessToken(); - var nonce = Guid.NewGuid().ToString("N"); - - var authorizationParts = new SortedDictionary - { - { "oauth_consumer_key", consumerKey }, - { "oauth_nonce", nonce }, - { "oauth_signature_method", "HMAC-SHA1" }, - { "oauth_token", token.Token }, - { "oauth_timestamp", GenerateTimeStamp() }, - { "oauth_verifier", verifier }, - { "oauth_version", "1.0" }, - }; - - var parameterBuilder = new StringBuilder(); - foreach (var authorizationKey in authorizationParts) - { - parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.Encode(authorizationKey.Key), UrlEncoder.Encode(authorizationKey.Value)); - } - parameterBuilder.Length--; - var parameterString = parameterBuilder.ToString(); - - var canonicalizedRequestBuilder = new StringBuilder(); - canonicalizedRequestBuilder.Append(HttpMethod.Post.Method); - canonicalizedRequestBuilder.Append("&"); - canonicalizedRequestBuilder.Append(UrlEncoder.Encode(AccessTokenEndpoint)); - canonicalizedRequestBuilder.Append("&"); - canonicalizedRequestBuilder.Append(UrlEncoder.Encode(parameterString)); - - var signature = ComputeSignature(consumerSecret, token.TokenSecret, canonicalizedRequestBuilder.ToString()); - authorizationParts.Add("oauth_signature", signature); - authorizationParts.Remove("oauth_verifier"); - - var authorizationHeaderBuilder = new StringBuilder(); - authorizationHeaderBuilder.Append("OAuth "); - foreach (var authorizationPart in authorizationParts) - { - authorizationHeaderBuilder.AppendFormat( - "{0}=\"{1}\", ", authorizationPart.Key, UrlEncoder.Encode(authorizationPart.Value)); - } - authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2; - - var request = new HttpRequestMessage(HttpMethod.Post, AccessTokenEndpoint); - request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString()); - - var formPairs = new Dictionary() - { - { "oauth_verifier", verifier }, - }; - - request.Content = new FormUrlEncodedContent(formPairs); - - var response = await _httpClient.SendAsync(request, Context.RequestAborted); + var formPost = new Dictionary { { "oauth_verifier", verifier } }; + var response = await ExecuteRequestAsync(TwitterDefaults.AccessTokenEndpoint, HttpMethod.Post, token, formData: formPost); if (!response.IsSuccessStatusCode) { @@ -277,7 +261,7 @@ private async Task ObtainAccessTokenAsync(string consumerKey, strin } var responseText = await response.Content.ReadAsStringAsync(); - var responseParameters = new FormCollection(FormReader.ReadForm(responseText)); + var responseParameters = new FormCollection(new FormReader(responseText).ReadForm()); return new AccessToken { @@ -288,6 +272,25 @@ private async Task ObtainAccessTokenAsync(string consumerKey, strin }; } + // https://dev.twitter.com/rest/reference/get/account/verify_credentials + private async Task RetrieveUserDetailsAsync(AccessToken accessToken, ClaimsIdentity identity) + { + Logger.RetrieveUserDetails(); + + var response = await ExecuteRequestAsync("https://api.twitter.com/1.1/account/verify_credentials.json", HttpMethod.Get, accessToken, queryParameters: new Dictionary() { { "include_email", "true" } }); + + if (!response.IsSuccessStatusCode) + { + Logger.LogError("Email request failed with a status code of " + response.StatusCode); + response.EnsureSuccessStatusCode(); // throw + } + var responseText = await response.Content.ReadAsStringAsync(); + + var result = JObject.Parse(responseText); + + return result; + } + private static string GenerateTimeStamp() { var secondsSinceUnixEpocStart = DateTime.UtcNow - Epoch; @@ -301,8 +304,8 @@ private string ComputeSignature(string consumerSecret, string tokenSecret, strin algorithm.Key = Encoding.ASCII.GetBytes( string.Format(CultureInfo.InvariantCulture, "{0}&{1}", - UrlEncoder.Encode(consumerSecret), - string.IsNullOrEmpty(tokenSecret) ? string.Empty : UrlEncoder.Encode(tokenSecret))); + Uri.EscapeDataString(consumerSecret), + string.IsNullOrEmpty(tokenSecret) ? string.Empty : Uri.EscapeDataString(tokenSecret))); var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(signatureData)); return Convert.ToBase64String(hash); } diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterMiddleware.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterMiddleware.cs deleted file mode 100644 index 67fb903dd..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterMiddleware.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Globalization; -using System.Net.Http; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Authentication.Twitter -{ - /// - /// ASP.NET Core middleware for authenticating users using Twitter. - /// - public class TwitterMiddleware : AuthenticationMiddleware - { - private readonly HttpClient _httpClient; - - /// - /// Initializes a - /// - /// The next middleware in the HTTP pipeline to invoke - /// - /// - /// - /// - /// Configuration options for the middleware - public TwitterMiddleware( - RequestDelegate next, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - UrlEncoder encoder, - IOptions sharedOptions, - IOptions options) - : base(next, options, loggerFactory, encoder) - { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - - if (dataProtectionProvider == null) - { - throw new ArgumentNullException(nameof(dataProtectionProvider)); - } - - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - if (sharedOptions == null) - { - throw new ArgumentNullException(nameof(sharedOptions)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (string.IsNullOrEmpty(Options.ConsumerSecret)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.ConsumerSecret))); - } - if (string.IsNullOrEmpty(Options.ConsumerKey)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.ConsumerKey))); - } - if (!Options.CallbackPath.HasValue) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.CallbackPath))); - } - - if (Options.Events == null) - { - Options.Events = new TwitterEvents(); - } - if (Options.StateDataFormat == null) - { - var dataProtector = dataProtectionProvider.CreateProtector( - typeof(TwitterMiddleware).FullName, Options.AuthenticationScheme, "v1"); - Options.StateDataFormat = new SecureDataFormat( - new RequestTokenSerializer(), - dataProtector); - } - - if (string.IsNullOrEmpty(Options.SignInScheme)) - { - Options.SignInScheme = sharedOptions.Value.SignInScheme; - } - if (string.IsNullOrEmpty(Options.SignInScheme)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "SignInScheme")); - } - - _httpClient = new HttpClient(Options.BackchannelHttpHandler ?? new HttpClientHandler()); - _httpClient.Timeout = Options.BackchannelTimeout; - _httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB - _httpClient.DefaultRequestHeaders.Accept.ParseAdd("*/*"); - _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core Twitter middleware"); - _httpClient.DefaultRequestHeaders.ExpectContinue = false; - } - - /// - /// Provides the object for processing authentication-related requests. - /// - /// An configured with the supplied to the constructor. - protected override AuthenticationHandler CreateHandler() - { - return new TwitterHandler(_httpClient); - } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs index 77fb0bd7a..269516ae1 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs @@ -2,28 +2,41 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.ComponentModel; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Twitter; +using System.Globalization; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Authentication.Twitter { /// - /// Options for the Twitter authentication middleware. + /// Options for the Twitter authentication handler. /// public class TwitterOptions : RemoteAuthenticationOptions { + private const string DefaultStateCookieName = "__TwitterState"; + + private CookieBuilder _stateCookieBuilder; + /// /// Initializes a new instance of the class. /// public TwitterOptions() { - AuthenticationScheme = TwitterDefaults.AuthenticationScheme; - DisplayName = AuthenticationScheme; CallbackPath = new PathString("/signin-twitter"); BackchannelTimeout = TimeSpan.FromSeconds(60); Events = new TwitterEvents(); + + ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email); + + _stateCookieBuilder = new TwitterCookieBuilder(this) + { + Name = DefaultStateCookieName, + SecurePolicy = CookieSecurePolicy.SameAsRequest, + HttpOnly = true, + SameSite = SameSiteMode.Lax, + IsEssential = true, + }; } /// @@ -39,23 +52,77 @@ public TwitterOptions() public string ConsumerSecret { get; set; } /// - /// Gets or sets the type used to secure data handled by the middleware. + /// Enables the retrieval user details during the authentication process, including + /// e-mail addresses. Retrieving e-mail addresses requires special permissions + /// from Twitter Support on a per application basis. The default is false. + /// See https://dev.twitter.com/rest/reference/get/account/verify_credentials + /// + public bool RetrieveUserDetails { get; set; } + + /// + /// A collection of claim actions used to select values from the json user data and create Claims. + /// + public ClaimActionCollection ClaimActions { get; } = new ClaimActionCollection(); + + /// + /// Gets or sets the type used to secure data handled by the handler. /// public ISecureDataFormat StateDataFormat { get; set; } /// - /// Gets or sets the used to handle authentication events. + /// Gets or sets the used to handle authentication events. /// - public new ITwitterEvents Events + public new TwitterEvents Events { - get { return (ITwitterEvents)base.Events; } - set { base.Events = value; } + get => (TwitterEvents)base.Events; + set => base.Events = value; } /// - /// For testing purposes only. + /// Determines the settings used to create the state cookie before the + /// cookie gets added to the response. /// - [EditorBrowsable(EditorBrowsableState.Never)] - public ISystemClock SystemClock { get; set; } = new SystemClock(); + public CookieBuilder StateCookie + { + get => _stateCookieBuilder; + set => _stateCookieBuilder = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Added the validate method to ensure that the customer key and customer secret values are not not empty for the twitter authentication middleware + /// + public override void Validate() + { + base.Validate(); + if (string.IsNullOrEmpty(ConsumerKey)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ConsumerKey)), nameof(ConsumerKey)); + } + + if (string.IsNullOrEmpty(ConsumerSecret)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(ConsumerSecret)), nameof(ConsumerSecret)); + } + } + + private class TwitterCookieBuilder : CookieBuilder + { + private readonly TwitterOptions _twitterOptions; + + public TwitterCookieBuilder(TwitterOptions twitterOptions) + { + _twitterOptions = twitterOptions; + } + + public override CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom) + { + var options = base.Build(context, expiresFrom); + if (!Expiration.HasValue) + { + options.Expires = expiresFrom.Add(_twitterOptions.RemoteAuthenticationTimeout); + } + return options; + } + } } } diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterPostConfigureOptions.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterPostConfigureOptions.cs new file mode 100644 index 000000000..09db5699f --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterPostConfigureOptions.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net.Http; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication.Twitter +{ + /// + /// Used to setup defaults for all . + /// + public class TwitterPostConfigureOptions : IPostConfigureOptions + { + private readonly IDataProtectionProvider _dp; + + public TwitterPostConfigureOptions(IDataProtectionProvider dataProtection) + { + _dp = dataProtection; + } + + /// + /// Invoked to post configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configure. + public void PostConfigure(string name, TwitterOptions options) + { + options.DataProtectionProvider = options.DataProtectionProvider ?? _dp; + + if (options.StateDataFormat == null) + { + var dataProtector = options.DataProtectionProvider.CreateProtector( + typeof(TwitterHandler).FullName, name, "v1"); + options.StateDataFormat = new SecureDataFormat( + new RequestTokenSerializer(), + dataProtector); + } + + if (options.Backchannel == null) + { + options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler()); + options.Backchannel.Timeout = options.BackchannelTimeout; + options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + options.Backchannel.DefaultRequestHeaders.Accept.ParseAdd("*/*"); + options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core Twitter handler"); + options.Backchannel.DefaultRequestHeaders.ExpectContinue = false; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json new file mode 100644 index 000000000..03ee64562 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json @@ -0,0 +1,924 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Twitter, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.TwitterExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddTwitter", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddTwitter", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddTwitter", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddTwitter", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.TwitterAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseTwitterAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseTwitterAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterCreatingTicketContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.ResultContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_UserId", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ScreenName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AccessToken", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AccessTokenSecret", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "Newtonsoft.Json.Linq.JObject", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "userId", + "Type": "System.String" + }, + { + "Name": "screenName", + "Type": "System.String" + }, + { + "Name": "accessToken", + "Type": "System.String" + }, + { + "Name": "accessTokenSecret", + "Type": "System.String" + }, + { + "Name": "user", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnCreatingTicket", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnCreatingTicket", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToAuthorizationEndpoint", + "Parameters": [], + "ReturnType": "System.Func, System.Threading.Tasks.Task>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToAuthorizationEndpoint", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func, System.Threading.Tasks.Task>" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreatingTicket", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterCreatingTicketContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToAuthorizationEndpoint", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.AccessToken", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_UserId", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_UserId", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ScreenName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ScreenName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Token", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Token", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenSecret", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenSecret", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CallbackConfirmed", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CallbackConfirmed", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Properties", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.RequestTokenSerializer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IDataSerializer" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Serialize", + "Parameters": [ + { + "Name": "model", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken" + } + ], + "ReturnType": "System.Byte[]", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Deserialize", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "token", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.Twitter.RequestToken", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Twitter\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateTicketAsync", + "Parameters": [ + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "token", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.AccessToken" + }, + { + "Name": "user", + "Type": "Newtonsoft.Json.Linq.JObject" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ConsumerKey", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConsumerKey", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConsumerSecret", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConsumerSecret", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RetrieveUserDetails", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RetrieveUserDetails", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClaimActions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.OAuth.Claims.ClaimActionCollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StateDataFormat", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StateDataFormat", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StateCookie", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StateCookie", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieBuilder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Twitter.TwitterPostConfigureOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Options.IPostConfigureOptions" + ], + "Members": [ + { + "Kind": "Method", + "Name": "PostConfigure", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.Twitter.TwitterOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "dataProtection", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/project.json b/src/Microsoft.AspNetCore.Authentication.Twitter/project.json deleted file mode 100644 index 23ca0cf31..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/project.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "ASP.NET Core middleware that enables an application to support Twitter's OAuth 2.0 authentication workflow.", - "tags": [ - "aspnetcore", - "authentication", - "security" - ], - "repository": { - "type": "git", - "url": "git://github.com/aspnet/security" - }, - "compilationOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk", - "nowarn": [ - "CS1591" - ], - "xmlDoc": true - }, - "dependencies": { - "Microsoft.AspNetCore.Authentication": "1.0.0-*" - }, - "frameworks": { - "net451": {}, - "netstandard1.3": {} - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs new file mode 100644 index 000000000..f643fad97 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/AuthenticationFailedContext.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.WsFederation; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// The context object used in for . + /// + public class AuthenticationFailedContext : RemoteAuthenticationContext + { + /// + /// Creates a new context object + /// + /// + /// + /// + public AuthenticationFailedContext(HttpContext context, AuthenticationScheme scheme, WsFederationOptions options) + : base(context, scheme, options, new AuthenticationProperties()) + { } + + /// + /// The from the request, if any. + /// + public WsFederationMessage ProtocolMessage { get; set; } + + /// + /// The that triggered this event. + /// + public Exception Exception { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs new file mode 100644 index 000000000..4028fa5e3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/MessageReceivedContext.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.WsFederation; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// The context object used for . + /// + public class MessageReceivedContext : RemoteAuthenticationContext + { + /// + /// Creates a new context object. + /// + /// + /// + /// + /// + public MessageReceivedContext( + HttpContext context, + AuthenticationScheme scheme, + WsFederationOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) { } + + /// + /// The received on this request. + /// + public WsFederationMessage ProtocolMessage { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs new file mode 100644 index 000000000..654037d0a --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RedirectContext.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.WsFederation; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// When a user configures the to be notified prior to redirecting to an IdentityProvider + /// an instance of is passed to the 'RedirectToAuthenticationEndpoint' or 'RedirectToEndSessionEndpoint' events. + /// + public class RedirectContext : PropertiesContext + { + /// + /// Creates a new context object. + /// + /// + /// + /// + /// + public RedirectContext( + HttpContext context, + AuthenticationScheme scheme, + WsFederationOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) { } + + /// + /// The used to compose the redirect. + /// + public WsFederationMessage ProtocolMessage { get; set; } + + /// + /// If true, will skip any default logic for this redirect. + /// + public bool Handled { get; private set; } + + /// + /// Skips any default logic for this redirect. + /// + public void HandleResponse() => Handled = true; + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RemoteSignoutContext.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RemoteSignoutContext.cs new file mode 100644 index 000000000..8aec24a64 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/RemoteSignoutContext.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.WsFederation; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// An event context for RemoteSignOut. + /// + public class RemoteSignOutContext : RemoteAuthenticationContext + { + /// + /// + /// + /// + /// + /// + /// + public RemoteSignOutContext(HttpContext context, AuthenticationScheme scheme, WsFederationOptions options, WsFederationMessage message) + : base(context, scheme, options, new AuthenticationProperties()) + => ProtocolMessage = message; + + /// + /// The signout message. + /// + public WsFederationMessage ProtocolMessage { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenReceivedContext.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenReceivedContext.cs new file mode 100644 index 000000000..311f41515 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenReceivedContext.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.WsFederation; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// This Context can be used to be informed when an 'AuthorizationCode' is redeemed for tokens at the token endpoint. + /// + public class SecurityTokenReceivedContext : RemoteAuthenticationContext + { + /// + /// Creates a + /// + public SecurityTokenReceivedContext(HttpContext context, AuthenticationScheme scheme, WsFederationOptions options, AuthenticationProperties properties) + : base(context, scheme, options, properties) + { + } + + /// + /// The received on this request. + /// + public WsFederationMessage ProtocolMessage { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.cs new file mode 100644 index 000000000..1f32014b6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/SecurityTokenValidatedContext.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.WsFederation; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// The context object used for . + /// + public class SecurityTokenValidatedContext : RemoteAuthenticationContext + { + /// + /// Creates a + /// + public SecurityTokenValidatedContext(HttpContext context, AuthenticationScheme scheme, WsFederationOptions options, ClaimsPrincipal principal, AuthenticationProperties properties) + : base(context, scheme, options, properties) + => Principal = principal; + + /// + /// The received on this request. + /// + public WsFederationMessage ProtocolMessage { get; set; } + + /// + /// The that was validated. + /// + public SecurityToken SecurityToken { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs new file mode 100644 index 000000000..55c3936f9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/Events/WsFederationEvents.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// Specifies events which the invokes to enable developer control over the authentication process. /> + /// + public class WsFederationEvents : RemoteAuthenticationEvents + { + /// + /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. + /// + public Func OnAuthenticationFailed { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked when a protocol message is first received. + /// + public Func OnMessageReceived { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked to manipulate redirects to the identity provider for SignIn, SignOut, or Challenge. + /// + public Func OnRedirectToIdentityProvider { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked when a wsignoutcleanup request is received at the RemoteSignOutPath endpoint. + /// + public Func OnRemoteSignOut { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked with the security token that has been extracted from the protocol message. + /// + public Func OnSecurityTokenReceived { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. + /// + public Func OnSecurityTokenValidated { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. + /// + public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context); + + /// + /// Invoked when a protocol message is first received. + /// + public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context); + + /// + /// Invoked to manipulate redirects to the identity provider for SignIn, SignOut, or Challenge. + /// + public virtual Task RedirectToIdentityProvider(RedirectContext context) => OnRedirectToIdentityProvider(context); + + /// + /// Invoked when a wsignoutcleanup request is received at the RemoteSignOutPath endpoint. + /// + public virtual Task RemoteSignOut(RemoteSignOutContext context) => OnRemoteSignOut(context); + + /// + /// Invoked with the security token that has been extracted from the protocol message. + /// + public virtual Task SecurityTokenReceived(SecurityTokenReceivedContext context) => OnSecurityTokenReceived(context); + + /// + /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. + /// + public virtual Task SecurityTokenValidated(SecurityTokenValidatedContext context) => OnSecurityTokenValidated(context); + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/LoggingExtensions.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/LoggingExtensions.cs new file mode 100644 index 000000000..e28b7e15b --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/LoggingExtensions.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging +{ + internal static class LoggingExtensions + { + private static Action _signInWithoutWresult; + private static Action _signInWithoutToken; + private static Action _exceptionProcessingMessage; + private static Action _malformedRedirectUri; + private static Action _remoteSignOutHandledResponse; + private static Action _remoteSignOutSkipped; + private static Action _remoteSignOut; + + static LoggingExtensions() + { + _signInWithoutWresult = LoggerMessage.Define( + eventId: 1, + logLevel: LogLevel.Debug, + formatString: "Received a sign-in message without a WResult."); + _signInWithoutToken = LoggerMessage.Define( + eventId: 2, + logLevel: LogLevel.Debug, + formatString: "Received a sign-in message without a token."); + _exceptionProcessingMessage = LoggerMessage.Define( + eventId: 3, + logLevel: LogLevel.Error, + formatString: "Exception occurred while processing message."); + _malformedRedirectUri = LoggerMessage.Define( + eventId: 4, + logLevel: LogLevel.Warning, + formatString: "The sign-out redirect URI '{0}' is malformed."); + _remoteSignOutHandledResponse = LoggerMessage.Define( + eventId: 5, + logLevel: LogLevel.Debug, + formatString: "RemoteSignOutContext.HandledResponse"); + _remoteSignOutSkipped = LoggerMessage.Define( + eventId: 6, + logLevel: LogLevel.Debug, + formatString: "RemoteSignOutContext.Skipped"); + _remoteSignOut = LoggerMessage.Define( + eventId: 7, + logLevel: LogLevel.Information, + formatString: "Remote signout request processed."); + } + + public static void SignInWithoutWresult(this ILogger logger) + { + _signInWithoutWresult(logger, null); + } + + public static void SignInWithoutToken(this ILogger logger) + { + _signInWithoutToken(logger, null); + } + + public static void ExceptionProcessingMessage(this ILogger logger, Exception ex) + { + _exceptionProcessingMessage(logger, ex); + } + + public static void MalformedRedirectUri(this ILogger logger, string uri) + { + _malformedRedirectUri(logger, uri, null); + } + + public static void RemoteSignOutHandledResponse(this ILogger logger) + { + _remoteSignOutHandledResponse(logger, null); + } + + public static void RemoteSignOutSkipped(this ILogger logger) + { + _remoteSignOutSkipped(logger, null); + } + + public static void RemoteSignOut(this ILogger logger) + { + _remoteSignOut(logger, null); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/Microsoft.AspNetCore.Authentication.WsFederation.csproj b/src/Microsoft.AspNetCore.Authentication.WsFederation/Microsoft.AspNetCore.Authentication.WsFederation.csproj new file mode 100644 index 000000000..ae4d52aff --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/Microsoft.AspNetCore.Authentication.WsFederation.csproj @@ -0,0 +1,17 @@ + + + + ASP.NET Core middleware that enables an application to support the WsFederation authentication workflow. + netcoreapp3.0 + true + aspnetcore;authentication;security + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/Properties/Resources.Designer.cs new file mode 100644 index 000000000..564e826a7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/Properties/Resources.Designer.cs @@ -0,0 +1,114 @@ +// +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Authentication.WsFederation.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The service descriptor is missing. + /// + internal static string Exception_MissingDescriptor + { + get => GetString("Exception_MissingDescriptor"); + } + + /// + /// The service descriptor is missing. + /// + internal static string FormatException_MissingDescriptor() + => GetString("Exception_MissingDescriptor"); + + /// + /// No token validator was found for the given token. + /// + internal static string Exception_NoTokenValidatorFound + { + get => GetString("Exception_NoTokenValidatorFound"); + } + + /// + /// No token validator was found for the given token. + /// + internal static string FormatException_NoTokenValidatorFound() + => GetString("Exception_NoTokenValidatorFound"); + + /// + /// The '{0}' option must be provided. + /// + internal static string Exception_OptionMustBeProvided + { + get => GetString("Exception_OptionMustBeProvided"); + } + + /// + /// The '{0}' option must be provided. + /// + internal static string FormatException_OptionMustBeProvided(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string Exception_ValidatorHandlerMismatch + { + get => GetString("Exception_ValidatorHandlerMismatch"); + } + + /// + /// An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string FormatException_ValidatorHandlerMismatch() + => GetString("Exception_ValidatorHandlerMismatch"); + + /// + /// The sign in message does not contain a required token. + /// + internal static string SignInMessageTokenIsMissing + { + get => GetString("SignInMessageTokenIsMissing"); + } + + /// + /// The sign in message does not contain a required token. + /// + internal static string FormatSignInMessageTokenIsMissing() + => GetString("SignInMessageTokenIsMissing"); + + /// + /// The sign in message does not contain a required wresult. + /// + internal static string SignInMessageWresultIsMissing + { + get => GetString("SignInMessageWresultIsMissing"); + } + + /// + /// The sign in message does not contain a required wresult. + /// + internal static string FormatSignInMessageWresultIsMissing() + => GetString("SignInMessageWresultIsMissing"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Resources.resx b/src/Microsoft.AspNetCore.Authentication.WsFederation/Resources.resx similarity index 86% rename from src/Microsoft.AspNetCore.Authentication.Cookies/Resources.resx rename to src/Microsoft.AspNetCore.Authentication.WsFederation/Resources.resx index 71debecfa..e2edafb67 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/Resources.resx +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/Resources.resx @@ -117,10 +117,22 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - The cookie key and options are larger than ChunksSize, leaving no room for data. + + The service descriptor is missing. - - The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded. + + No token validator was found for the given token. + + + The '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + + The sign in message does not contain a required token. + + + The sign in message does not contain a required wresult. \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationDefaults.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationDefaults.cs new file mode 100644 index 000000000..3b97d995b --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationDefaults.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// Default values related to WsFederation authentication handler + /// + public static class WsFederationDefaults + { + /// + /// The default authentication type used when registering the WsFederationHandler. + /// + public const string AuthenticationScheme = "WsFederation"; + + /// + /// The default display name used when registering the WsFederationHandler. + /// + public const string DisplayName = "WsFederation"; + + /// + /// Constant used to identify userstate inside AuthenticationProperties that have been serialized in the 'wctx' parameter. + /// + public static readonly string UserstatePropertiesKey = "WsFederation.Userstate"; + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationExtensions.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationExtensions.cs new file mode 100644 index 000000000..47091d58d --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationExtensions.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.WsFederation; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extensions for registering the . + /// + public static class WsFederationExtensions + { + /// + /// Registers the using the default authentication scheme, display name, and options. + /// + /// + /// + public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder) + => builder.AddWsFederation(WsFederationDefaults.AuthenticationScheme, _ => { }); + + /// + /// Registers the using the default authentication scheme, display name, and the given options configuration. + /// + /// + /// A delegate that configures the . + /// + public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddWsFederation(WsFederationDefaults.AuthenticationScheme, configureOptions); + + /// + /// Registers the using the given authentication scheme, default display name, and the given options configuration. + /// + /// + /// + /// A delegate that configures the . + /// + public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + => builder.AddWsFederation(authenticationScheme, WsFederationDefaults.DisplayName, configureOptions); + + /// + /// Registers the using the given authentication scheme, display name, and options configuration. + /// + /// + /// + /// + /// A delegate that configures the . + /// + public static AuthenticationBuilder AddWsFederation(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, WsFederationPostConfigureOptions>()); + return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationHandler.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationHandler.cs new file mode 100644 index 000000000..c4bb8fff7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationHandler.cs @@ -0,0 +1,425 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.WsFederation; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// A per-request authentication handler for the WsFederation. + /// + public class WsFederationHandler : RemoteAuthenticationHandler, IAuthenticationSignOutHandler + { + private const string CorrelationProperty = ".xsrf"; + private WsFederationConfiguration _configuration; + + /// + /// Creates a new WsFederationAuthenticationHandler + /// + /// + /// + /// + /// + public WsFederationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new WsFederationEvents Events + { + get { return (WsFederationEvents)base.Events; } + set { base.Events = value; } + } + + /// + /// Creates a new instance of the events instance. + /// + /// A new instance of the events instance. + protected override Task CreateEventsAsync() => Task.FromResult(new WsFederationEvents()); + + /// + /// Overridden to handle remote signout requests + /// + /// + public override Task HandleRequestAsync() + { + // RemoteSignOutPath and CallbackPath may be the same, fall through if the message doesn't match. + if (Options.RemoteSignOutPath.HasValue && Options.RemoteSignOutPath == Request.Path && HttpMethods.IsGet(Request.Method) + && string.Equals(Request.Query[WsFederationConstants.WsFederationParameterNames.Wa], + WsFederationConstants.WsFederationActions.SignOutCleanup, StringComparison.OrdinalIgnoreCase)) + { + // We've received a remote sign-out request + return HandleRemoteSignOutAsync(); + } + + return base.HandleRequestAsync(); + } + + /// + /// Handles Challenge + /// + /// + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + if (_configuration == null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + // Save the original challenge URI so we can redirect back to it when we're done. + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString; + } + + var wsFederationMessage = new WsFederationMessage() + { + IssuerAddress = _configuration.TokenEndpoint ?? string.Empty, + Wtrealm = Options.Wtrealm, + Wa = WsFederationConstants.WsFederationActions.SignIn, + }; + + if (!string.IsNullOrEmpty(Options.Wreply)) + { + wsFederationMessage.Wreply = Options.Wreply; + } + else + { + wsFederationMessage.Wreply = BuildRedirectUri(Options.CallbackPath); + } + + GenerateCorrelationId(properties); + + var redirectContext = new RedirectContext(Context, Scheme, Options, properties) + { + ProtocolMessage = wsFederationMessage + }; + await Events.RedirectToIdentityProvider(redirectContext); + + if (redirectContext.Handled) + { + return; + } + + wsFederationMessage = redirectContext.ProtocolMessage; + + if (!string.IsNullOrEmpty(wsFederationMessage.Wctx)) + { + properties.Items[WsFederationDefaults.UserstatePropertiesKey] = wsFederationMessage.Wctx; + } + + wsFederationMessage.Wctx = Uri.EscapeDataString(Options.StateDataFormat.Protect(properties)); + + var redirectUri = wsFederationMessage.CreateSignInUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + Logger.MalformedRedirectUri(redirectUri); + } + Response.Redirect(redirectUri); + } + + /// + /// Invoked to process incoming authentication messages. + /// + /// + protected override async Task HandleRemoteAuthenticateAsync() + { + WsFederationMessage wsFederationMessage = null; + AuthenticationProperties properties = null; + + // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small. + if (HttpMethods.IsPost(Request.Method) + && !string.IsNullOrEmpty(Request.ContentType) + // May have media/type; charset=utf-8, allow partial match. + && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) + && Request.Body.CanRead) + { + var form = await Request.ReadFormAsync(); + + wsFederationMessage = new WsFederationMessage(form.Select(pair => new KeyValuePair(pair.Key, pair.Value))); + } + + if (wsFederationMessage == null || !wsFederationMessage.IsSignInMessage) + { + if (Options.SkipUnrecognizedRequests) + { + // Not for us? + return HandleRequestResult.SkipHandler(); + } + + return HandleRequestResult.Fail("No message."); + } + + try + { + // Retrieve our cached redirect uri + var state = wsFederationMessage.Wctx; + // WsFed allows for uninitiated logins, state may be missing. See AllowUnsolicitedLogins. + properties = Options.StateDataFormat.Unprotect(state); + + if (properties == null) + { + if (!Options.AllowUnsolicitedLogins) + { + return HandleRequestResult.Fail("Unsolicited logins are not allowed."); + } + } + else + { + // Extract the user state from properties and reset. + properties.Items.TryGetValue(WsFederationDefaults.UserstatePropertiesKey, out var userState); + wsFederationMessage.Wctx = userState; + } + + var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options, properties) + { + ProtocolMessage = wsFederationMessage + }; + await Events.MessageReceived(messageReceivedContext); + if (messageReceivedContext.Result != null) + { + return messageReceivedContext.Result; + } + wsFederationMessage = messageReceivedContext.ProtocolMessage; + properties = messageReceivedContext.Properties; // Provides a new instance if not set. + + // If state did flow from the challenge then validate it. See AllowUnsolicitedLogins above. + if (properties.Items.TryGetValue(CorrelationProperty, out string correlationId) + && !ValidateCorrelationId(properties)) + { + return HandleRequestResult.Fail("Correlation failed.", properties); + } + + if (wsFederationMessage.Wresult == null) + { + Logger.SignInWithoutWresult(); + return HandleRequestResult.Fail(Resources.SignInMessageWresultIsMissing, properties); + } + + var token = wsFederationMessage.GetToken(); + if (string.IsNullOrEmpty(token)) + { + Logger.SignInWithoutToken(); + return HandleRequestResult.Fail(Resources.SignInMessageTokenIsMissing, properties); + } + + var securityTokenReceivedContext = new SecurityTokenReceivedContext(Context, Scheme, Options, properties) + { + ProtocolMessage = wsFederationMessage + }; + await Events.SecurityTokenReceived(securityTokenReceivedContext); + if (securityTokenReceivedContext.Result != null) + { + return securityTokenReceivedContext.Result; + } + wsFederationMessage = securityTokenReceivedContext.ProtocolMessage; + properties = messageReceivedContext.Properties; + + if (_configuration == null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + // Copy and augment to avoid cross request race conditions for updated configurations. + var tvp = Options.TokenValidationParameters.Clone(); + var issuers = new[] { _configuration.Issuer }; + tvp.ValidIssuers = (tvp.ValidIssuers == null ? issuers : tvp.ValidIssuers.Concat(issuers)); + tvp.IssuerSigningKeys = (tvp.IssuerSigningKeys == null ? _configuration.SigningKeys : tvp.IssuerSigningKeys.Concat(_configuration.SigningKeys)); + + ClaimsPrincipal principal = null; + SecurityToken parsedToken = null; + foreach (var validator in Options.SecurityTokenHandlers) + { + if (validator.CanReadToken(token)) + { + principal = validator.ValidateToken(token, tvp, out parsedToken); + break; + } + } + + if (principal == null) + { + throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound); + } + + if (Options.UseTokenLifetime && parsedToken != null) + { + // Override any session persistence to match the token lifetime. + var issued = parsedToken.ValidFrom; + if (issued != DateTime.MinValue) + { + properties.IssuedUtc = issued.ToUniversalTime(); + } + var expires = parsedToken.ValidTo; + if (expires != DateTime.MinValue) + { + properties.ExpiresUtc = expires.ToUniversalTime(); + } + properties.AllowRefresh = false; + } + + var securityTokenValidatedContext = new SecurityTokenValidatedContext(Context, Scheme, Options, principal, properties) + { + ProtocolMessage = wsFederationMessage, + SecurityToken = parsedToken, + }; + + await Events.SecurityTokenValidated(securityTokenValidatedContext); + if (securityTokenValidatedContext.Result != null) + { + return securityTokenValidatedContext.Result; + } + + // Flow possible changes + principal = securityTokenValidatedContext.Principal; + properties = securityTokenValidatedContext.Properties; + + return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name)); + } + catch (Exception exception) + { + Logger.ExceptionProcessingMessage(exception); + + // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification. + if (Options.RefreshOnIssuerKeyNotFound && exception.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException))) + { + Options.ConfigurationManager.RequestRefresh(); + } + + var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options) + { + ProtocolMessage = wsFederationMessage, + Exception = exception + }; + await Events.AuthenticationFailed(authenticationFailedContext); + if (authenticationFailedContext.Result != null) + { + return authenticationFailedContext.Result; + } + + return HandleRequestResult.Fail(exception, properties); + } + } + + /// + /// Handles Signout + /// + /// + public async virtual Task SignOutAsync(AuthenticationProperties properties) + { + var target = ResolveTarget(Options.ForwardSignOut); + if (target != null) + { + await Context.SignOutAsync(target, properties); + return; + } + + if (_configuration == null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + var wsFederationMessage = new WsFederationMessage() + { + IssuerAddress = _configuration.TokenEndpoint ?? string.Empty, + Wtrealm = Options.Wtrealm, + Wa = WsFederationConstants.WsFederationActions.SignOut, + }; + + // Set Wreply in order: + // 1. properties.Redirect + // 2. Options.SignOutWreply + // 3. Options.Wreply + if (properties != null && !string.IsNullOrEmpty(properties.RedirectUri)) + { + wsFederationMessage.Wreply = BuildRedirectUriIfRelative(properties.RedirectUri); + } + else if (!string.IsNullOrEmpty(Options.SignOutWreply)) + { + wsFederationMessage.Wreply = BuildRedirectUriIfRelative(Options.SignOutWreply); + } + else if (!string.IsNullOrEmpty(Options.Wreply)) + { + wsFederationMessage.Wreply = BuildRedirectUriIfRelative(Options.Wreply); + } + + var redirectContext = new RedirectContext(Context, Scheme, Options, properties) + { + ProtocolMessage = wsFederationMessage + }; + await Events.RedirectToIdentityProvider(redirectContext); + + if (!redirectContext.Handled) + { + var redirectUri = redirectContext.ProtocolMessage.CreateSignOutUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + Logger.MalformedRedirectUri(redirectUri); + } + Response.Redirect(redirectUri); + } + } + + /// + /// Handles wsignoutcleanup1.0 messages sent to the RemoteSignOutPath + /// + /// + protected virtual async Task HandleRemoteSignOutAsync() + { + var message = new WsFederationMessage(Request.Query.Select(pair => new KeyValuePair(pair.Key, pair.Value))); + var remoteSignOutContext = new RemoteSignOutContext(Context, Scheme, Options, message); + await Events.RemoteSignOut(remoteSignOutContext); + + if (remoteSignOutContext.Result != null) + { + if (remoteSignOutContext.Result.Handled) + { + Logger.RemoteSignOutHandledResponse(); + return true; + } + if (remoteSignOutContext.Result.Skipped) + { + Logger.RemoteSignOutSkipped(); + return false; + } + } + + Logger.RemoteSignOut(); + + await Context.SignOutAsync(Options.SignOutScheme); + return true; + } + + /// + /// Build a redirect path if the given path is a relative path. + /// + private string BuildRedirectUriIfRelative(string uri) + { + if (string.IsNullOrEmpty(uri)) + { + return uri; + } + + if (!uri.StartsWith("/", StringComparison.Ordinal)) + { + return uri; + } + + return BuildRedirectUri(uri); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationOptions.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationOptions.cs new file mode 100644 index 000000000..4e0612677 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationOptions.cs @@ -0,0 +1,180 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.WsFederation; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens.Saml; +using Microsoft.IdentityModel.Tokens.Saml2; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// Configuration options for + /// + public class WsFederationOptions : RemoteAuthenticationOptions + { + private ICollection _securityTokenHandlers = new Collection() + { + new Saml2SecurityTokenHandler(), + new SamlSecurityTokenHandler(), + new JwtSecurityTokenHandler() + }; + private TokenValidationParameters _tokenValidationParameters = new TokenValidationParameters(); + + /// + /// Initializes a new + /// + public WsFederationOptions() + { + CallbackPath = "/signin-wsfed"; + // In ADFS the cleanup messages are sent to the same callback path as the initial login. + // In AAD it sends the cleanup message to a random Reply Url and there's no deterministic way to configure it. + // If you manage to get it configured, then you can set RemoteSignOutPath accordingly. + RemoteSignOutPath = "/signin-wsfed"; + Events = new WsFederationEvents(); + } + + /// + /// Check that the options are valid. Should throw an exception if things are not ok. + /// + public override void Validate() + { + base.Validate(); + + if (ConfigurationManager == null) + { + throw new InvalidOperationException($"Provide {nameof(MetadataAddress)}, " + + $"{nameof(Configuration)}, or {nameof(ConfigurationManager)} to {nameof(WsFederationOptions)}"); + } + } + + /// + /// Configuration provided directly by the developer. If provided, then MetadataAddress and the Backchannel properties + /// will not be used. This information should not be updated during request processing. + /// + public WsFederationConfiguration Configuration { get; set; } + + /// + /// Gets or sets the address to retrieve the wsFederation metadata + /// + public string MetadataAddress { get; set; } + + /// + /// Responsible for retrieving, caching, and refreshing the configuration from metadata. + /// If not provided, then one will be created using the MetadataAddress and Backchannel properties. + /// + public IConfigurationManager ConfigurationManager { get; set; } + + /// + /// Gets or sets if a metadata refresh should be attempted after a SecurityTokenSignatureKeyNotFoundException. This allows for automatic + /// recovery in the event of a signature key rollover. This is enabled by default. + /// + public bool RefreshOnIssuerKeyNotFound { get; set; } = true; + + /// + /// Indicates if requests to the CallbackPath may also be for other components. If enabled the handler will pass + /// requests through that do not contain WsFederation authentication responses. Disabling this and setting the + /// CallbackPath to a dedicated endpoint may provide better error handling. + /// This is disabled by default. + /// + public bool SkipUnrecognizedRequests { get; set; } + + /// + /// Gets or sets the to call when processing WsFederation messages. + /// + public new WsFederationEvents Events + { + get => (WsFederationEvents)base.Events; + set => base.Events = value; + } + + /// + /// Gets or sets the collection of used to read and validate the s. + /// + public ICollection SecurityTokenHandlers + { + get + { + return _securityTokenHandlers; + } + set + { + _securityTokenHandlers = value ?? throw new ArgumentNullException(nameof(SecurityTokenHandlers)); + } + } + + /// + /// Gets or sets the type used to secure data handled by the middleware. + /// + public ISecureDataFormat StateDataFormat { get; set; } + + /// + /// Gets or sets the + /// + /// if 'TokenValidationParameters' is null. + public TokenValidationParameters TokenValidationParameters + { + get + { + return _tokenValidationParameters; + } + set + { + _tokenValidationParameters = value ?? throw new ArgumentNullException(nameof(TokenValidationParameters)); + } + } + + /// + /// Gets or sets the 'wreply'. CallbackPath must be set to match or cleared so it can be generated dynamically. + /// This field is optional. If not set then it will be generated from the current request and the CallbackPath. + /// + public string Wreply { get; set; } + + /// + /// Gets or sets the 'wreply' value used during sign-out. + /// If none is specified then the value from the Wreply field is used. + /// + public string SignOutWreply { get; set; } + + /// + /// Gets or sets the 'wtrealm'. + /// + public string Wtrealm { get; set; } + + /// + /// Indicates that the authentication session lifetime (e.g. cookies) should match that of the authentication token. + /// If the token does not provide lifetime information then normal session lifetimes will be used. + /// This is enabled by default. + /// + public bool UseTokenLifetime { get; set; } = true; + + /// + /// Gets or sets if HTTPS is required for the metadata address or authority. + /// The default is true. This should be disabled only in development environments. + /// + public bool RequireHttpsMetadata { get; set; } = true; + + /// + /// The Ws-Federation protocol allows the user to initiate logins without contacting the application for a Challenge first. + /// However, that flow is susceptible to XSRF and other attacks so it is disabled here by default. + /// + public bool AllowUnsolicitedLogins { get; set; } + + /// + /// Requests received on this path will cause the handler to invoke SignOut using the SignOutScheme. + /// + public PathString RemoteSignOutPath { get; set; } + + /// + /// The Authentication Scheme to use with SignOutAsync from RemoteSignOutPath. SignInScheme will be used if this + /// is not set. + /// + public string SignOutScheme { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs b/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs new file mode 100644 index 000000000..62647d4fc --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationPostConfigureOptions.cs @@ -0,0 +1,89 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.WsFederation; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + /// + /// Used to setup defaults for all . + /// + public class WsFederationPostConfigureOptions : IPostConfigureOptions + { + private readonly IDataProtectionProvider _dp; + + /// + /// + /// + /// + public WsFederationPostConfigureOptions(IDataProtectionProvider dataProtection) + { + _dp = dataProtection; + } + + /// + /// Invoked to post configure a TOptions instance. + /// + /// The name of the options instance being configured. + /// The options instance to configure. + public void PostConfigure(string name, WsFederationOptions options) + { + options.DataProtectionProvider = options.DataProtectionProvider ?? _dp; + + if (string.IsNullOrEmpty(options.SignOutScheme)) + { + options.SignOutScheme = options.SignInScheme; + } + + if (options.StateDataFormat == null) + { + var dataProtector = options.DataProtectionProvider.CreateProtector( + typeof(WsFederationHandler).FullName, name, "v1"); + options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + if (!options.CallbackPath.HasValue && !string.IsNullOrEmpty(options.Wreply) && Uri.TryCreate(options.Wreply, UriKind.Absolute, out var wreply)) + { + // Wreply must be a very specific, case sensitive value, so we can't generate it. Instead we generate CallbackPath from it. + options.CallbackPath = PathString.FromUriComponent(wreply); + } + + if (string.IsNullOrEmpty(options.TokenValidationParameters.ValidAudience)) + { + options.TokenValidationParameters.ValidAudience = options.Wtrealm; + } + + if (options.Backchannel == null) + { + options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler()); + options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core WsFederation handler"); + options.Backchannel.Timeout = options.BackchannelTimeout; + options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + } + + if (options.ConfigurationManager == null) + { + if (options.Configuration != null) + { + options.ConfigurationManager = new StaticConfigurationManager(options.Configuration); + } + else if (!string.IsNullOrEmpty(options.MetadataAddress)) + { + if (options.RequireHttpsMetadata && !options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("The MetadataAddress must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false."); + } + + options.ConfigurationManager = new ConfigurationManager(options.MetadataAddress, new WsFederationConfigurationRetriever(), + new HttpDocumentRetriever(options.Backchannel) { RequireHttps = options.RequireHttpsMetadata }); + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.WsFederation/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.WsFederation/baseline.netcore.json new file mode 100644 index 000000000..41150cbc0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/baseline.netcore.json @@ -0,0 +1,1314 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.WsFederation, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.WsFederationExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddWsFederation", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddWsFederation", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddWsFederation", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddWsFederation", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.AuthenticationFailedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Exception", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Exception", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.MessageReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.RedirectContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Handled", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleResponse", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.RemoteSignOutContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + }, + { + "Name": "message", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenValidatedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ProtocolMessage", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ProtocolMessage", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationMessage" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SecurityToken", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Tokens.SecurityToken", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SecurityToken", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Tokens.SecurityToken" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + }, + { + "Name": "principal", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnAuthenticationFailed", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnAuthenticationFailed", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnMessageReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnMessageReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRedirectToIdentityProvider", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRedirectToIdentityProvider", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnRemoteSignOut", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRemoteSignOut", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnSecurityTokenReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnSecurityTokenReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnSecurityTokenValidated", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnSecurityTokenValidated", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticationFailed", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.AuthenticationFailedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MessageReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.MessageReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RedirectToIdentityProvider", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.RedirectContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RemoteSignOut", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.RemoteSignOutContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SecurityTokenReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SecurityTokenValidated", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.SecurityTokenValidatedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationDefaults", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "UserstatePropertiesKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "AuthenticationScheme", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"WsFederation\"" + }, + { + "Kind": "Field", + "Name": "DisplayName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"WsFederation\"" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleRequestAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteSignOutAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Configuration", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationConfiguration", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Configuration", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.WsFederation.WsFederationConfiguration" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MetadataAddress", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MetadataAddress", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConfigurationManager", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Protocols.IConfigurationManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConfigurationManager", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Protocols.IConfigurationManager" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RefreshOnIssuerKeyNotFound", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RefreshOnIssuerKeyNotFound", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SkipUnrecognizedRequests", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SkipUnrecognizedRequests", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SecurityTokenHandlers", + "Parameters": [], + "ReturnType": "System.Collections.Generic.ICollection", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SecurityTokenHandlers", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.ICollection" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StateDataFormat", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_StateDataFormat", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TokenValidationParameters", + "Parameters": [], + "ReturnType": "Microsoft.IdentityModel.Tokens.TokenValidationParameters", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TokenValidationParameters", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.IdentityModel.Tokens.TokenValidationParameters" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Wreply", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Wreply", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SignOutWreply", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SignOutWreply", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Wtrealm", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Wtrealm", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UseTokenLifetime", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_UseTokenLifetime", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequireHttpsMetadata", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequireHttpsMetadata", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AllowUnsolicitedLogins", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AllowUnsolicitedLogins", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemoteSignOutPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemoteSignOutPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SignOutScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SignOutScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationPostConfigureOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Extensions.Options.IPostConfigureOptions" + ], + "Members": [ + { + "Kind": "Method", + "Name": "PostConfigure", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Extensions.Options.IPostConfigureOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "dataProtection", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/AuthAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Authentication/AuthAppBuilderExtensions.cs new file mode 100644 index 000000000..771601ed1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/AuthAppBuilderExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods to add authentication capabilities to an HTTP application pipeline. + /// + public static class AuthAppBuilderExtensions + { + /// + /// Adds the to the specified , which enables authentication capabilities. + /// + /// The to add the middleware to. + /// A reference to this instance after the operation has completed. + public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/AuthenticateResult.cs b/src/Microsoft.AspNetCore.Authentication/AuthenticateResult.cs deleted file mode 100644 index fb97931cb..000000000 --- a/src/Microsoft.AspNetCore.Authentication/AuthenticateResult.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.AspNetCore.Authentication -{ - /// - /// Contains the result of an Authenticate call - /// - public class AuthenticateResult - { - private AuthenticateResult() { } - - /// - /// If a ticket was produced, authenticate was successful. - /// - public bool Succeeded - { - get - { - return Ticket != null; - } - } - - /// - /// The authentication ticket. - /// - public AuthenticationTicket Ticket { get; private set; } - - /// - /// Holds failure information from the authentication. - /// - public Exception Failure { get; private set; } - - /// - /// Indicates that this stage of authentication was skipped by user intervention. - /// - public bool Skipped { get; private set; } - - public static AuthenticateResult Success(AuthenticationTicket ticket) - { - if (ticket == null) - { - throw new ArgumentNullException(nameof(ticket)); - } - return new AuthenticateResult() { Ticket = ticket }; - } - - public static AuthenticateResult Skip() - { - return new AuthenticateResult() { Skipped = true }; - } - - public static AuthenticateResult Fail(Exception failure) - { - return new AuthenticateResult() { Failure = failure }; - } - - public static AuthenticateResult Fail(string failureMessage) - { - return new AuthenticateResult() { Failure = new Exception(failureMessage) }; - } - - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/AuthenticationBuilder.cs b/src/Microsoft.AspNetCore.Authentication/AuthenticationBuilder.cs new file mode 100644 index 000000000..401b1f488 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/AuthenticationBuilder.cs @@ -0,0 +1,120 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Used to configure authentication + /// + public class AuthenticationBuilder + { + /// + /// Constructor. + /// + /// The services being configured. + public AuthenticationBuilder(IServiceCollection services) + => Services = services; + + /// + /// The services being configured. + /// + public virtual IServiceCollection Services { get; } + + + private AuthenticationBuilder AddSchemeHelper(string authenticationScheme, string displayName, Action configureOptions) + where TOptions : class, new() + where THandler : class, IAuthenticationHandler + { + Services.Configure(o => + { + o.AddScheme(authenticationScheme, scheme => { + scheme.HandlerType = typeof(THandler); + scheme.DisplayName = displayName; + }); + }); + if (configureOptions != null) + { + Services.Configure(authenticationScheme, configureOptions); + } + Services.AddTransient(); + return this; + } + + /// + /// Adds a which can be used by . + /// + /// The type to configure the handler."/>. + /// The used to handle this scheme. + /// The name of this scheme. + /// The display name of this scheme. + /// Used to configure the scheme options. + /// The builder. + public virtual AuthenticationBuilder AddScheme(string authenticationScheme, string displayName, Action configureOptions) + where TOptions : AuthenticationSchemeOptions, new() + where THandler : AuthenticationHandler + => AddSchemeHelper(authenticationScheme, displayName, configureOptions); + + /// + /// Adds a which can be used by . + /// + /// The type to configure the handler."/>. + /// The used to handle this scheme. + /// The name of this scheme. + /// Used to configure the scheme options. + /// The builder. + public virtual AuthenticationBuilder AddScheme(string authenticationScheme, Action configureOptions) + where TOptions : AuthenticationSchemeOptions, new() + where THandler : AuthenticationHandler + => AddScheme(authenticationScheme, displayName: null, configureOptions: configureOptions); + + /// + /// Adds a based that supports remote authentication + /// which can be used by . + /// + /// The type to configure the handler."/>. + /// The used to handle this scheme. + /// The name of this scheme. + /// The display name of this scheme. + /// Used to configure the scheme options. + /// The builder. + public virtual AuthenticationBuilder AddRemoteScheme(string authenticationScheme, string displayName, Action configureOptions) + where TOptions : RemoteAuthenticationOptions, new() + where THandler : RemoteAuthenticationHandler + { + Services.TryAddEnumerable(ServiceDescriptor.Singleton, EnsureSignInScheme>()); + return AddScheme(authenticationScheme, displayName, configureOptions: configureOptions); + } + + /// + /// Adds a based authentication handler which can be used to + /// redirect to other authentication schemes. + /// + /// The name of this scheme. + /// The display name of this scheme. + /// Used to configure the scheme options. + /// The builder. + public virtual AuthenticationBuilder AddPolicyScheme(string authenticationScheme, string displayName, Action configureOptions) + => AddSchemeHelper(authenticationScheme, displayName, configureOptions); + + // Used to ensure that there's always a default sign in scheme that's not itself + private class EnsureSignInScheme : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions + { + private readonly AuthenticationOptions _authOptions; + + public EnsureSignInScheme(IOptions authOptions) + { + _authOptions = authOptions.Value; + } + + public void PostConfigure(string name, TOptions options) + { + options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme ?? _authOptions.DefaultScheme; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs b/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs index 639be0103..5c9a6473f 100644 --- a/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs @@ -4,290 +4,200 @@ using System; using System.Text.Encodings.Web; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Http.Features.Authentication; -using Microsoft.Extensions.Internal; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Authentication { - /// - /// Base class for the per-request work performed by most authentication middleware. - /// - /// Specifies which type for of AuthenticationOptions property - public abstract class AuthenticationHandler : IAuthenticationHandler where TOptions : AuthenticationOptions + public abstract class AuthenticationHandler : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new() { private Task _authenticateTask; - private bool _finishCalled; - - protected bool SignInAccepted { get; set; } - protected bool SignOutAccepted { get; set; } - protected bool ChallengeCalled { get; set; } + public AuthenticationScheme Scheme { get; private set; } + public TOptions Options { get; private set; } protected HttpContext Context { get; private set; } protected HttpRequest Request { - get { return Context.Request; } + get => Context.Request; } protected HttpResponse Response { - get { return Context.Response; } + get => Context.Response; } - protected PathString OriginalPathBase { get; private set; } + protected PathString OriginalPath => Context.Features.Get()?.OriginalPath ?? Request.Path; + + protected PathString OriginalPathBase => Context.Features.Get()?.OriginalPathBase ?? Request.PathBase; - protected PathString OriginalPath { get; private set; } + protected ILogger Logger { get; } - protected ILogger Logger { get; private set; } + protected UrlEncoder UrlEncoder { get; } - protected UrlEncoder UrlEncoder { get; private set; } + protected ISystemClock Clock { get; } - public IAuthenticationHandler PriorHandler { get; set; } + protected IOptionsMonitor OptionsMonitor { get; } + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected virtual object Events { get; set; } + + protected virtual string ClaimsIssuer => Options.ClaimsIssuer ?? Scheme.Name; protected string CurrentUri { - get - { - return Request.Scheme + "://" + Request.Host + Request.PathBase + Request.Path + Request.QueryString; - } + get => Request.Scheme + "://" + Request.Host + Request.PathBase + Request.Path + Request.QueryString; } - protected TOptions Options { get; private set; } + protected AuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + { + Logger = logger.CreateLogger(this.GetType().FullName); + UrlEncoder = encoder; + Clock = clock; + OptionsMonitor = options; + } /// - /// Initialize is called once per request to contextualize this instance with appropriate state. + /// Initialize the handler, resolve the options and validate them. /// - /// The original options passed by the application control behavior - /// The utility object to observe the current request and response - /// The logging factory used to create loggers - /// The . - /// async completion - public async Task InitializeAsync(TOptions options, HttpContext context, ILogger logger, UrlEncoder encoder) + /// + /// + /// + public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) { - if (options == null) + if (scheme == null) { - throw new ArgumentNullException(nameof(options)); + throw new ArgumentNullException(nameof(scheme)); } - if (context == null) { throw new ArgumentNullException(nameof(context)); } - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - Options = options; + Scheme = scheme; Context = context; - OriginalPathBase = Request.PathBase; - OriginalPath = Request.Path; - Logger = logger; - UrlEncoder = encoder; - - RegisterAuthenticationHandler(); - - Response.OnStarting(OnStartingCallback, this); - - if (ShouldHandleScheme(AuthenticationManager.AutomaticScheme, Options.AutomaticAuthenticate)) - { - var result = await HandleAuthenticateOnceAsync(); - if (result?.Failure != null) - { - Logger.AuthenticationSchemeNotAuthenticatedWithFailure(Options.AuthenticationScheme, result.Failure.Message); - } - var ticket = result?.Ticket; - if (ticket?.Principal != null) - { - Context.User = SecurityHelper.MergeUserPrincipal(Context.User, ticket.Principal); - Logger.UserPrinicpalMerged(Options.AuthenticationScheme); - } - } - } - protected string BuildRedirectUri(string targetPath) - { - return Request.Scheme + "://" + Request.Host + OriginalPathBase + targetPath; - } + Options = OptionsMonitor.Get(Scheme.Name) ?? new TOptions(); + Options.Validate(Scheme.Name); - private static async Task OnStartingCallback(object state) - { - var handler = (AuthenticationHandler)state; - await handler.FinishResponseOnce(); - } - - private async Task FinishResponseOnce() - { - if (!_finishCalled) - { - _finishCalled = true; - await FinishResponseAsync(); - await HandleAutomaticChallengeIfNeeded(); - } + await InitializeEventsAsync(); + await InitializeHandlerAsync(); } /// - /// Hook that is called when the response about to be sent + /// Initializes the events object, called once per request by . /// - /// - protected virtual Task FinishResponseAsync() - { - return Task.FromResult(0); - } - - private async Task HandleAutomaticChallengeIfNeeded() + protected virtual async Task InitializeEventsAsync() { - if (!ChallengeCalled && Options.AutomaticChallenge && Response.StatusCode == 401) + Events = Options.Events; + if (Options.EventsType != null) { - await HandleUnauthorizedAsync(new ChallengeContext(Options.AuthenticationScheme)); + Events = Context.RequestServices.GetRequiredService(Options.EventsType); } + Events = Events ?? await CreateEventsAsync(); } /// - /// Called once after Invoke by AuthenticationMiddleware. + /// Creates a new instance of the events instance. /// - /// async completion - internal async Task TeardownAsync() - { - try - { - await FinishResponseOnce(); - } - finally - { - UnregisterAuthenticationHandler(); - } - } + /// A new instance of the events instance. + protected virtual Task CreateEventsAsync() => Task.FromResult(new object()); /// - /// Called once by common code after initialization. If an authentication middleware responds directly to - /// specifically known paths it must override this virtual, compare the request path to it's known paths, - /// provide any response information as appropriate, and true to stop further processing. + /// Called after options/events have been initialized for the handler to finish initializing itself. /// - /// Returning false will cause the common code to call the next middleware in line. Returning true will - /// cause the common code to begin the async completion journey without calling the rest of the middleware - /// pipeline. - public virtual Task HandleRequestAsync() - { - return Task.FromResult(false); - } - - public void GetDescriptions(DescribeSchemesContext describeContext) - { - describeContext.Accept(Options.Description.Items); + /// A task + protected virtual Task InitializeHandlerAsync() => Task.CompletedTask; - if (PriorHandler != null) - { - PriorHandler.GetDescriptions(describeContext); - } - } + protected string BuildRedirectUri(string targetPath) + => Request.Scheme + "://" + Request.Host + OriginalPathBase + targetPath; - public bool ShouldHandleScheme(string authenticationScheme, bool handleAutomatic) + protected virtual string ResolveTarget(string scheme) { - return string.Equals(Options.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) || - (handleAutomatic && string.Equals(authenticationScheme, AuthenticationManager.AutomaticScheme, StringComparison.Ordinal)); + var target = scheme ?? Options.ForwardDefaultSelector?.Invoke(Context) ?? Options.ForwardDefault; + + // Prevent self targetting + return string.Equals(target, Scheme.Name, StringComparison.Ordinal) + ? null + : target; } - public async Task AuthenticateAsync(AuthenticateContext context) + public async Task AuthenticateAsync() { - var handled = false; - if (ShouldHandleScheme(context.AuthenticationScheme, Options.AutomaticAuthenticate)) + var target = ResolveTarget(Options.ForwardAuthenticate); + if (target != null) { - // Calling Authenticate more than once should always return the original value. - var result = await HandleAuthenticateOnceAsync(); + return await Context.AuthenticateAsync(target); + } - if (result?.Failure != null) + // Calling Authenticate more than once should always return the original value. + var result = await HandleAuthenticateOnceAsync(); + if (result?.Failure == null) + { + var ticket = result?.Ticket; + if (ticket?.Principal != null) { - context.Failed(result.Failure); + Logger.AuthenticationSchemeAuthenticated(Scheme.Name); } else { - var ticket = result?.Ticket; - if (ticket?.Principal != null) - { - context.Authenticated(ticket.Principal, ticket.Properties.Items, Options.Description.Items); - Logger.AuthenticationSchemeAuthenticated(Options.AuthenticationScheme); - handled = true; - } - else - { - context.NotAuthenticated(); - Logger.AuthenticationSchemeNotAuthenticated(Options.AuthenticationScheme); - } + Logger.AuthenticationSchemeNotAuthenticated(Scheme.Name); } } - - if (PriorHandler != null && !handled) + else { - await PriorHandler.AuthenticateAsync(context); + Logger.AuthenticationSchemeNotAuthenticatedWithFailure(Scheme.Name, result.Failure.Message); } + return result; } + /// + /// Used to ensure HandleAuthenticateAsync is only invoked once. The subsequent calls + /// will return the same authenticate result. + /// protected Task HandleAuthenticateOnceAsync() { if (_authenticateTask == null) { _authenticateTask = HandleAuthenticateAsync(); } - return _authenticateTask; - } - - protected abstract Task HandleAuthenticateAsync(); - public async Task SignInAsync(SignInContext context) - { - if (ShouldHandleScheme(context.AuthenticationScheme, handleAutomatic: false)) - { - SignInAccepted = true; - await HandleSignInAsync(context); - Logger.AuthenticationSchemeSignedIn(Options.AuthenticationScheme); - context.Accept(); - } - else if (PriorHandler != null) - { - await PriorHandler.SignInAsync(context); - } - } - - protected virtual Task HandleSignInAsync(SignInContext context) - { - return Task.FromResult(0); + return _authenticateTask; } - public async Task SignOutAsync(SignOutContext context) + /// + /// Used to ensure HandleAuthenticateAsync is only invoked once safely. The subsequent + /// calls will return the same authentication result. Any exceptions will be converted + /// into a failed authentication result containing the exception. + /// + protected async Task HandleAuthenticateOnceSafeAsync() { - if (ShouldHandleScheme(context.AuthenticationScheme, handleAutomatic: false)) + try { - SignOutAccepted = true; - await HandleSignOutAsync(context); - Logger.AuthenticationSchemeSignedOut(Options.AuthenticationScheme); - context.Accept(); + return await HandleAuthenticateOnceAsync(); } - else if (PriorHandler != null) + catch (Exception ex) { - await PriorHandler.SignOutAsync(context); + return AuthenticateResult.Fail(ex); } } - protected virtual Task HandleSignOutAsync(SignOutContext context) - { - return Task.FromResult(0); - } + protected abstract Task HandleAuthenticateAsync(); - protected virtual Task HandleForbiddenAsync(ChallengeContext context) + /// + /// Override this method to handle Forbid. + /// + /// + /// A Task. + protected virtual Task HandleForbiddenAsync(AuthenticationProperties properties) { Response.StatusCode = 403; - return Task.FromResult(true); + return Task.CompletedTask; } /// @@ -295,59 +205,40 @@ protected virtual Task HandleForbiddenAsync(ChallengeContext context) /// deals an authentication interaction as part of it's request flow. (like adding a response header, or /// changing the 401 result to 302 of a login page or external sign-in location.) /// - /// - /// True if no other handlers should be called - protected virtual Task HandleUnauthorizedAsync(ChallengeContext context) + /// + /// A Task. + protected virtual Task HandleChallengeAsync(AuthenticationProperties properties) { Response.StatusCode = 401; - return Task.FromResult(false); + return Task.CompletedTask; } - public async Task ChallengeAsync(ChallengeContext context) + public async Task ChallengeAsync(AuthenticationProperties properties) { - ChallengeCalled = true; - var handled = false; - if (ShouldHandleScheme(context.AuthenticationScheme, Options.AutomaticChallenge)) + var target = ResolveTarget(Options.ForwardChallenge); + if (target != null) { - switch (context.Behavior) - { - case ChallengeBehavior.Automatic: - // If there is a principal already, invoke the forbidden code path - var result = await HandleAuthenticateOnceAsync(); - if (result?.Ticket?.Principal != null) - { - goto case ChallengeBehavior.Forbidden; - } - goto case ChallengeBehavior.Unauthorized; - case ChallengeBehavior.Unauthorized: - handled = await HandleUnauthorizedAsync(context); - Logger.AuthenticationSchemeChallenged(Options.AuthenticationScheme); - break; - case ChallengeBehavior.Forbidden: - handled = await HandleForbiddenAsync(context); - Logger.AuthenticationSchemeForbidden(Options.AuthenticationScheme); - break; - } - context.Accept(); + await Context.ChallengeAsync(target, properties); + return; } - if (!handled && PriorHandler != null) - { - await PriorHandler.ChallengeAsync(context); - } + properties = properties ?? new AuthenticationProperties(); + await HandleChallengeAsync(properties); + Logger.AuthenticationSchemeChallenged(Scheme.Name); } - private void RegisterAuthenticationHandler() + public async Task ForbidAsync(AuthenticationProperties properties) { - var auth = Context.GetAuthentication(); - PriorHandler = auth.Handler; - auth.Handler = this; - } + var target = ResolveTarget(Options.ForwardForbid); + if (target != null) + { + await Context.ForbidAsync(target, properties); + return; + } - private void UnregisterAuthenticationHandler() - { - var auth = Context.GetAuthentication(); - auth.Handler = PriorHandler; + properties = properties ?? new AuthenticationProperties(); + await HandleForbiddenAsync(properties); + Logger.AuthenticationSchemeForbidden(Scheme.Name); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs b/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs index a01490c3e..0c62cc3c3 100644 --- a/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs +++ b/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs @@ -2,90 +2,63 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Text.Encodings.Web; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Authentication { - public abstract class AuthenticationMiddleware where TOptions : AuthenticationOptions, new() + public class AuthenticationMiddleware { private readonly RequestDelegate _next; - protected AuthenticationMiddleware( - RequestDelegate next, - IOptions options, - ILoggerFactory loggerFactory, - UrlEncoder encoder) + public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes) { if (next == null) { throw new ArgumentNullException(nameof(next)); } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - Options = options.Value; - Logger = loggerFactory.CreateLogger(this.GetType().FullName); - UrlEncoder = encoder; - - if (string.IsNullOrEmpty(Options.ClaimsIssuer)) + if (schemes == null) { - // Default to something reasonable - Options.ClaimsIssuer = Options.AuthenticationScheme; + throw new ArgumentNullException(nameof(schemes)); } _next = next; + Schemes = schemes; } - public string AuthenticationScheme { get; set; } - - public TOptions Options { get; set; } - - public ILogger Logger { get; set; } - - public UrlEncoder UrlEncoder { get; set; } + public IAuthenticationSchemeProvider Schemes { get; set; } public async Task Invoke(HttpContext context) { - var handler = CreateHandler(); - await handler.InitializeAsync(Options, context, Logger, UrlEncoder); - try + context.Features.Set(new AuthenticationFeature + { + OriginalPath = context.Request.Path, + OriginalPathBase = context.Request.PathBase + }); + + // Give any IAuthenticationRequestHandler schemes a chance to handle the request + var handlers = context.RequestServices.GetRequiredService(); + foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()) { - if (!await handler.HandleRequestAsync()) + var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler; + if (handler != null && await handler.HandleRequestAsync()) { - await _next(context); + return; } } - finally + + var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync(); + if (defaultAuthenticate != null) { - try - { - await handler.TeardownAsync(); - } - catch (Exception) + var result = await context.AuthenticateAsync(defaultAuthenticate.Name); + if (result?.Principal != null) { - // Don't mask the original exception, if any + context.User = result.Principal; } } - } - protected abstract AuthenticationHandler CreateHandler(); + await _next(context); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/AuthenticationOptions.cs b/src/Microsoft.AspNetCore.Authentication/AuthenticationOptions.cs deleted file mode 100644 index 04d050b06..000000000 --- a/src/Microsoft.AspNetCore.Authentication/AuthenticationOptions.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Http.Authentication; - -namespace Microsoft.AspNetCore.Builder -{ - /// - /// Base Options for all authentication middleware. - /// - public abstract class AuthenticationOptions - { - private string _authenticationScheme; - - /// - /// The AuthenticationScheme in the options corresponds to the logical name for a particular authentication scheme. A different - /// value may be assigned in order to use the same authentication middleware type more than once in a pipeline. - /// - public string AuthenticationScheme - { - get { return _authenticationScheme; } - set - { - _authenticationScheme = value; - Description.AuthenticationScheme = value; - } - } - - /// - /// If true the authentication middleware alter the request user coming in. If false the authentication middleware will only provide - /// identity when explicitly indicated by the AuthenticationScheme. - /// - public bool AutomaticAuthenticate { get; set; } - - /// - /// If true the authentication middleware should handle automatic challenge. - /// If false the authentication middleware will only alter responses when explicitly indicated by the AuthenticationScheme. - /// - public bool AutomaticChallenge { get; set; } - - /// - /// Gets or sets the issuer that should be used for any claims that are created - /// - public string ClaimsIssuer { get; set; } - - /// - /// Additional information about the authentication type which is made available to the application. - /// - public AuthenticationDescription Description { get; set; } = new AuthenticationDescription(); - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs b/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs new file mode 100644 index 000000000..a547d203b --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Contains the options used by the . + /// + public class AuthenticationSchemeOptions + { + /// + /// Check that the options are valid. Should throw an exception if things are not ok. + /// + public virtual void Validate() { } + + /// + /// Checks that the options are valid for a specific scheme + /// + /// The scheme being validated. + public virtual void Validate(string scheme) + => Validate(); + + /// + /// Gets or sets the issuer that should be used for any claims that are created + /// + public string ClaimsIssuer { get; set; } + + /// + /// Instance used for events + /// + public object Events { get; set; } + + /// + /// If set, will be used as the service type to get the Events instance instead of the property. + /// + public Type EventsType { get; set; } + + /// + /// If set, this specifies a default scheme that authentication handlers should forward all authentication operations to + /// by default. The default forwarding logic will check the most specific ForwardAuthenticate/Challenge/Forbid/SignIn/SignOut + /// setting first, followed by checking the ForwardDefaultSelector, followed by ForwardDefault. The first non null result + /// will be used as the target scheme to forward to. + /// + public string ForwardDefault { get; set; } + + /// + /// If set, this specifies the target scheme that this scheme should forward AuthenticateAsync calls to. + /// For example Context.AuthenticateAsync("ThisScheme") => Context.AuthenticateAsync("ForwardAuthenticateValue"); + /// Set the target to the current scheme to disable forwarding and allow normal processing. + /// + public string ForwardAuthenticate { get; set; } + + /// + /// If set, this specifies the target scheme that this scheme should forward ChallengeAsync calls to. + /// For example Context.ChallengeAsync("ThisScheme") => Context.ChallengeAsync("ForwardChallengeValue"); + /// Set the target to the current scheme to disable forwarding and allow normal processing. + /// + public string ForwardChallenge { get; set; } + + /// + /// If set, this specifies the target scheme that this scheme should forward ForbidAsync calls to. + /// For example Context.ForbidAsync("ThisScheme") => Context.ForbidAsync("ForwardForbidValue"); + /// Set the target to the current scheme to disable forwarding and allow normal processing. + /// + public string ForwardForbid { get; set; } + + /// + /// If set, this specifies the target scheme that this scheme should forward SignInAsync calls to. + /// For example Context.SignInAsync("ThisScheme") => Context.SignInAsync("ForwardSignInValue"); + /// Set the target to the current scheme to disable forwarding and allow normal processing. + /// + public string ForwardSignIn { get; set; } + + /// + /// If set, this specifies the target scheme that this scheme should forward SignOutAsync calls to. + /// For example Context.SignOutAsync("ThisScheme") => Context.SignInAsync("ForwardSignOutValue"); + /// Set the target to the current scheme to disable forwarding and allow normal processing. + /// + public string ForwardSignOut { get; set; } + + /// + /// Used to select a default scheme for the current request that authentication handlers should forward all authentication operations to + /// by default. The default forwarding logic will check the most specific ForwardAuthenticate/Challenge/Forbid/SignIn/SignOut + /// setting first, followed by checking the ForwardDefaultSelector, followed by ForwardDefault. The first non null result + /// will be used as the target scheme to forward to. + /// + public Func ForwardDefaultSelector { get; set; } + + } +} diff --git a/src/Microsoft.AspNetCore.Authentication/AuthenticationServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Authentication/AuthenticationServiceCollectionExtensions.cs index 2aa320ae2..6c381b84a 100644 --- a/src/Microsoft.AspNetCore.Authentication/AuthenticationServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication/AuthenticationServiceCollectionExtensions.cs @@ -3,6 +3,8 @@ using System; using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection { @@ -11,31 +13,24 @@ namespace Microsoft.Extensions.DependencyInjection /// public static class AuthenticationServiceCollectionExtensions { - /// - /// Adds authentication services to the specified . - /// - /// The to add services to. - /// The so that additional calls can be chained. - public static IServiceCollection AddAuthentication(this IServiceCollection services) + public static AuthenticationBuilder AddAuthentication(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } - services.AddWebEncoders(); + services.AddAuthenticationCore(); services.AddDataProtection(); - return services; + services.AddWebEncoders(); + services.TryAddSingleton(); + return new AuthenticationBuilder(services); } - /// - /// Adds authentication services to the specified . - /// - /// The to add services to. - /// An action delegate to configure the provided . - /// The so that additional calls can be chained. - public static IServiceCollection AddAuthentication(this IServiceCollection services, Action configureOptions) - { + public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, string defaultScheme) + => services.AddAuthentication(o => o.DefaultScheme = defaultScheme); + + public static AuthenticationBuilder AddAuthentication(this IServiceCollection services, Action configureOptions) { if (services == null) { throw new ArgumentNullException(nameof(services)); @@ -46,8 +41,26 @@ public static IServiceCollection AddAuthentication(this IServiceCollection servi throw new ArgumentNullException(nameof(configureOptions)); } + var builder = services.AddAuthentication(); services.Configure(configureOptions); - return services.AddAuthentication(); + return builder; + } + + // Used to ensure that there's always a sign in scheme + private class EnsureSignInScheme : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions + { + private readonly AuthenticationOptions _authOptions; + + public EnsureSignInScheme(IOptions authOptions) + { + _authOptions = authOptions.Value; + } + + public void PostConfigure(string name, TOptions options) + { + options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme; + } } + } } diff --git a/src/Microsoft.AspNetCore.Authentication/AuthenticationTicket.cs b/src/Microsoft.AspNetCore.Authentication/AuthenticationTicket.cs deleted file mode 100644 index 1d56a8fb3..000000000 --- a/src/Microsoft.AspNetCore.Authentication/AuthenticationTicket.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Security.Claims; -using Microsoft.AspNetCore.Http.Authentication; - -namespace Microsoft.AspNetCore.Authentication -{ - /// - /// Contains user identity information as well as additional authentication state. - /// - public class AuthenticationTicket - { - /// - /// Initializes a new instance of the class - /// - /// the that represents the authenticated user. - /// additional properties that can be consumed by the user or runtime. - /// the authentication middleware that was responsible for this ticket. - public AuthenticationTicket(ClaimsPrincipal principal, AuthenticationProperties properties, string authenticationScheme) - { - if (principal == null) - { - throw new ArgumentNullException(nameof(principal)); - } - AuthenticationScheme = authenticationScheme; - Principal = principal; - Properties = properties ?? new AuthenticationProperties(); - } - - /// - /// Gets the authentication type. - /// - public string AuthenticationScheme { get; private set; } - - /// - /// Gets the claims-principal with authenticated user identities. - /// - public ClaimsPrincipal Principal{ get; private set; } - - /// - /// Additional state values for the authentication session. - /// - public AuthenticationProperties Properties { get; private set; } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationAppBuilderExtensions.cs deleted file mode 100644 index 1edb4a0f4..000000000 --- a/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationAppBuilderExtensions.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Builder -{ - /// - /// Extension methods to add claims transformation capabilities to an HTTP application pipeline. - /// - public static class ClaimsTransformationAppBuilderExtensions - { - /// - /// Adds the middleware to the specified , which enables claims transformation capabilities. - /// - /// The to add the middleware to. - /// A reference to this instance after the operation has completed. - public static IApplicationBuilder UseClaimsTransformation(this IApplicationBuilder app) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware(); - } - - /// - /// Adds the middleware to the specified , which enables claims transformation capabilities. - /// - /// The to add the middleware to. - /// A function that asynchronously transforms one to another. - /// A reference to this instance after the operation has completed. - public static IApplicationBuilder UseClaimsTransformation(this IApplicationBuilder app, Func> transform) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (transform == null) - { - throw new ArgumentNullException(nameof(transform)); - } - - return app.UseClaimsTransformation(new ClaimsTransformationOptions - { - Transformer = new ClaimsTransformer { OnTransform = transform } - }); - } - - /// - /// Adds the middleware to the specified , which enables claims transformation capabilities. - /// - /// The to add the middleware to. - /// The to configure the middleware with. - /// A reference to this instance after the operation has completed. - public static IApplicationBuilder UseClaimsTransformation(this IApplicationBuilder app, ClaimsTransformationOptions options) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - return app.UseMiddleware(Options.Create(options)); - } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationContext.cs b/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationContext.cs deleted file mode 100644 index 3c363ca98..000000000 --- a/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationContext.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System.Security.Claims; - -namespace Microsoft.AspNetCore.Authentication -{ - public class ClaimsTransformationContext - { - public ClaimsTransformationContext(HttpContext context) - { - Context = context; - } - public HttpContext Context { get; } - public ClaimsPrincipal Principal { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationHandler.cs b/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationHandler.cs deleted file mode 100644 index 7a2c47e40..000000000 --- a/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationHandler.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features.Authentication; - -namespace Microsoft.AspNetCore.Authentication -{ - /// - /// Handler that applies ClaimsTransformation to authentication - /// - public class ClaimsTransformationHandler : IAuthenticationHandler - { - private readonly IClaimsTransformer _transform; - private readonly HttpContext _httpContext; - - public ClaimsTransformationHandler(IClaimsTransformer transform, HttpContext httpContext) - { - _transform = transform; - _httpContext = httpContext; - } - - public IAuthenticationHandler PriorHandler { get; set; } - - public async Task AuthenticateAsync(AuthenticateContext context) - { - if (PriorHandler != null) - { - await PriorHandler.AuthenticateAsync(context); - if (_transform != null && context?.Principal != null) - { - var transformationContext = new ClaimsTransformationContext(_httpContext) - { - Principal = context.Principal - }; - context.Authenticated( - await _transform.TransformAsync(transformationContext), - context.Properties, - context.Description); - } - } - } - - public Task ChallengeAsync(ChallengeContext context) - { - if (PriorHandler != null) - { - return PriorHandler.ChallengeAsync(context); - } - return Task.FromResult(0); - } - - public void GetDescriptions(DescribeSchemesContext context) - { - if (PriorHandler != null) - { - PriorHandler.GetDescriptions(context); - } - } - - public Task SignInAsync(SignInContext context) - { - if (PriorHandler != null) - { - return PriorHandler.SignInAsync(context); - } - return Task.FromResult(0); - } - - public Task SignOutAsync(SignOutContext context) - { - if (PriorHandler != null) - { - return PriorHandler.SignOutAsync(context); - } - return Task.FromResult(0); - } - - public void RegisterAuthenticationHandler(IHttpAuthenticationFeature auth) - { - PriorHandler = auth.Handler; - auth.Handler = this; - } - - public void UnregisterAuthenticationHandler(IHttpAuthenticationFeature auth) - { - auth.Handler = PriorHandler; - } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationMiddleware.cs b/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationMiddleware.cs deleted file mode 100644 index 53f6a07a8..000000000 --- a/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationMiddleware.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Authentication -{ - public class ClaimsTransformationMiddleware - { - private readonly RequestDelegate _next; - - public ClaimsTransformationMiddleware( - RequestDelegate next, - IOptions options) - { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - Options = options.Value; - _next = next; - } - - public ClaimsTransformationOptions Options { get; set; } - - public async Task Invoke(HttpContext context) - { - var handler = new ClaimsTransformationHandler(Options.Transformer, context); - handler.RegisterAuthenticationHandler(context.GetAuthentication()); - try - { - if (Options.Transformer != null) - { - var transformationContext = new ClaimsTransformationContext(context) - { - Principal = context.User - }; - context.User = await Options.Transformer.TransformAsync(transformationContext); - } - await _next(context); - } - finally - { - handler.UnregisterAuthenticationHandler(context.GetAuthentication()); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationOptions.cs b/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationOptions.cs deleted file mode 100644 index 70a76f27c..000000000 --- a/src/Microsoft.AspNetCore.Authentication/ClaimsTransformationOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Authentication; - -namespace Microsoft.AspNetCore.Builder -{ - /// - /// Contains the options used by the . - /// - public class ClaimsTransformationOptions - { - /// - /// Responsible for transforming the claims principal. - /// - public IClaimsTransformer Transformer { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/ClaimsTransformer.cs b/src/Microsoft.AspNetCore.Authentication/ClaimsTransformer.cs deleted file mode 100644 index db05db0e5..000000000 --- a/src/Microsoft.AspNetCore.Authentication/ClaimsTransformer.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Security.Claims; -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Authentication -{ - public class ClaimsTransformer : IClaimsTransformer - { - public Func> OnTransform { get; set; } - - public virtual Task TransformAsync(ClaimsTransformationContext context) - { - return OnTransform?.Invoke(context) ?? Task.FromResult(context.Principal); - } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/DataHandler/IDataSerializer.cs b/src/Microsoft.AspNetCore.Authentication/Data/IDataSerializer.cs similarity index 100% rename from src/Microsoft.AspNetCore.Authentication/DataHandler/IDataSerializer.cs rename to src/Microsoft.AspNetCore.Authentication/Data/IDataSerializer.cs diff --git a/src/Microsoft.AspNetCore.Authentication/DataHandler/ISecureDataFormat.cs b/src/Microsoft.AspNetCore.Authentication/Data/ISecureDataFormat.cs similarity index 100% rename from src/Microsoft.AspNetCore.Authentication/DataHandler/ISecureDataFormat.cs rename to src/Microsoft.AspNetCore.Authentication/Data/ISecureDataFormat.cs diff --git a/src/Microsoft.AspNetCore.Authentication/DataHandler/PropertiesDataFormat.cs b/src/Microsoft.AspNetCore.Authentication/Data/PropertiesDataFormat.cs similarity index 100% rename from src/Microsoft.AspNetCore.Authentication/DataHandler/PropertiesDataFormat.cs rename to src/Microsoft.AspNetCore.Authentication/Data/PropertiesDataFormat.cs diff --git a/src/Microsoft.AspNetCore.Authentication/DataHandler/PropertiesSerializer.cs b/src/Microsoft.AspNetCore.Authentication/Data/PropertiesSerializer.cs similarity index 100% rename from src/Microsoft.AspNetCore.Authentication/DataHandler/PropertiesSerializer.cs rename to src/Microsoft.AspNetCore.Authentication/Data/PropertiesSerializer.cs diff --git a/src/Microsoft.AspNetCore.Authentication/DataHandler/SecureDataFormat.cs b/src/Microsoft.AspNetCore.Authentication/Data/SecureDataFormat.cs similarity index 100% rename from src/Microsoft.AspNetCore.Authentication/DataHandler/SecureDataFormat.cs rename to src/Microsoft.AspNetCore.Authentication/Data/SecureDataFormat.cs diff --git a/src/Microsoft.AspNetCore.Authentication/Data/TextEncoder.cs b/src/Microsoft.AspNetCore.Authentication/Data/TextEncoder.cs new file mode 100644 index 000000000..1f7ecc718 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/Data/TextEncoder.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication +{ + public static class Base64UrlTextEncoder + { + /// + /// Encodes supplied data into Base64 and replaces any URL encodable characters into non-URL encodable + /// characters. + /// + /// Data to be encoded. + /// Base64 encoded string modified with non-URL encodable characters + public static string Encode(byte[] data) + { + return WebUtilities.WebEncoders.Base64UrlEncode(data); + } + + /// + /// Decodes supplied string by replacing the non-URL encodable characters with URL encodable characters and + /// then decodes the Base64 string. + /// + /// The string to be decoded. + /// The decoded data. + public static byte[] Decode(string text) + { + return WebUtilities.WebEncoders.Base64UrlDecode(text); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication/DataHandler/TicketDataFormat.cs b/src/Microsoft.AspNetCore.Authentication/Data/TicketDataFormat.cs similarity index 100% rename from src/Microsoft.AspNetCore.Authentication/DataHandler/TicketDataFormat.cs rename to src/Microsoft.AspNetCore.Authentication/Data/TicketDataFormat.cs diff --git a/src/Microsoft.AspNetCore.Authentication/DataHandler/TicketSerializer.cs b/src/Microsoft.AspNetCore.Authentication/Data/TicketSerializer.cs similarity index 100% rename from src/Microsoft.AspNetCore.Authentication/DataHandler/TicketSerializer.cs rename to src/Microsoft.AspNetCore.Authentication/Data/TicketSerializer.cs diff --git a/src/Microsoft.AspNetCore.Authentication/DataHandler/TextEncoder.cs b/src/Microsoft.AspNetCore.Authentication/DataHandler/TextEncoder.cs deleted file mode 100644 index c07a314b0..000000000 --- a/src/Microsoft.AspNetCore.Authentication/DataHandler/TextEncoder.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.AspNetCore.Authentication -{ - public static class Base64UrlTextEncoder - { - public static string Encode(byte[] data) - { - return Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_'); - } - - public static byte[] Decode(string text) - { - return Convert.FromBase64String(Pad(text.Replace('-', '+').Replace('_', '/'))); - } - - private static string Pad(string text) - { - var padding = 3 - ((text.Length + 3) % 4); - if (padding == 0) - { - return text; - } - return text + new string('=', padding); - } - - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/Events/AccessDeniedContext.cs b/src/Microsoft.AspNetCore.Authentication/Events/AccessDeniedContext.cs new file mode 100644 index 000000000..f01d69453 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/Events/AccessDeniedContext.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Provides access denied failure context information to handler providers. + /// + public class AccessDeniedContext : HandleRequestContext + { + public AccessDeniedContext( + HttpContext context, + AuthenticationScheme scheme, + RemoteAuthenticationOptions options) + : base(context, scheme, options) + { + } + + /// + /// Gets or sets the endpoint path the user agent will be redirected to. + /// By default, this property is set to . + /// + public PathString AccessDeniedPath { get; set; } + + /// + /// Additional state values for the authentication session. + /// + public AuthenticationProperties Properties { get; set; } + + /// + /// Gets or sets the return URL that will be flowed up to the access denied page. + /// If is not set, this property is not used. + /// + public string ReturnUrl { get; set; } + + /// + /// Gets or sets the parameter name that will be used to flow the return URL. + /// By default, this property is set to . + /// + public string ReturnUrlParameter { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication/Events/BaseContext.cs b/src/Microsoft.AspNetCore.Authentication/Events/BaseContext.cs index 10b3325d4..915fc2377 100644 --- a/src/Microsoft.AspNetCore.Authentication/Events/BaseContext.cs +++ b/src/Microsoft.AspNetCore.Authentication/Events/BaseContext.cs @@ -1,27 +1,65 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Authentication { - public abstract class BaseContext + /// + /// Base class used by other context classes. + /// + public abstract class BaseContext where TOptions : AuthenticationSchemeOptions { - protected BaseContext(HttpContext context) + /// + /// Constructor. + /// + /// The context. + /// The authentication scheme. + /// The authentication options associated with the scheme. + protected BaseContext(HttpContext context, AuthenticationScheme scheme, TOptions options) { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + if (scheme == null) + { + throw new ArgumentNullException(nameof(scheme)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + HttpContext = context; + Scheme = scheme; + Options = options; } - public HttpContext HttpContext { get; private set; } + /// + /// The authentication scheme. + /// + public AuthenticationScheme Scheme { get; } - public HttpRequest Request - { - get { return HttpContext.Request; } - } + /// + /// Gets the authentication options associated with the scheme. + /// + public TOptions Options { get; } - public HttpResponse Response - { - get { return HttpContext.Response; } - } + /// + /// The context. + /// + public HttpContext HttpContext { get; } + + /// + /// The request. + /// + public HttpRequest Request => HttpContext.Request; + + /// + /// The response. + /// + public HttpResponse Response => HttpContext.Response; } } diff --git a/src/Microsoft.AspNetCore.Authentication/Events/BaseControlContext.cs b/src/Microsoft.AspNetCore.Authentication/Events/BaseControlContext.cs deleted file mode 100644 index 24f3ba8e5..000000000 --- a/src/Microsoft.AspNetCore.Authentication/Events/BaseControlContext.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Authentication -{ - public class BaseControlContext : BaseContext - { - protected BaseControlContext(HttpContext context) : base(context) - { - } - - public EventResultState State { get; set; } - - public bool HandledResponse - { - get { return State == EventResultState.HandledResponse; } - } - - public bool Skipped - { - get { return State == EventResultState.Skipped; } - } - - /// - /// Discontinue all processing for this request and return to the client. - /// The caller is responsible for generating the full response. - /// Set the to trigger SignIn. - /// - public void HandleResponse() - { - State = EventResultState.HandledResponse; - } - - /// - /// Discontinue processing the request in the current middleware and pass control to the next one. - /// SignIn will not be called. - /// - public void SkipToNextMiddleware() - { - State = EventResultState.Skipped; - } - - /// - /// Gets or set the to return if this event signals it handled the event. - /// - public AuthenticationTicket Ticket { get; set; } - - public bool CheckEventResult(out AuthenticateResult result) - { - if (HandledResponse) - { - result = AuthenticateResult.Success(Ticket); - return true; - } - else if (Skipped) - { - result = AuthenticateResult.Skip(); - return true; - } - result = null; - return false; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/Events/EventResultState.cs b/src/Microsoft.AspNetCore.Authentication/Events/EventResultState.cs deleted file mode 100644 index 80a6f949a..000000000 --- a/src/Microsoft.AspNetCore.Authentication/Events/EventResultState.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.AspNetCore.Authentication -{ - public enum EventResultState - { - /// - /// Continue with normal processing. - /// - Continue, - - /// - /// Discontinue processing the request in the current middleware and pass control to the next one. - /// - Skipped, - - /// - /// Discontinue all processing for this request. - /// - HandledResponse - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/Events/FailureContext.cs b/src/Microsoft.AspNetCore.Authentication/Events/FailureContext.cs deleted file mode 100644 index 35af9cee3..000000000 --- a/src/Microsoft.AspNetCore.Authentication/Events/FailureContext.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Authentication -{ - /// - /// Provides failure context information to middleware providers. - /// - public class FailureContext : BaseControlContext - { - public FailureContext(HttpContext context, Exception failure) - : base(context) - { - Failure = failure; - } - - /// - /// User friendly error message for the error. - /// - public Exception Failure { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/Events/HandleRequestContext.cs b/src/Microsoft.AspNetCore.Authentication/Events/HandleRequestContext.cs new file mode 100644 index 000000000..52dd9ce12 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/Events/HandleRequestContext.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + public class HandleRequestContext : BaseContext where TOptions : AuthenticationSchemeOptions + { + protected HandleRequestContext( + HttpContext context, + AuthenticationScheme scheme, + TOptions options) + : base(context, scheme, options) { } + + /// + /// The which is used by the handler. + /// + public HandleRequestResult Result { get; protected set; } + + /// + /// Discontinue all processing for this request and return to the client. + /// The caller is responsible for generating the full response. + /// + public void HandleResponse() => Result = HandleRequestResult.Handle(); + + /// + /// Discontinue processing the request in the current handler. + /// + public void SkipHandler() => Result = HandleRequestResult.SkipHandler(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/Events/IRemoteAuthenticationEvents.cs b/src/Microsoft.AspNetCore.Authentication/Events/IRemoteAuthenticationEvents.cs deleted file mode 100644 index e2109a065..000000000 --- a/src/Microsoft.AspNetCore.Authentication/Events/IRemoteAuthenticationEvents.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Authentication -{ - public interface IRemoteAuthenticationEvents - { - /// - /// Invoked when the remote authentication process has an error. - /// - Task RemoteFailure(FailureContext context); - - /// - /// Invoked before sign in. - /// - Task TicketReceived(TicketReceivedContext context); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/Events/PrincipalContext.cs b/src/Microsoft.AspNetCore.Authentication/Events/PrincipalContext.cs new file mode 100644 index 000000000..8bf40760a --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/Events/PrincipalContext.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Base context for authentication events which deal with a ClaimsPrincipal. + /// + public abstract class PrincipalContext : PropertiesContext where TOptions : AuthenticationSchemeOptions + { + /// + /// Constructor. + /// + /// The context. + /// The authentication scheme. + /// The authentication options associated with the scheme. + /// The authentication properties. + protected PrincipalContext(HttpContext context, AuthenticationScheme scheme, TOptions options, AuthenticationProperties properties) + : base(context, scheme, options, properties) { } + + /// + /// Gets the containing the user claims. + /// + public virtual ClaimsPrincipal Principal { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication/Events/PropertiesContext.cs b/src/Microsoft.AspNetCore.Authentication/Events/PropertiesContext.cs new file mode 100644 index 000000000..f1730d0d7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/Events/PropertiesContext.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Base context for authentication events which contain . + /// + public abstract class PropertiesContext : BaseContext where TOptions : AuthenticationSchemeOptions + { + /// + /// Constructor. + /// + /// The context. + /// The authentication scheme. + /// The authentication options associated with the scheme. + /// The authentication properties. + protected PropertiesContext(HttpContext context, AuthenticationScheme scheme, TOptions options, AuthenticationProperties properties) + : base(context, scheme, options) + { + Properties = properties ?? new AuthenticationProperties(); + } + + /// + /// Gets or sets the . + /// + public virtual AuthenticationProperties Properties { get; protected set; } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieRedirectContext.cs b/src/Microsoft.AspNetCore.Authentication/Events/RedirectContext.cs similarity index 56% rename from src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieRedirectContext.cs rename to src/Microsoft.AspNetCore.Authentication/Events/RedirectContext.cs index 2cbb5ff09..dac24cafa 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieRedirectContext.cs +++ b/src/Microsoft.AspNetCore.Authentication/Events/RedirectContext.cs @@ -1,36 +1,38 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -namespace Microsoft.AspNetCore.Authentication.Cookies +namespace Microsoft.AspNetCore.Authentication { /// - /// Context passed when a Challenge, SignIn, or SignOut causes a redirect in the cookie middleware + /// Context passed for redirect events. /// - public class CookieRedirectContext : BaseCookieContext + public class RedirectContext : PropertiesContext where TOptions : AuthenticationSchemeOptions { /// /// Creates a new context object. /// /// The HTTP request context - /// The cookie middleware options + /// The scheme data + /// The handler options /// The initial redirect URI /// The . - public CookieRedirectContext(HttpContext context, CookieAuthenticationOptions options, string redirectUri, AuthenticationProperties properties) - : base(context, options) + public RedirectContext( + HttpContext context, + AuthenticationScheme scheme, + TOptions options, + AuthenticationProperties properties, + string redirectUri) + : base(context, scheme, options, properties) { - RedirectUri = redirectUri; Properties = properties; + RedirectUri = redirectUri; } /// /// Gets or Sets the URI used for the redirect operation. /// public string RedirectUri { get; set; } - - public AuthenticationProperties Properties { get; } } } diff --git a/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationContext.cs b/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationContext.cs new file mode 100644 index 000000000..b7a016879 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationContext.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Base context for remote authentication. + /// + public abstract class RemoteAuthenticationContext : HandleRequestContext where TOptions : AuthenticationSchemeOptions + { + /// + /// Constructor. + /// + /// The context. + /// The authentication scheme. + /// The authentication options associated with the scheme. + /// The authentication properties. + protected RemoteAuthenticationContext( + HttpContext context, + AuthenticationScheme scheme, + TOptions options, + AuthenticationProperties properties) + : base(context, scheme, options) + => Properties = properties ?? new AuthenticationProperties(); + + /// + /// Gets the containing the user claims. + /// + public ClaimsPrincipal Principal { get; set; } + + /// + /// Gets or sets the . + /// + public virtual AuthenticationProperties Properties { get; set; } + + /// + /// Calls success creating a ticket with the and . + /// + public void Success() => Result = HandleRequestResult.Success(new AuthenticationTicket(Principal, Properties, Scheme.Name)); + + public void Fail(Exception failure) => Result = HandleRequestResult.Fail(failure); + + public void Fail(string failureMessage) => Result = HandleRequestResult.Fail(failureMessage); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.cs b/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.cs index ee45b8afd..be4c6ab49 100644 --- a/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.cs +++ b/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.cs @@ -6,16 +6,22 @@ namespace Microsoft.AspNetCore.Authentication { - public class RemoteAuthenticationEvents : IRemoteAuthenticationEvents + public class RemoteAuthenticationEvents { - public Func OnRemoteFailure { get; set; } = context => Task.FromResult(0); + public Func OnAccessDenied { get; set; } = context => Task.CompletedTask; + public Func OnRemoteFailure { get; set; } = context => Task.CompletedTask; - public Func OnTicketReceived { get; set; } = context => Task.FromResult(0); + public Func OnTicketReceived { get; set; } = context => Task.CompletedTask; /// - /// Invoked when there is a remote failure + /// Invoked when an access denied error was returned by the remote server. /// - public virtual Task RemoteFailure(FailureContext context) => OnRemoteFailure(context); + public virtual Task AccessDenied(AccessDeniedContext context) => OnAccessDenied(context); + + /// + /// Invoked when there is a remote failure. + /// + public virtual Task RemoteFailure(RemoteFailureContext context) => OnRemoteFailure(context); /// /// Invoked after the remote ticket has been received. diff --git a/src/Microsoft.AspNetCore.Authentication/Events/RemoteFailureContext.cs b/src/Microsoft.AspNetCore.Authentication/Events/RemoteFailureContext.cs new file mode 100644 index 000000000..6b3598f40 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/Events/RemoteFailureContext.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Provides failure context information to handler providers. + /// + public class RemoteFailureContext : HandleRequestContext + { + public RemoteFailureContext( + HttpContext context, + AuthenticationScheme scheme, + RemoteAuthenticationOptions options, + Exception failure) + : base(context, scheme, options) + { + Failure = failure; + } + + /// + /// User friendly error message for the error. + /// + public Exception Failure { get; set; } + + /// + /// Additional state values for the authentication session. + /// + public AuthenticationProperties Properties { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication/Events/ResultContext.cs b/src/Microsoft.AspNetCore.Authentication/Events/ResultContext.cs new file mode 100644 index 000000000..12b21f4bf --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/Events/ResultContext.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Base context for events that produce AuthenticateResults. + /// + public abstract class ResultContext : BaseContext where TOptions : AuthenticationSchemeOptions + { + /// + /// Constructor. + /// + /// The context. + /// The authentication scheme. + /// The authentication options associated with the scheme. + protected ResultContext(HttpContext context, AuthenticationScheme scheme, TOptions options) + : base(context, scheme, options) { } + + /// + /// Gets or sets the containing the user claims. + /// + public ClaimsPrincipal Principal { get; set; } + + private AuthenticationProperties _properties; + /// + /// Gets or sets the . + /// + public AuthenticationProperties Properties { + get => _properties ?? (_properties = new AuthenticationProperties()); + set => _properties = value; + } + + /// + /// Gets the result. + /// + public AuthenticateResult Result { get; private set; } + + /// + /// Calls success creating a ticket with the and . + /// + public void Success() => Result = HandleRequestResult.Success(new AuthenticationTicket(Principal, Properties, Scheme.Name)); + + /// + /// Indicates that there was no information returned for this authentication scheme. + /// + public void NoResult() => Result = AuthenticateResult.NoResult(); + + /// + /// Indicates that there was a failure during authentication. + /// + /// + public void Fail(Exception failure) => Result = AuthenticateResult.Fail(failure); + + /// + /// Indicates that there was a failure during authentication. + /// + /// + public void Fail(string failureMessage) => Result = AuthenticateResult.Fail(failureMessage); + } +} diff --git a/src/Microsoft.AspNetCore.Authentication/Events/TicketReceivedContext.cs b/src/Microsoft.AspNetCore.Authentication/Events/TicketReceivedContext.cs index 5d5fd4883..51b77a37f 100644 --- a/src/Microsoft.AspNetCore.Authentication/Events/TicketReceivedContext.cs +++ b/src/Microsoft.AspNetCore.Authentication/Events/TicketReceivedContext.cs @@ -2,32 +2,22 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Security.Claims; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; namespace Microsoft.AspNetCore.Authentication { /// - /// Provides context information to middleware providers. + /// Provides context information to handler providers. /// - public class TicketReceivedContext : BaseControlContext + public class TicketReceivedContext : RemoteAuthenticationContext { - public TicketReceivedContext(HttpContext context, RemoteAuthenticationOptions options, AuthenticationTicket ticket) - : base(context) - { - Options = options; - Ticket = ticket; - if (ticket != null) - { - Principal = ticket.Principal; - Properties = ticket.Properties; - } - } - - public ClaimsPrincipal Principal { get; set; } - public AuthenticationProperties Properties { get; set; } - public RemoteAuthenticationOptions Options { get; set; } + public TicketReceivedContext( + HttpContext context, + AuthenticationScheme scheme, + RemoteAuthenticationOptions options, + AuthenticationTicket ticket) + : base(context, scheme, options, ticket?.Properties) + => Principal = ticket?.Principal; public string ReturnUri { get; set; } } diff --git a/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs b/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs new file mode 100644 index 000000000..da9b6ea01 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Contains the result of an Authenticate call + /// + public class HandleRequestResult : AuthenticateResult + { + /// + /// Indicates that stage of authentication was directly handled by + /// user intervention and no further processing should be attempted. + /// + public bool Handled { get; private set; } + + /// + /// Indicates that the default authentication logic should be + /// skipped and that the rest of the pipeline should be invoked. + /// + public bool Skipped { get; private set; } + + /// + /// Indicates that authentication was successful. + /// + /// The ticket representing the authentication result. + /// The result. + public static new HandleRequestResult Success(AuthenticationTicket ticket) + { + if (ticket == null) + { + throw new ArgumentNullException(nameof(ticket)); + } + return new HandleRequestResult() { Ticket = ticket, Properties = ticket.Properties }; + } + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure exception. + /// The result. + public static new HandleRequestResult Fail(Exception failure) + { + return new HandleRequestResult() { Failure = failure }; + } + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure exception. + /// Additional state values for the authentication session. + /// The result. + public static new HandleRequestResult Fail(Exception failure, AuthenticationProperties properties) + { + return new HandleRequestResult() { Failure = failure, Properties = properties }; + } + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure message. + /// The result. + public static new HandleRequestResult Fail(string failureMessage) + => Fail(new Exception(failureMessage)); + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure message. + /// Additional state values for the authentication session. + /// The result. + public static new HandleRequestResult Fail(string failureMessage, AuthenticationProperties properties) + => Fail(new Exception(failureMessage), properties); + + /// + /// Discontinue all processing for this request and return to the client. + /// The caller is responsible for generating the full response. + /// + /// The result. + public static HandleRequestResult Handle() + { + return new HandleRequestResult() { Handled = true }; + } + + /// + /// Discontinue processing the request in the current handler. + /// + /// The result. + public static HandleRequestResult SkipHandler() + { + return new HandleRequestResult() { Skipped = true }; + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication/HttpContextExtensions.cs b/src/Microsoft.AspNetCore.Authentication/HttpContextExtensions.cs deleted file mode 100644 index 0d245cf0a..000000000 --- a/src/Microsoft.AspNetCore.Authentication/HttpContextExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features.Authentication; - -namespace Microsoft.AspNetCore.Authentication -{ - internal static class HttpContextExtensions - { - internal static IHttpAuthenticationFeature GetAuthentication(this HttpContext context) - { - var auth = context.Features.Get(); - if (auth == null) - { - auth = new HttpAuthenticationFeature(); - context.Features.Set(auth); - } - return auth; - } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/IClaimsTransformer.cs b/src/Microsoft.AspNetCore.Authentication/IClaimsTransformer.cs deleted file mode 100644 index cd42915c0..000000000 --- a/src/Microsoft.AspNetCore.Authentication/IClaimsTransformer.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Security.Claims; -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Authentication -{ - /// - /// Used for claims transformation. - /// - public interface IClaimsTransformer - { - /// - /// Provides a central transformation point to change the specified principal. - /// - /// containing principal to transform and current HttpContext. - /// The transformed principal. - Task TransformAsync(ClaimsTransformationContext context); - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/Internal/RequestPathBaseCookieBuilder.cs b/src/Microsoft.AspNetCore.Authentication/Internal/RequestPathBaseCookieBuilder.cs new file mode 100644 index 000000000..f42617cb2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/Internal/RequestPathBaseCookieBuilder.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Internal +{ + /// + /// A cookie builder that sets to the request path base. + /// + public class RequestPathBaseCookieBuilder : CookieBuilder + { + /// + /// Gets an optional value that is appended to the request path base. + /// + protected virtual string AdditionalPath { get; } + + public override CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom) + { + // check if the user has overridden the default value of path. If so, use that instead of our default value. + var path = Path; + if (path == null) + { + var originalPathBase = context.Features.Get()?.OriginalPathBase ?? context.Request.PathBase; + path = originalPathBase + AdditionalPath; + } + + var options = base.Build(context, expiresFrom); + + options.Path = !string.IsNullOrEmpty(path) + ? path + : "/"; + + return options; + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs b/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs index 49fc8db05..042bfabca 100644 --- a/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs @@ -7,37 +7,23 @@ namespace Microsoft.Extensions.Logging { internal static class LoggingExtensions { - private static Action _authSchemeAuthenticated; - private static Action _authSchemeNotAuthenticated; - private static Action _authSchemeNotAuthenticatedWithFailure; - private static Action _authSchemeSignedIn; - private static Action _authSchemeSignedOut; - private static Action _authSchemeChallenged; - private static Action _authSchemeForbidden; - private static Action _userAuthorizationFailed; - private static Action _userAuthorizationSucceeded; - private static Action _userPrincipalMerged; - private static Action _remoteAuthenticationError; - private static Action _signInHandled; - private static Action _signInSkipped; - private static Action _correlationPropertyNotFound; - private static Action _correlationCookieNotFound; - private static Action _unexpectedCorrelationCookieValue; + private static readonly Action _authSchemeAuthenticated; + private static readonly Action _authSchemeNotAuthenticated; + private static readonly Action _authSchemeNotAuthenticatedWithFailure; + private static readonly Action _authSchemeChallenged; + private static readonly Action _authSchemeForbidden; + private static readonly Action _remoteAuthenticationError; + private static readonly Action _signInHandled; + private static readonly Action _signInSkipped; + private static readonly Action _correlationPropertyNotFound; + private static readonly Action _correlationCookieNotFound; + private static readonly Action _unexpectedCorrelationCookieValue; + private static readonly Action _accessDeniedError; + private static readonly Action _accessDeniedContextHandled; + private static readonly Action _accessDeniedContextSkipped; static LoggingExtensions() { - _userAuthorizationSucceeded = LoggerMessage.Define( - eventId: 1, - logLevel: LogLevel.Information, - formatString: "Authorization was successful for user: {UserName}."); - _userAuthorizationFailed = LoggerMessage.Define( - eventId: 2, - logLevel: LogLevel.Information, - formatString: "Authorization failed for user: {UserName}."); - _userPrincipalMerged = LoggerMessage.Define( - eventId: 3, - logLevel: LogLevel.Information, - formatString: "HttpContext.User merged via AutomaticAuthentication from authenticationScheme: {AuthenticationScheme}."); _remoteAuthenticationError = LoggerMessage.Define( eventId: 4, logLevel: LogLevel.Information, @@ -56,20 +42,12 @@ static LoggingExtensions() formatString: "{AuthenticationScheme} was not authenticated. Failure message: {FailureMessage}"); _authSchemeAuthenticated = LoggerMessage.Define( eventId: 8, - logLevel: LogLevel.Information, + logLevel: LogLevel.Debug, formatString: "AuthenticationScheme: {AuthenticationScheme} was successfully authenticated."); _authSchemeNotAuthenticated = LoggerMessage.Define( eventId: 9, logLevel: LogLevel.Debug, formatString: "AuthenticationScheme: {AuthenticationScheme} was not authenticated."); - _authSchemeSignedIn = LoggerMessage.Define( - eventId: 10, - logLevel: LogLevel.Information, - formatString: "AuthenticationScheme: {AuthenticationScheme} signed in."); - _authSchemeSignedOut = LoggerMessage.Define( - eventId: 11, - logLevel: LogLevel.Information, - formatString: "AuthenticationScheme: {AuthenticationScheme} signed out."); _authSchemeChallenged = LoggerMessage.Define( eventId: 12, logLevel: LogLevel.Information, @@ -90,6 +68,18 @@ static LoggingExtensions() eventId: 16, logLevel: LogLevel.Warning, formatString: "The correlation cookie value '{CorrelationCookieName}' did not match the expected value '{CorrelationCookieValue}'."); + _accessDeniedError = LoggerMessage.Define( + eventId: 17, + logLevel: LogLevel.Information, + formatString: "Access was denied by the resource owner or by the remote server."); + _accessDeniedContextHandled = LoggerMessage.Define( + eventId: 18, + logLevel: LogLevel.Debug, + formatString: "The AccessDenied event returned Handled."); + _accessDeniedContextSkipped = LoggerMessage.Define( + eventId: 19, + logLevel: LogLevel.Debug, + formatString: "The AccessDenied event returned Skipped."); } public static void AuthenticationSchemeAuthenticated(this ILogger logger, string authenticationScheme) @@ -107,16 +97,6 @@ public static void AuthenticationSchemeNotAuthenticatedWithFailure(this ILogger _authSchemeNotAuthenticatedWithFailure(logger, authenticationScheme, failureMessage, null); } - public static void AuthenticationSchemeSignedIn(this ILogger logger, string authenticationScheme) - { - _authSchemeSignedIn(logger, authenticationScheme, null); - } - - public static void AuthenticationSchemeSignedOut(this ILogger logger, string authenticationScheme) - { - _authSchemeSignedOut(logger, authenticationScheme, null); - } - public static void AuthenticationSchemeChallenged(this ILogger logger, string authenticationScheme) { _authSchemeChallenged(logger, authenticationScheme, null); @@ -127,21 +107,6 @@ public static void AuthenticationSchemeForbidden(this ILogger logger, string aut _authSchemeForbidden(logger, authenticationScheme, null); } - public static void UserAuthorizationSucceeded(this ILogger logger, string userName) - { - _userAuthorizationSucceeded(logger, userName, null); - } - - public static void UserAuthorizationFailed(this ILogger logger, string userName) - { - _userAuthorizationFailed(logger, userName, null); - } - - public static void UserPrinicpalMerged(this ILogger logger, string authenticationScheme) - { - _userPrincipalMerged(logger, authenticationScheme, null); - } - public static void RemoteAuthenticationError(this ILogger logger, string errorMessage) { _remoteAuthenticationError(logger, errorMessage, null); @@ -171,5 +136,20 @@ public static void UnexpectedCorrelationCookieValue(this ILogger logger, string { _unexpectedCorrelationCookieValue(logger, cookieName, cookieValue, null); } + + public static void AccessDeniedError(this ILogger logger) + { + _accessDeniedError(logger, null); + } + + public static void AccessDeniedContextHandled(this ILogger logger) + { + _accessDeniedContextHandled(logger, null); + } + + public static void AccessDeniedContextSkipped(this ILogger logger) + { + _accessDeniedContextSkipped(logger, null); + } } } diff --git a/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj b/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj new file mode 100644 index 000000000..9ae0a7bcc --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj @@ -0,0 +1,22 @@ + + + + ASP.NET Core common types used by the various authentication middleware components. + netcoreapp3.0 + $(NoWarn);CS1591 + true + aspnetcore;authentication;security + + + + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.xproj b/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.xproj deleted file mode 100644 index e03db0847..000000000 --- a/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.xproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 2286250a-52c8-4126-9f93-b1e45f0ad078 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/PolicySchemeHandler.cs b/src/Microsoft.AspNetCore.Authentication/PolicySchemeHandler.cs new file mode 100644 index 000000000..4dbbb7de2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/PolicySchemeHandler.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// PolicySchemes are used to redirect authentication methods to another scheme. + /// + public class PolicySchemeHandler : SignInAuthenticationHandler + { + public PolicySchemeHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { } + + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + => throw new NotImplementedException(); + + protected override Task HandleForbiddenAsync(AuthenticationProperties properties) + => throw new NotImplementedException(); + + protected override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + => throw new NotImplementedException(); + + protected override Task HandleSignOutAsync(AuthenticationProperties properties) + => throw new NotImplementedException(); + + protected override Task HandleAuthenticateAsync() + => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/AuthenticationToken.cs b/src/Microsoft.AspNetCore.Authentication/PolicySchemeOptions.cs similarity index 53% rename from src/Microsoft.AspNetCore.Authentication/AuthenticationToken.cs rename to src/Microsoft.AspNetCore.Authentication/PolicySchemeOptions.cs index 6503f0bb8..1921c77ec 100644 --- a/src/Microsoft.AspNetCore.Authentication/AuthenticationToken.cs +++ b/src/Microsoft.AspNetCore.Authentication/PolicySchemeOptions.cs @@ -1,12 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - namespace Microsoft.AspNetCore.Authentication { - public class AuthenticationToken - { - public string Name { get; set; } - public string Value { get; set; } - } -} \ No newline at end of file + /// + /// Contains the options used by the . + /// + public class PolicySchemeOptions : AuthenticationSchemeOptions + { } +} diff --git a/src/Microsoft.AspNetCore.Authentication/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Authentication/Properties/AssemblyInfo.cs deleted file mode 100644 index 76feceeff..000000000 --- a/src/Microsoft.AspNetCore.Authentication/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Reflection; -using System.Resources; - -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: AssemblyCompany("Microsoft Corporation.")] -[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Authentication/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Authentication/Properties/Resources.Designer.cs index 29dd7c67e..b1941a7dc 100644 --- a/src/Microsoft.AspNetCore.Authentication/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Authentication/Properties/Resources.Designer.cs @@ -15,49 +15,71 @@ private static readonly ResourceManager _resourceManager /// internal static string Exception_DefaultDpapiRequiresAppNameKey { - get { return GetString("Exception_DefaultDpapiRequiresAppNameKey"); } + get => GetString("Exception_DefaultDpapiRequiresAppNameKey"); } /// /// The default data protection provider may only be used when the IApplicationBuilder.Properties contains an appropriate 'host.AppName' key. /// internal static string FormatException_DefaultDpapiRequiresAppNameKey() - { - return GetString("Exception_DefaultDpapiRequiresAppNameKey"); - } + => GetString("Exception_DefaultDpapiRequiresAppNameKey"); /// /// The state passed to UnhookAuthentication may only be the return value from HookAuthentication. /// internal static string Exception_UnhookAuthenticationStateType { - get { return GetString("Exception_UnhookAuthenticationStateType"); } + get => GetString("Exception_UnhookAuthenticationStateType"); } /// /// The state passed to UnhookAuthentication may only be the return value from HookAuthentication. /// internal static string FormatException_UnhookAuthenticationStateType() - { - return GetString("Exception_UnhookAuthenticationStateType"); - } + => GetString("Exception_UnhookAuthenticationStateType"); /// /// The AuthenticationTokenProvider's required synchronous events have not been registered. /// internal static string Exception_AuthenticationTokenDoesNotProvideSyncMethods { - get { return GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods"); } + get => GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods"); } /// /// The AuthenticationTokenProvider's required synchronous events have not been registered. /// internal static string FormatException_AuthenticationTokenDoesNotProvideSyncMethods() + => GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods"); + + /// + /// The '{0}' option must be provided. + /// + internal static string Exception_OptionMustBeProvided + { + get => GetString("Exception_OptionMustBeProvided"); + } + + /// + /// The '{0}' option must be provided. + /// + internal static string FormatException_OptionMustBeProvided(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_OptionMustBeProvided"), p0); + + /// + /// The SignInScheme for a remote authentication handler cannot be set to itself. If it was not explicitly set, the AuthenticationOptions.DefaultSignInScheme or DefaultScheme is used. + /// + internal static string Exception_RemoteSignInSchemeCannotBeSelf { - return GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods"); + get => GetString("Exception_RemoteSignInSchemeCannotBeSelf"); } + /// + /// The SignInScheme for a remote authentication handler cannot be set to itself. If it was not explicitly set, the AuthenticationOptions.DefaultSignInScheme or DefaultScheme is used. + /// + internal static string FormatException_RemoteSignInSchemeCannotBeSelf() + => GetString("Exception_RemoteSignInSchemeCannotBeSelf"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs b/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs index d6cc4ce7f..c72583b5c 100644 --- a/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs @@ -3,112 +3,189 @@ using System; using System.Security.Cryptography; +using System.Text.Encodings.Web; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features.Authentication; -using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Authentication { - public abstract class RemoteAuthenticationHandler : AuthenticationHandler where TOptions : RemoteAuthenticationOptions + public abstract class RemoteAuthenticationHandler : AuthenticationHandler, IAuthenticationRequestHandler + where TOptions : RemoteAuthenticationOptions, new() { - private const string CorrelationPrefix = ".AspNetCore.Correlation."; private const string CorrelationProperty = ".xsrf"; private const string CorrelationMarker = "N"; + private const string AuthSchemeKey = ".AuthScheme"; private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create(); - public override async Task HandleRequestAsync() + protected string SignInScheme => Options.SignInScheme; + + /// + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// + protected new RemoteAuthenticationEvents Events { - if (Options.CallbackPath == Request.Path) - { - return await HandleRemoteCallbackAsync(); - } - return false; + get { return (RemoteAuthenticationEvents)base.Events; } + set { base.Events = value; } } - protected virtual async Task HandleRemoteCallbackAsync() + protected RemoteAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) { } + + protected override Task CreateEventsAsync() + => Task.FromResult(new RemoteAuthenticationEvents()); + + public virtual Task ShouldHandleRequestAsync() + => Task.FromResult(Options.CallbackPath == Request.Path); + + public virtual async Task HandleRequestAsync() { - var authResult = await HandleRemoteAuthenticateAsync(); - if (authResult != null && authResult.Skipped) + if (!await ShouldHandleRequestAsync()) { return false; } - if (authResult == null || !authResult.Succeeded) + + AuthenticationTicket ticket = null; + Exception exception = null; + AuthenticationProperties properties = null; + try { - var errorContext = new FailureContext(Context, authResult?.Failure ?? new Exception("Invalid return state, unable to redirect.")); - Logger.RemoteAuthenticationError(errorContext.Failure.Message); - await Options.Events.RemoteFailure(errorContext); - if (errorContext.HandledResponse) + var authResult = await HandleRemoteAuthenticateAsync(); + if (authResult == null) + { + exception = new InvalidOperationException("Invalid return state, unable to redirect."); + } + else if (authResult.Handled) { return true; } - if (errorContext.Skipped) + else if (authResult.Skipped || authResult.None) { return false; } + else if (!authResult.Succeeded) + { + exception = authResult.Failure ?? new InvalidOperationException("Invalid return state, unable to redirect."); + properties = authResult.Properties; + } + + ticket = authResult?.Ticket; + } + catch (Exception ex) + { + exception = ex; + } + + if (exception != null) + { + Logger.RemoteAuthenticationError(exception.Message); + var errorContext = new RemoteFailureContext(Context, Scheme, Options, exception) + { + Properties = properties + }; + await Events.RemoteFailure(errorContext); + + if (errorContext.Result != null) + { + if (errorContext.Result.Handled) + { + return true; + } + else if (errorContext.Result.Skipped) + { + return false; + } + else if (errorContext.Result.Failure != null) + { + throw new Exception("An error was returned from the RemoteFailure event.", errorContext.Result.Failure); + } + } - throw new AggregateException("Unhandled remote failure.", errorContext.Failure); + if (errorContext.Failure != null) + { + throw new Exception("An error was encountered while handling the remote login.", errorContext.Failure); + } } // We have a ticket if we get here - var ticket = authResult.Ticket; - var context = new TicketReceivedContext(Context, Options, ticket) + var ticketContext = new TicketReceivedContext(Context, Scheme, Options, ticket) { - ReturnUri = ticket.Properties.RedirectUri, + ReturnUri = ticket.Properties.RedirectUri }; - // REVIEW: is this safe or good? + ticket.Properties.RedirectUri = null; - await Options.Events.TicketReceived(context); + // Mark which provider produced this identity so we can cross-check later in HandleAuthenticateAsync + ticketContext.Properties.Items[AuthSchemeKey] = Scheme.Name; - if (context.HandledResponse) - { - Logger.SigninHandled(); - return true; - } - else if (context.Skipped) + await Events.TicketReceived(ticketContext); + + if (ticketContext.Result != null) { - Logger.SigninSkipped(); - return false; + if (ticketContext.Result.Handled) + { + Logger.SigninHandled(); + return true; + } + else if (ticketContext.Result.Skipped) + { + Logger.SigninSkipped(); + return false; + } } - await Context.Authentication.SignInAsync(Options.SignInScheme, context.Principal, context.Properties); + await Context.SignInAsync(SignInScheme, ticketContext.Principal, ticketContext.Properties); // Default redirect path is the base path - if (string.IsNullOrEmpty(context.ReturnUri)) + if (string.IsNullOrEmpty(ticketContext.ReturnUri)) { - context.ReturnUri = "/"; + ticketContext.ReturnUri = "/"; } - Response.Redirect(context.ReturnUri); + Response.Redirect(ticketContext.ReturnUri); return true; } - protected abstract Task HandleRemoteAuthenticateAsync(); + /// + /// Authenticate the user identity with the identity provider. + /// + /// The method process the request on the endpoint defined by CallbackPath. + /// + protected abstract Task HandleRemoteAuthenticateAsync(); - protected override Task HandleAuthenticateAsync() + protected override async Task HandleAuthenticateAsync() { - return Task.FromResult(AuthenticateResult.Fail("Remote authentication does not support authenticate")); - } + var result = await Context.AuthenticateAsync(SignInScheme); + if (result != null) + { + if (result.Failure != null) + { + return result; + } - protected override Task HandleSignOutAsync(SignOutContext context) - { - throw new NotSupportedException(); - } + // The SignInScheme may be shared with multiple providers, make sure this provider issued the identity. + string authenticatedScheme; + var ticket = result.Ticket; + if (ticket != null && ticket.Principal != null && ticket.Properties != null + && ticket.Properties.Items.TryGetValue(AuthSchemeKey, out authenticatedScheme) + && string.Equals(Scheme.Name, authenticatedScheme, StringComparison.Ordinal)) + { + return AuthenticateResult.Success(new AuthenticationTicket(ticket.Principal, + ticket.Properties, Scheme.Name)); + } - protected override Task HandleSignInAsync(SignInContext context) - { - throw new NotSupportedException(); - } + return AuthenticateResult.Fail("Not authenticated"); + } - protected override Task HandleForbiddenAsync(ChallengeContext context) - { - throw new NotSupportedException(); + return AuthenticateResult.Fail("Remote authentication does not directly support AuthenticateAsync"); } + protected override Task HandleForbiddenAsync(AuthenticationProperties properties) + => Context.ForbidAsync(SignInScheme); + protected virtual void GenerateCorrelationId(AuthenticationProperties properties) { if (properties == null) @@ -120,16 +197,11 @@ protected virtual void GenerateCorrelationId(AuthenticationProperties properties CryptoRandom.GetBytes(bytes); var correlationId = Base64UrlTextEncoder.Encode(bytes); - var cookieOptions = new CookieOptions - { - HttpOnly = true, - Secure = Request.IsHttps, - Expires = properties.ExpiresUtc - }; + var cookieOptions = Options.CorrelationCookie.Build(Context, Clock.UtcNow); properties.Items[CorrelationProperty] = correlationId; - var cookieName = CorrelationPrefix + Options.AuthenticationScheme + "." + correlationId; + var cookieName = Options.CorrelationCookie.Name + Scheme.Name + "." + correlationId; Response.Cookies.Append(cookieName, CorrelationMarker, cookieOptions); } @@ -141,16 +213,15 @@ protected virtual bool ValidateCorrelationId(AuthenticationProperties properties throw new ArgumentNullException(nameof(properties)); } - string correlationId; - if (!properties.Items.TryGetValue(CorrelationProperty, out correlationId)) + if (!properties.Items.TryGetValue(CorrelationProperty, out string correlationId)) { - Logger.CorrelationPropertyNotFound(CorrelationPrefix); + Logger.CorrelationPropertyNotFound(Options.CorrelationCookie.Name); return false; } properties.Items.Remove(CorrelationProperty); - var cookieName = CorrelationPrefix + Options.AuthenticationScheme + "." + correlationId; + var cookieName = Options.CorrelationCookie.Name + Scheme.Name + "." + correlationId; var correlationCookie = Request.Cookies[cookieName]; if (string.IsNullOrEmpty(correlationCookie)) @@ -159,11 +230,8 @@ protected virtual bool ValidateCorrelationId(AuthenticationProperties properties return false; } - var cookieOptions = new CookieOptions - { - HttpOnly = true, - Secure = Request.IsHttps - }; + var cookieOptions = Options.CorrelationCookie.Build(Context, Clock.UtcNow); + Response.Cookies.Delete(cookieName, cookieOptions); if (!string.Equals(correlationCookie, CorrelationMarker, StringComparison.Ordinal)) @@ -174,5 +242,48 @@ protected virtual bool ValidateCorrelationId(AuthenticationProperties properties return true; } + + protected virtual async Task HandleAccessDeniedErrorAsync(AuthenticationProperties properties) + { + Logger.AccessDeniedError(); + var context = new AccessDeniedContext(Context, Scheme, Options) + { + AccessDeniedPath = Options.AccessDeniedPath, + Properties = properties, + ReturnUrl = properties?.RedirectUri, + ReturnUrlParameter = Options.ReturnUrlParameter + }; + await Events.AccessDenied(context); + + if (context.Result != null) + { + if (context.Result.Handled) + { + Logger.AccessDeniedContextHandled(); + } + else if (context.Result.Skipped) + { + Logger.AccessDeniedContextSkipped(); + } + + return context.Result; + } + + // If an access denied endpoint was specified, redirect the user agent. + // Otherwise, invoke the RemoteFailure event for further processing. + if (context.AccessDeniedPath.HasValue) + { + string uri = context.AccessDeniedPath; + if (!string.IsNullOrEmpty(context.ReturnUrlParameter) && !string.IsNullOrEmpty(context.ReturnUrl)) + { + uri = QueryHelpers.AddQueryString(uri, context.ReturnUrlParameter, context.ReturnUrl); + } + Response.Redirect(uri); + + return HandleRequestResult.Handle(); + } + + return HandleRequestResult.Fail("Access was denied by the resource owner or by the remote server.", properties); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs b/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs index a78c14bbe..188b7a991 100644 --- a/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs @@ -3,18 +3,63 @@ using System; using System.Net.Http; +using Microsoft.AspNetCore.Authentication.Internal; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Authentication; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Authentication { /// /// Contains the options used by the . /// - public class RemoteAuthenticationOptions : AuthenticationOptions + public class RemoteAuthenticationOptions : AuthenticationSchemeOptions { + private const string CorrelationPrefix = ".AspNetCore.Correlation."; + + private CookieBuilder _correlationCookieBuilder; + + /// + /// Initializes a new . + /// + public RemoteAuthenticationOptions() + { + _correlationCookieBuilder = new CorrelationCookieBuilder(this) + { + Name = CorrelationPrefix, + HttpOnly = true, + SameSite = SameSiteMode.None, + SecurePolicy = CookieSecurePolicy.SameAsRequest, + IsEssential = true, + }; + } + /// - /// Gets or sets timeout value in milliseconds for back channel communications with the remote provider. + /// Checks that the options are valid for a specific scheme + /// + /// The scheme being validated. + public override void Validate(string scheme) + { + base.Validate(scheme); + if (string.Equals(scheme, SignInScheme, StringComparison.Ordinal)) + { + throw new InvalidOperationException(Resources.Exception_RemoteSignInSchemeCannotBeSelf); + } + } + + /// + /// Check that the options are valid. Should throw an exception if things are not ok. + /// + public override void Validate() + { + base.Validate(); + if (CallbackPath == null || !CallbackPath.HasValue) + { + throw new ArgumentException(Resources.FormatException_OptionMustBeProvided(nameof(CallbackPath)), nameof(CallbackPath)); + } + } + + /// + /// Gets or sets timeout value in milliseconds for back channel communications with the remote identity provider. /// /// /// The back channel timeout. @@ -22,41 +67,62 @@ public class RemoteAuthenticationOptions : AuthenticationOptions public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromSeconds(60); /// - /// The HttpMessageHandler used to communicate with Twitter. + /// The HttpMessageHandler used to communicate with remote identity provider. /// This cannot be set at the same time as BackchannelCertificateValidator unless the value /// can be downcast to a WebRequestHandler. /// public HttpMessageHandler BackchannelHttpHandler { get; set; } + /// + /// Used to communicate with the remote identity provider. + /// + public HttpClient Backchannel { get; set; } + + /// + /// Gets or sets the type used to secure data. + /// + public IDataProtectionProvider DataProtectionProvider { get; set; } + /// /// The request path within the application's base path where the user-agent will be returned. /// The middleware will process this request when it arrives. /// public PathString CallbackPath { get; set; } + /// + /// Gets or sets the optional path the user agent is redirected to if the user + /// doesn't approve the authorization demand requested by the remote server. + /// This property is not set by default. In this case, an exception is thrown + /// if an access_denied response is returned by the remote authorization server. + /// + public PathString AccessDeniedPath { get; set; } + + /// + /// Gets or sets the name of the parameter used to convey the original location + /// of the user before the remote challenge was triggered up to the access denied page. + /// This property is only used when the is explicitly specified. + /// + // Note: this deliberately matches the default parameter name used by the cookie handler. + public string ReturnUrlParameter { get; set; } = "ReturnUrl"; + /// /// Gets or sets the authentication scheme corresponding to the middleware /// responsible of persisting user's identity after a successful authentication. /// This value typically corresponds to a cookie middleware registered in the Startup class. - /// When omitted, is used as a fallback value. + /// When omitted, is used as a fallback value. /// public string SignInScheme { get; set; } - /// - /// Get or sets the text that the user can display on a sign in user interface. - /// - public string DisplayName - { - get { return Description.DisplayName; } - set { Description.DisplayName = value; } - } - /// /// Gets or sets the time limit for completing the authentication flow (15 minutes by default). /// public TimeSpan RemoteAuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(15); - public IRemoteAuthenticationEvents Events = new RemoteAuthenticationEvents(); + public new RemoteAuthenticationEvents Events + { + get => (RemoteAuthenticationEvents)base.Events; + set => base.Events = value; + } /// /// Defines whether access and refresh tokens should be stored in the @@ -65,5 +131,39 @@ public string DisplayName /// the size of the final authentication cookie. /// public bool SaveTokens { get; set; } + + /// + /// Determines the settings used to create the correlation cookie before the + /// cookie gets added to the response. + /// + public CookieBuilder CorrelationCookie + { + get => _correlationCookieBuilder; + set => _correlationCookieBuilder = value ?? throw new ArgumentNullException(nameof(value)); + } + + private class CorrelationCookieBuilder : RequestPathBaseCookieBuilder + { + private readonly RemoteAuthenticationOptions _options; + + public CorrelationCookieBuilder(RemoteAuthenticationOptions remoteAuthenticationOptions) + { + _options = remoteAuthenticationOptions; + } + + protected override string AdditionalPath => _options.CallbackPath; + + public override CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom) + { + var cookieOptions = base.Build(context, expiresFrom); + + if (!Expiration.HasValue || !cookieOptions.Expires.HasValue) + { + cookieOptions.Expires = expiresFrom.Add(_options.RemoteAuthenticationTimeout); + } + + return cookieOptions; + } + } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Authentication/Resources.resx b/src/Microsoft.AspNetCore.Authentication/Resources.resx index 77060045e..9e831dc74 100644 --- a/src/Microsoft.AspNetCore.Authentication/Resources.resx +++ b/src/Microsoft.AspNetCore.Authentication/Resources.resx @@ -126,4 +126,10 @@ The AuthenticationTokenProvider's required synchronous events have not been registered. + + The '{0}' option must be provided. + + + The SignInScheme for a remote authentication handler cannot be set to itself. If it was not explicitly set, the AuthenticationOptions.DefaultSignInScheme or DefaultScheme is used. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/SharedAuthenticationOptions.cs b/src/Microsoft.AspNetCore.Authentication/SharedAuthenticationOptions.cs deleted file mode 100644 index bf30cae9e..000000000 --- a/src/Microsoft.AspNetCore.Authentication/SharedAuthenticationOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - - -using System; - -namespace Microsoft.AspNetCore.Authentication -{ - public class SharedAuthenticationOptions - { - /// - /// Gets or sets the authentication scheme corresponding to the default middleware - /// responsible of persisting user's identity after a successful authentication. - /// This value typically corresponds to a cookie middleware registered in the Startup class. - /// - public string SignInScheme { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/SignInAuthenticationHandler.cs b/src/Microsoft.AspNetCore.Authentication/SignInAuthenticationHandler.cs new file mode 100644 index 000000000..dbd612dc1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/SignInAuthenticationHandler.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Adds support for SignInAsync + /// + public abstract class SignInAuthenticationHandler : SignOutAuthenticationHandler, IAuthenticationSignInHandler + where TOptions : AuthenticationSchemeOptions, new() + { + public SignInAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { } + + public virtual Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + var target = ResolveTarget(Options.ForwardSignIn); + return (target != null) + ? Context.SignInAsync(target, user, properties) + : HandleSignInAsync(user, properties ?? new AuthenticationProperties()); + } + + /// + /// Override this method to handle SignIn. + /// + /// + /// + /// A Task. + protected abstract Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties); + + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/SignOutAuthenticationHandler.cs b/src/Microsoft.AspNetCore.Authentication/SignOutAuthenticationHandler.cs new file mode 100644 index 000000000..015cb39e0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/SignOutAuthenticationHandler.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Adds support for SignOutAsync + /// + public abstract class SignOutAuthenticationHandler : AuthenticationHandler, IAuthenticationSignOutHandler + where TOptions : AuthenticationSchemeOptions, new() + { + public SignOutAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { } + + public virtual Task SignOutAsync(AuthenticationProperties properties) + { + var target = ResolveTarget(Options.ForwardSignOut); + return (target != null) + ? Context.SignOutAsync(target, properties) + : HandleSignOutAsync(properties ?? new AuthenticationProperties()); + } + + /// + /// Override this method to handle SignOut. + /// + /// + /// A Task. + protected abstract Task HandleSignOutAsync(AuthenticationProperties properties); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/SystemClock.cs b/src/Microsoft.AspNetCore.Authentication/SystemClock.cs index 0f9c2a30a..2320982ce 100644 --- a/src/Microsoft.AspNetCore.Authentication/SystemClock.cs +++ b/src/Microsoft.AspNetCore.Authentication/SystemClock.cs @@ -1,13 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - using System; namespace Microsoft.AspNetCore.Authentication { /// - /// Provides access to the normal system clock. + /// Provides access to the normal system clock with precision in seconds. /// public class SystemClock : ISystemClock { @@ -20,8 +19,8 @@ public DateTimeOffset UtcNow { // the clock measures whole seconds only, to have integral expires_in results, and // because milliseconds do not round-trip serialization formats - DateTimeOffset utcNow = DateTimeOffset.UtcNow; - return utcNow.AddMilliseconds(-utcNow.Millisecond); + var utcNowPrecisionSeconds = new DateTime((DateTime.UtcNow.Ticks / TimeSpan.TicksPerSecond) * TimeSpan.TicksPerSecond, DateTimeKind.Utc); + return new DateTimeOffset(utcNowPrecisionSeconds); } } } diff --git a/src/Microsoft.AspNetCore.Authentication/TokenExtensions.cs b/src/Microsoft.AspNetCore.Authentication/TokenExtensions.cs deleted file mode 100644 index 8065139ba..000000000 --- a/src/Microsoft.AspNetCore.Authentication/TokenExtensions.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Http.Features.Authentication; - -namespace Microsoft.AspNetCore.Authentication -{ - public static class AuthenticationTokenExtensions - { - private static string TokenNamesKey = ".TokenNames"; - private static string TokenKeyPrefix = ".Token."; - - public static void StoreTokens(this AuthenticationProperties properties, IEnumerable tokens) - { - if (properties == null) - { - throw new ArgumentNullException(nameof(properties)); - } - if (tokens == null) - { - throw new ArgumentNullException(nameof(tokens)); - } - - // Clear old tokens first - var oldTokens = properties.GetTokens(); - foreach (var t in oldTokens) - { - properties.Items.Remove(TokenKeyPrefix + t.Name); - } - properties.Items.Remove(TokenNamesKey); - - var tokenNames = new List(); - foreach (var token in tokens) - { - // REVIEW: should probably check that there are no ; in the token name and throw or encode - tokenNames.Add(token.Name); - properties.Items[TokenKeyPrefix+token.Name] = token.Value; - } - if (tokenNames.Count > 0) - { - properties.Items[TokenNamesKey] = string.Join(";", tokenNames.ToArray()); - } - } - - public static string GetTokenValue(this AuthenticationProperties properties, string tokenName) - { - if (properties == null) - { - throw new ArgumentNullException(nameof(properties)); - } - if (tokenName == null) - { - throw new ArgumentNullException(nameof(tokenName)); - } - - var tokenKey = TokenKeyPrefix + tokenName; - return properties.Items.ContainsKey(tokenKey) - ? properties.Items[tokenKey] - : null; - } - - public static IEnumerable GetTokens(this AuthenticationProperties properties) - { - if (properties == null) - { - throw new ArgumentNullException(nameof(properties)); - } - - var tokens = new List(); - if (properties.Items.ContainsKey(TokenNamesKey)) - { - var tokenNames = properties.Items[TokenNamesKey].Split(';'); - foreach (var name in tokenNames) - { - var token = properties.GetTokenValue(name); - if (token != null) - { - tokens.Add(new AuthenticationToken { Name = name, Value = token }); - } - } - } - - return tokens; - } - - public static Task GetTokenAsync(this AuthenticationManager manager, string tokenName) - { - return manager.GetTokenAsync(AuthenticationManager.AutomaticScheme, tokenName); - } - - public static async Task GetTokenAsync(this AuthenticationManager manager, string signInScheme, string tokenName) - { - if (manager == null) - { - throw new ArgumentNullException(nameof(manager)); - } - if (signInScheme == null) - { - throw new ArgumentNullException(nameof(signInScheme)); - } - if (tokenName == null) - { - throw new ArgumentNullException(nameof(tokenName)); - } - - var authContext = new AuthenticateContext(signInScheme); - await manager.AuthenticateAsync(authContext); - return new AuthenticationProperties(authContext.Properties).GetTokenValue(tokenName); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/Win32.cs b/src/Microsoft.AspNetCore.Authentication/Win32.cs deleted file mode 100644 index 0e757e42a..000000000 --- a/src/Microsoft.AspNetCore.Authentication/Win32.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - - -using System; -using System.ComponentModel; -using System.Runtime.InteropServices; - -namespace Microsoft.Win32 -{ - // TODO: ? [Localizable(false)] - internal static class NativeMethods - { - // ReSharper disable InconsistentNaming - public const int X509_ASN_ENCODING = 0x00000001; - public const int X509_PUBLIC_KEY_INFO = 8; - // ReSharper restore InconsistentNaming - - /// - /// Encodes a structure of the type indicated by the value of the lpszStructType parameter. - /// - /// Type of encoding used. - /// The high-order word is zero, the low-order word specifies the integer identifier for the type of the specified structure so - /// we can use the constants in http://msdn.microsoft.com/en-us/library/windows/desktop/aa378145%28v=vs.85%29.aspx - /// A pointer to the structure to be encoded. - /// A pointer to a buffer to receive the encoded structure. This parameter can be NULL to retrieve the size of this information for memory allocation purposes. - /// A pointer to a DWORD variable that contains the size, in bytes, of the buffer pointed to by the pbEncoded parameter. - /// - [DllImport("crypt32.dll", CharSet = CharSet.Ansi, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool CryptEncodeObject( - UInt32 dwCertEncodingType, - IntPtr lpszStructType, - ref CERT_PUBLIC_KEY_INFO pvStructInfo, - byte[] pbEncoded, - ref UInt32 pcbEncoded); - - // ReSharper disable InconsistentNaming - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - internal struct CRYPT_BLOB - { - public Int32 cbData; - public IntPtr pbData; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct CERT_CONTEXT - { - public Int32 dwCertEncodingType; - public IntPtr pbCertEncoded; - public Int32 cbCertEncoded; - public IntPtr pCertInfo; - public IntPtr hCertStore; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] - internal struct CRYPT_ALGORITHM_IDENTIFIER - { - public string pszObjId; - public CRYPT_BLOB Parameters; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] - internal struct CRYPT_BIT_BLOB - { - public Int32 cbData; - public IntPtr pbData; - public Int32 cUnusedBits; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - internal struct CERT_PUBLIC_KEY_INFO - { - public CRYPT_ALGORITHM_IDENTIFIER Algorithm; - public CRYPT_BIT_BLOB PublicKey; - } - - [StructLayout(LayoutKind.Sequential)] - internal class CERT_INFO - { - public Int32 dwVersion; - public CRYPT_BLOB SerialNumber; - public CRYPT_ALGORITHM_IDENTIFIER SignatureAlgorithm; - public CRYPT_BLOB Issuer; - public System.Runtime.InteropServices.ComTypes.FILETIME NotBefore; - public System.Runtime.InteropServices.ComTypes.FILETIME NotAfter; - public CRYPT_BLOB Subject; - public CERT_PUBLIC_KEY_INFO SubjectPublicKeyInfo; - public CRYPT_BIT_BLOB IssuerUniqueId; - public CRYPT_BIT_BLOB SubjectUniqueId; - public Int32 cExtension; - public IntPtr rgExtension; - } - - // ReSharper restore InconsistentNaming - } -} diff --git a/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json new file mode 100644 index 000000000..08eeb5e7b --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json @@ -0,0 +1,3330 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Services", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddRemoteScheme", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddPolicyScheme", + "Parameters": [ + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationHandler", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Scheme", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationScheme", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Options", + "Parameters": [], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Context", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Request", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpRequest", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Response", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpResponse", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OriginalPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OriginalPathBase", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Logger", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Logging.ILogger", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UrlEncoder", + "Parameters": [], + "ReturnType": "System.Text.Encodings.Web.UrlEncoder", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Clock", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.ISystemClock", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OptionsMonitor", + "Parameters": [], + "ReturnType": "Microsoft.Extensions.Options.IOptionsMonitor", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "System.Object", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClaimsIssuer", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CurrentUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeAsync", + "Parameters": [ + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "InitializeHandlerAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "BuildRedirectUri", + "Parameters": [ + { + "Name": "targetPath", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ResolveTarget", + "Parameters": [ + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateOnceAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateOnceSafeAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleForbiddenAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ForbidAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Schemes", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Schemes", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "schemes", + "Type": "Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [ + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClaimsIssuer", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClaimsIssuer", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "System.Object", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_EventsType", + "Parameters": [], + "ReturnType": "System.Type", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_EventsType", + "Parameters": [ + { + "Name": "value", + "Type": "System.Type" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardDefault", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardDefault", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardAuthenticate", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardAuthenticate", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardChallenge", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardChallenge", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardForbid", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardForbid", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardSignIn", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardSignIn", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardSignOut", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardSignOut", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ForwardDefaultSelector", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ForwardDefaultSelector", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Serialize", + "Parameters": [ + { + "Name": "model", + "Type": "T0" + } + ], + "ReturnType": "System.Byte[]", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Deserialize", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "T0", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TModel", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Protect", + "Parameters": [ + { + "Name": "data", + "Type": "T0" + } + ], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Protect", + "Parameters": [ + { + "Name": "data", + "Type": "T0" + }, + { + "Name": "purpose", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unprotect", + "Parameters": [ + { + "Name": "protectedText", + "Type": "System.String" + } + ], + "ReturnType": "T0", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unprotect", + "Parameters": [ + { + "Name": "protectedText", + "Type": "System.String" + }, + { + "Name": "purpose", + "Type": "System.String" + } + ], + "ReturnType": "T0", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TData", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.PropertiesDataFormat", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.SecureDataFormat", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "protector", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.PropertiesSerializer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IDataSerializer" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Default", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.PropertiesSerializer", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Serialize", + "Parameters": [ + { + "Name": "model", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Byte[]", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Deserialize", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.SecureDataFormat", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.ISecureDataFormat" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Protect", + "Parameters": [ + { + "Name": "data", + "Type": "T0" + } + ], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Protect", + "Parameters": [ + { + "Name": "data", + "Type": "T0" + }, + { + "Name": "purpose", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unprotect", + "Parameters": [ + { + "Name": "protectedText", + "Type": "System.String" + } + ], + "ReturnType": "T0", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unprotect", + "Parameters": [ + { + "Name": "protectedText", + "Type": "System.String" + }, + { + "Name": "purpose", + "Type": "System.String" + } + ], + "ReturnType": "T0", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISecureDataFormat", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "serializer", + "Type": "Microsoft.AspNetCore.Authentication.IDataSerializer" + }, + { + "Name": "protector", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TData", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.Base64UrlTextEncoder", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Encode", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Decode", + "Parameters": [ + { + "Name": "text", + "Type": "System.String" + } + ], + "ReturnType": "System.Byte[]", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.TicketDataFormat", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.SecureDataFormat", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "protector", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.TicketSerializer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IDataSerializer" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Default", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.TicketSerializer", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Serialize", + "Parameters": [ + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "System.Byte[]", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Deserialize", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationTicket", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteIdentity", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteClaim", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "claim", + "Type": "System.Security.Claims.Claim" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationTicket", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadIdentity", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + } + ], + "ReturnType": "System.Security.Claims.ClaimsIdentity", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadClaim", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + } + ], + "ReturnType": "System.Security.Claims.Claim", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.BaseContext", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Scheme", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationScheme", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Options", + "Parameters": [], + "ReturnType": "T0", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HttpContext", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Request", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpRequest", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Response", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpResponse", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.HandleRequestContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.BaseContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Result", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Result", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.HandleRequestResult" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleResponse", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SkipHandler", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.PrincipalContext", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Principal", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.BaseContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Properties", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.RedirectContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.PropertiesContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RedirectUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RedirectUri", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + }, + { + "Name": "redirectUri", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.HandleRequestContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Principal", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Properties", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Success", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failure", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failureMessage", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OnRemoteFailure", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnRemoteFailure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnTicketReceived", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnTicketReceived", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RemoteFailure", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.RemoteFailureContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TicketReceived", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authentication.TicketReceivedContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.RemoteFailureContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.HandleRequestContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Failure", + "Parameters": [], + "ReturnType": "System.Exception", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Failure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Properties", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions" + }, + { + "Name": "failure", + "Type": "System.Exception" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.ResultContext", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.BaseContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Principal", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Principal", + "Parameters": [ + { + "Name": "value", + "Type": "System.Security.Claims.ClaimsPrincipal" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Properties", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationProperties", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Properties", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Result", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Success", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "NoResult", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failure", + "Type": "System.Exception" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failureMessage", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "T0" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.TicketReceivedContext", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationContext", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ReturnUri", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ReturnUri", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "scheme", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationScheme" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions" + }, + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Handled", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Skipped", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Success", + "Parameters": [ + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failure", + "Type": "System.Exception" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failure", + "Type": "System.Exception" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failureMessage", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [ + { + "Name": "failureMessage", + "Type": "System.String" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Handle", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SkipHandler", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.ISystemClock", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_UtcNow", + "Parameters": [], + "ReturnType": "System.DateTimeOffset", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.PolicySchemeHandler", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.SignInAuthenticationHandler", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "HandleChallengeAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleForbiddenAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignInAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.PolicySchemeOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_SignInScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CreateEventsAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ShouldHandleRequestAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRequestAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationRequestHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRemoteAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAuthenticateAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleForbiddenAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GenerateCorrelationId", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ValidateCorrelationId", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [ + { + "Name": "scheme", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BackchannelTimeout", + "Parameters": [], + "ReturnType": "System.TimeSpan", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BackchannelTimeout", + "Parameters": [ + { + "Name": "value", + "Type": "System.TimeSpan" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_BackchannelHttpHandler", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpMessageHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_BackchannelHttpHandler", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.Http.HttpMessageHandler" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Backchannel", + "Parameters": [], + "ReturnType": "System.Net.Http.HttpClient", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Backchannel", + "Parameters": [ + { + "Name": "value", + "Type": "System.Net.Http.HttpClient" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DataProtectionProvider", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DataProtectionProvider", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtectionProvider" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CallbackPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CallbackPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SignInScheme", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SignInScheme", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RemoteAuthenticationTimeout", + "Parameters": [], + "ReturnType": "System.TimeSpan", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RemoteAuthenticationTimeout", + "Parameters": [ + { + "Name": "value", + "Type": "System.TimeSpan" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Events", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Events", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SaveTokens", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SaveTokens", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CorrelationCookie", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CorrelationCookie", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieBuilder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.SignInAuthenticationHandler", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.SignOutAuthenticationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationSignInHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "SignInAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignInHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignInAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.SignOutAuthenticationHandler", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "SignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleSignOutAsync", + "Parameters": [ + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptionsMonitor" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.UrlEncoder" + }, + { + "Name": "clock", + "Type": "Microsoft.AspNetCore.Authentication.ISystemClock" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authentication.SystemClock", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.ISystemClock" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_UtcNow", + "Parameters": [], + "ReturnType": "System.DateTimeOffset", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.ISystemClock", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.AuthAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseAuthentication", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.DependencyInjection.AuthenticationServiceCollectionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddAuthentication", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddAuthentication", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "defaultScheme", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddAuthentication", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureScheme", + "Type": "System.Action" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddScheme", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationHandler" + ] + } + ] + }, + { + "Kind": "Method", + "Name": "AddRemoteScheme", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "authenticationScheme", + "Type": "System.String" + }, + { + "Name": "displayName", + "Type": "System.String" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions" + ] + }, + { + "ParameterName": "THandler", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler" + ] + } + ] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/breakingchanges.netcore.json b/src/Microsoft.AspNetCore.Authentication/breakingchanges.netcore.json new file mode 100644 index 000000000..d5bc6954e --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/breakingchanges.netcore.json @@ -0,0 +1,22 @@ + [ + { + "TypeId": "public static class Microsoft.Extensions.DependencyInjection.AuthenticationServiceCollectionExtensions", + "MemberId": "public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddRemoteScheme(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.String authenticationScheme, System.String displayName, System.Action configureOptions) where T0 : Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions, new() where T1 : Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler", + "Kind": "Removal" + }, + { + "TypeId": "public static class Microsoft.Extensions.DependencyInjection.AuthenticationServiceCollectionExtensions", + "MemberId": "public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddScheme(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.String authenticationScheme, System.Action configureOptions) where T0 : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions, new() where T1 : Microsoft.AspNetCore.Authentication.AuthenticationHandler", + "Kind": "Removal" + }, + { + "TypeId": "public static class Microsoft.Extensions.DependencyInjection.AuthenticationServiceCollectionExtensions", + "MemberId": "public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddScheme(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.String authenticationScheme, System.String displayName, System.Action configureScheme, System.Action configureOptions) where T0 : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions, new() where T1 : Microsoft.AspNetCore.Authentication.AuthenticationHandler", + "Kind": "Removal" + }, + { + "TypeId": "public static class Microsoft.Extensions.DependencyInjection.AuthenticationServiceCollectionExtensions", + "MemberId": "public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddScheme(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.String authenticationScheme, System.String displayName, System.Action configureOptions) where T0 : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions, new() where T1 : Microsoft.AspNetCore.Authentication.AuthenticationHandler", + "Kind": "Removal" + } + ] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/project.json b/src/Microsoft.AspNetCore.Authentication/project.json deleted file mode 100644 index ec5ff479c..000000000 --- a/src/Microsoft.AspNetCore.Authentication/project.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "ASP.NET Core common types used by the various authentication middleware components.", - "tags": [ - "aspnetcore", - "authentication", - "security" - ], - "repository": { - "type": "git", - "url": "git://github.com/aspnet/security" - }, - "compilationOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk", - "nowarn": [ - "CS1591" - ], - "xmlDoc": true - }, - "dependencies": { - "Microsoft.AspNetCore.DataProtection": "1.0.0-*", - "Microsoft.AspNetCore.Http": "1.0.0-*", - "Microsoft.AspNetCore.Http.Extensions": "1.0.0-*", - "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", - "Microsoft.Extensions.SecurityHelper.Sources": { - "type": "build", - "version": "1.0.0-*" - }, - "Microsoft.Extensions.Options": "1.0.0-*", - "Microsoft.Extensions.WebEncoders": "1.0.0-*" - }, - "frameworks": { - "net451": { - "frameworkAssemblies": { - "System.Net.Http": "" - } - }, - "netstandard1.3": { - "dependencies": { - "System.Net.Http": "4.0.1-*" - }, - "imports": [ - "portable-net451" - ] - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization.Policy/AuthorizationAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Authorization.Policy/AuthorizationAppBuilderExtensions.cs new file mode 100644 index 000000000..66f0eb6f1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization.Policy/AuthorizationAppBuilderExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authorization; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods to add authorization capabilities to an HTTP application pipeline. + /// + public static class AuthorizationAppBuilderExtensions + { + /// + /// Adds the to the specified , which enables authorization capabilities. + /// + /// The to add the middleware to. + /// A reference to this instance after the operation has completed. + public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authorization.Policy/AuthorizationEndpointConventionBuilderExtensions.cs b/src/Microsoft.AspNetCore.Authorization.Policy/AuthorizationEndpointConventionBuilderExtensions.cs new file mode 100644 index 000000000..81b05d70a --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization.Policy/AuthorizationEndpointConventionBuilderExtensions.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Builder +{ + public static class AuthorizationEndpointConventionBuilderExtensions + { + public static IEndpointConventionBuilder RequireAuthorization(this IEndpointConventionBuilder builder, params IAuthorizeData[] authorizeData) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (authorizeData == null) + { + throw new ArgumentNullException(nameof(authorizeData)); + } + + builder.Apply(endpointBuilder => + { + foreach (var data in authorizeData) + { + endpointBuilder.Metadata.Add(data); + } + }); + return builder; + } + + public static IEndpointConventionBuilder RequireAuthorization(this IEndpointConventionBuilder builder, params string[] policyNames) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (policyNames == null) + { + throw new ArgumentNullException(nameof(policyNames)); + } + + return builder.RequireAuthorization(policyNames.Select(n => new AuthorizeAttribute(n)).ToArray()); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization.Policy/AuthorizationMiddleware.cs b/src/Microsoft.AspNetCore.Authorization.Policy/AuthorizationMiddleware.cs new file mode 100644 index 000000000..1da5b123f --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization.Policy/AuthorizationMiddleware.cs @@ -0,0 +1,104 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization.Policy; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Authorization +{ + public class AuthorizationMiddleware + { + private readonly RequestDelegate _next; + private readonly IAuthorizationPolicyProvider _policyProvider; + + public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (policyProvider == null) + { + throw new ArgumentNullException(nameof(policyProvider)); + } + + _next = next; + _policyProvider = policyProvider; + } + + public async Task Invoke(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var endpoint = context.GetEndpoint(); + var authorizeData = endpoint?.Metadata.GetOrderedMetadata() ?? Array.Empty(); + var policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData); + if (policy == null) + { + await _next(context); + return; + } + + // Policy evaluator has transient lifetime so it fetched from request services instead of injecting in constructor + var policyEvaluator = context.RequestServices.GetRequiredService(); + + var authenticateResult = await policyEvaluator.AuthenticateAsync(policy, context); + + // Allow Anonymous skips all authorization + if (endpoint?.Metadata.GetMetadata() != null) + { + await _next(context); + return; + } + + // Note that the resource will be null if there is no matched endpoint + var authorizeResult = await policyEvaluator.AuthorizeAsync(policy, authenticateResult, context, resource: endpoint); + + if (authorizeResult.Challenged) + { + if (policy.AuthenticationSchemes.Any()) + { + foreach (var scheme in policy.AuthenticationSchemes) + { + await context.ChallengeAsync(scheme); + } + } + else + { + await context.ChallengeAsync(); + } + + return; + } + else if (authorizeResult.Forbidden) + { + if (policy.AuthenticationSchemes.Any()) + { + foreach (var scheme in policy.AuthenticationSchemes) + { + await context.ForbidAsync(scheme); + } + } + else + { + await context.ForbidAsync(); + } + + return; + } + + await _next(context); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization.Policy/IPolicyEvaluator.cs b/src/Microsoft.AspNetCore.Authorization.Policy/IPolicyEvaluator.cs new file mode 100644 index 000000000..dd5e6fc03 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization.Policy/IPolicyEvaluator.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authorization.Policy +{ + /// + /// Base class for authorization handlers that need to be called for a specific requirement type. + /// + public interface IPolicyEvaluator + { + /// + /// Does authentication for and sets the resulting + /// to . If no schemes are set, this is a no-op. + /// + /// The . + /// The . + /// unless all schemes specified by fail to authenticate. + Task AuthenticateAsync(AuthorizationPolicy policy, HttpContext context); + + /// + /// Attempts authorization for a policy using . + /// + /// The . + /// The result of a call to . + /// The . + /// + /// An optional resource the policy should be checked with. + /// If a resource is not required for policy evaluation you may pass null as the value. + /// + /// Returns if authorization succeeds. + /// Otherwise returns if , otherwise + /// returns + Task AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj b/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj new file mode 100644 index 000000000..d3ad99a51 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj @@ -0,0 +1,22 @@ + + + + ASP.NET Core authorization policy helper classes. + netcoreapp3.0 + $(NoWarn);CS1591;NU1605 + true + aspnetcore;authorization + + + + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authorization.Policy/PolicyAuthorizationResult.cs b/src/Microsoft.AspNetCore.Authorization.Policy/PolicyAuthorizationResult.cs new file mode 100644 index 000000000..d7d481dcd --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization.Policy/PolicyAuthorizationResult.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authorization.Policy +{ + public class PolicyAuthorizationResult + { + private PolicyAuthorizationResult() { } + + /// + /// If true, means the callee should challenge and try again. + /// + public bool Challenged { get; private set; } + + /// + /// Authorization was forbidden. + /// + public bool Forbidden { get; private set; } + + /// + /// Authorization was successful. + /// + public bool Succeeded { get; private set; } + + public static PolicyAuthorizationResult Challenge() + => new PolicyAuthorizationResult { Challenged = true }; + + public static PolicyAuthorizationResult Forbid() + => new PolicyAuthorizationResult { Forbidden = true }; + + public static PolicyAuthorizationResult Success() + => new PolicyAuthorizationResult { Succeeded = true }; + + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization.Policy/PolicyEvaluator.cs b/src/Microsoft.AspNetCore.Authorization.Policy/PolicyEvaluator.cs new file mode 100644 index 000000000..3100ff4d3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization.Policy/PolicyEvaluator.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Authorization.Policy +{ + public class PolicyEvaluator : IPolicyEvaluator + { + private readonly IAuthorizationService _authorization; + + /// + /// Constructor + /// + /// The authorization service. + public PolicyEvaluator(IAuthorizationService authorization) + { + _authorization = authorization; + } + + /// + /// Does authentication for and sets the resulting + /// to . If no schemes are set, this is a no-op. + /// + /// The . + /// The . + /// unless all schemes specified by failed to authenticate. + public virtual async Task AuthenticateAsync(AuthorizationPolicy policy, HttpContext context) + { + if (policy.AuthenticationSchemes != null && policy.AuthenticationSchemes.Count > 0) + { + ClaimsPrincipal newPrincipal = null; + foreach (var scheme in policy.AuthenticationSchemes) + { + var result = await context.AuthenticateAsync(scheme); + if (result != null && result.Succeeded) + { + newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result.Principal); + } + } + + if (newPrincipal != null) + { + context.User = newPrincipal; + return AuthenticateResult.Success(new AuthenticationTicket(newPrincipal, string.Join(";", policy.AuthenticationSchemes))); + } + else + { + context.User = new ClaimsPrincipal(new ClaimsIdentity()); + return AuthenticateResult.NoResult(); + } + } + + return (context.User?.Identity?.IsAuthenticated ?? false) + ? AuthenticateResult.Success(new AuthenticationTicket(context.User, "context.User")) + : AuthenticateResult.NoResult(); + } + + /// + /// Attempts authorization for a policy using . + /// + /// The . + /// The result of a call to . + /// The . + /// + /// An optional resource the policy should be checked with. + /// If a resource is not required for policy evaluation you may pass null as the value. + /// + /// Returns if authorization succeeds. + /// Otherwise returns if , otherwise + /// returns + public virtual async Task AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource) + { + if (policy == null) + { + throw new ArgumentNullException(nameof(policy)); + } + + var result = await _authorization.AuthorizeAsync(context.User, resource, policy); + if (result.Succeeded) + { + return PolicyAuthorizationResult.Success(); + } + + // If authentication was successful, return forbidden, otherwise challenge + return (authenticationResult.Succeeded) + ? PolicyAuthorizationResult.Forbid() + : PolicyAuthorizationResult.Challenge(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization.Policy/PolicyServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Authorization.Policy/PolicyServiceCollectionExtensions.cs new file mode 100644 index 000000000..9b72a5cab --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization.Policy/PolicyServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authorization.Policy; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extension methods for setting up authorization services in an . + /// + public static class PolicyServiceCollectionExtensions + { + /// + /// Adds authorization policy services to the specified . + /// + /// The to add services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddAuthorizationPolicyEvaluator(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAdd(ServiceDescriptor.Transient()); + return services; + } + } +} diff --git a/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json b/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json new file mode 100644 index 000000000..e8708538d --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json @@ -0,0 +1,211 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authorization.Policy, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + }, + { + "Name": "authenticationResult", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticateResult" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "resource", + "Type": "System.Object" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Challenged", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Forbidden", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Succeeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Challenge", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Forbid", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Success", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.Policy.PolicyAuthorizationResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator" + ], + "Members": [ + { + "Kind": "Method", + "Name": "AuthenticateAsync", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + }, + { + "Name": "authenticationResult", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticateResult" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "resource", + "Type": "System.Object" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authorization", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Extensions.DependencyInjection.PolicyServiceCollectionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddAuthorizationPolicyEvaluator", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizationContext.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizationContext.cs deleted file mode 100644 index 455adb0dd..000000000 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizationContext.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; - -namespace Microsoft.AspNetCore.Authorization -{ - /// - /// Contains authorization information used by . - /// - public class AuthorizationContext - { - private HashSet _pendingRequirements; - private bool _failCalled; - private bool _succeedCalled; - - public AuthorizationContext( - IEnumerable requirements, - ClaimsPrincipal user, - object resource) - { - if (requirements == null) - { - throw new ArgumentNullException(nameof(requirements)); - } - - Requirements = requirements; - User = user; - Resource = resource; - _pendingRequirements = new HashSet(requirements); - } - - public IEnumerable Requirements { get; } - public ClaimsPrincipal User { get; } - public object Resource { get; } - - public IEnumerable PendingRequirements { get { return _pendingRequirements; } } - - public bool HasFailed { get { return _failCalled; } } - - public bool HasSucceeded - { - get - { - return !_failCalled && _succeedCalled && !PendingRequirements.Any(); - } - } - - public void Fail() - { - _failCalled = true; - } - - public void Succeed(IAuthorizationRequirement requirement) - { - _succeedCalled = true; - _pendingRequirements.Remove(requirement); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizationFailure.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizationFailure.cs new file mode 100644 index 000000000..89956c9aa --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationFailure.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Encapsulates a failure result of . + /// + public class AuthorizationFailure + { + private AuthorizationFailure() { } + + /// + /// Failure was due to being called. + /// + public bool FailCalled { get; private set; } + + /// + /// Failure was due to these requirements not being met via . + /// + public IEnumerable FailedRequirements { get; private set; } + + /// + /// Return a failure due to being called. + /// + /// The failure. + public static AuthorizationFailure ExplicitFail() + => new AuthorizationFailure + { + FailCalled = true, + FailedRequirements = new IAuthorizationRequirement[0] + }; + + /// + /// Return a failure due to some requirements not being met via . + /// + /// The requirements that were not met. + /// The failure. + public static AuthorizationFailure Failed(IEnumerable failed) + => new AuthorizationFailure { FailedRequirements = failed }; + + } +} diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizationHandler.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizationHandler.cs index 2bd90aad4..a4a923c3c 100644 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizationHandler.cs +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationHandler.cs @@ -6,65 +6,63 @@ namespace Microsoft.AspNetCore.Authorization { + /// + /// Base class for authorization handlers that need to be called for a specific requirement type. + /// + /// The type of the requirement to handle. public abstract class AuthorizationHandler : IAuthorizationHandler - where TRequirement : IAuthorizationRequirement + where TRequirement : IAuthorizationRequirement { - public void Handle(AuthorizationContext context) + /// + /// Makes a decision if authorization is allowed. + /// + /// The authorization context. + public virtual async Task HandleAsync(AuthorizationHandlerContext context) { foreach (var req in context.Requirements.OfType()) { - Handle(context, req); + await HandleRequirementAsync(context, req); } } - public virtual async Task HandleAsync(AuthorizationContext context) - { - foreach (var req in context.Requirements.OfType()) - { - await HandleAsync(context, req); - } - } - - protected abstract void Handle(AuthorizationContext context, TRequirement requirement); - - protected virtual Task HandleAsync(AuthorizationContext context, TRequirement requirement) - { - Handle(context, requirement); - return Task.FromResult(0); - } + /// + /// Makes a decision if authorization is allowed based on a specific requirement. + /// + /// The authorization context. + /// The requirement to evaluate. + protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement); } + /// + /// Base class for authorization handlers that need to be called for specific requirement and + /// resource types. + /// + /// The type of the requirement to evaluate. + /// The type of the resource to evaluate. public abstract class AuthorizationHandler : IAuthorizationHandler where TRequirement : IAuthorizationRequirement { - public virtual async Task HandleAsync(AuthorizationContext context) - { - if (context.Resource is TResource) - { - foreach (var req in context.Requirements.OfType()) - { - await HandleAsync(context, req, (TResource)context.Resource); - } - } - } - - protected virtual Task HandleAsync(AuthorizationContext context, TRequirement requirement, TResource resource) - { - Handle(context, requirement, resource); - return Task.FromResult(0); - } - - public virtual void Handle(AuthorizationContext context) + /// + /// Makes a decision if authorization is allowed. + /// + /// The authorization context. + public virtual async Task HandleAsync(AuthorizationHandlerContext context) { if (context.Resource is TResource) { foreach (var req in context.Requirements.OfType()) { - Handle(context, req, (TResource)context.Resource); + await HandleRequirementAsync(context, req, (TResource)context.Resource); } } } - protected abstract void Handle(AuthorizationContext context, TRequirement requirement, TResource resource); + /// + /// Makes a decision if authorization is allowed based on a specific requirement and resource. + /// + /// The authorization context. + /// The requirement to evaluate. + /// The resource to evaluate. + protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, TResource resource); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs new file mode 100644 index 000000000..b6378e407 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationHandlerContext.cs @@ -0,0 +1,98 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Contains authorization information used by . + /// + public class AuthorizationHandlerContext + { + private HashSet _pendingRequirements; + private bool _failCalled; + private bool _succeedCalled; + + /// + /// Creates a new instance of . + /// + /// A collection of all the for the current authorization action. + /// A representing the current user. + /// An optional resource to evaluate the against. + public AuthorizationHandlerContext( + IEnumerable requirements, + ClaimsPrincipal user, + object resource) + { + if (requirements == null) + { + throw new ArgumentNullException(nameof(requirements)); + } + + Requirements = requirements; + User = user; + Resource = resource; + _pendingRequirements = new HashSet(requirements); + } + + /// + /// The collection of all the for the current authorization action. + /// + public virtual IEnumerable Requirements { get; } + + /// + /// The representing the current user. + /// + public virtual ClaimsPrincipal User { get; } + + /// + /// The optional resource to evaluate the against. + /// + public virtual object Resource { get; } + + /// + /// Gets the requirements that have not yet been marked as succeeded. + /// + public virtual IEnumerable PendingRequirements { get { return _pendingRequirements; } } + + /// + /// Flag indicating whether the current authorization processing has failed. + /// + public virtual bool HasFailed { get { return _failCalled; } } + + /// + /// Flag indicating whether the current authorization processing has succeeded. + /// + public virtual bool HasSucceeded + { + get + { + return !_failCalled && _succeedCalled && !PendingRequirements.Any(); + } + } + + /// + /// Called to indicate will + /// never return true, even if all requirements are met. + /// + public virtual void Fail() + { + _failCalled = true; + } + + /// + /// Called to mark the specified as being + /// successfully evaluated. + /// + /// The requirement whose evaluation has succeeded. + public virtual void Succeed(IAuthorizationRequirement requirement) + { + _succeedCalled = true; + _pendingRequirements.Remove(requirement); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs index 94799af29..f0c5527d2 100644 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs @@ -14,10 +14,31 @@ public class AuthorizationOptions private IDictionary PolicyMap { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); /// - /// The initial default policy is to require any authenticated user + /// Determines whether authentication handlers should be invoked after a failure. + /// Defaults to true. /// + public bool InvokeHandlersAfterFailure { get; set; } = true; + + /// + /// Gets or sets the default authorization policy. Defaults to require authenticated users. + /// + /// + /// The default policy used when evaluating with no policy name specified. + /// public AuthorizationPolicy DefaultPolicy { get; set; } = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + /// + /// Gets or sets the required authorization policy. Defaults to null. + /// + /// + /// By default the required policy is null. + /// + /// If a required policy has been specified then it is always evaluated, even if there are no + /// instances for a resource. If a resource has + /// then they are evaluated together with the required policy. + /// + public AuthorizationPolicy RequiredPolicy { get; set; } + /// /// Add an authorization policy with the provided name. /// diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicy.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicy.cs index 97aa5b381..b3b54f300 100644 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicy.cs +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicy.cs @@ -4,11 +4,27 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace Microsoft.AspNetCore.Authorization { + /// + /// Represents a collection of authorization requirements and the scheme or + /// schemes they are evaluated against, all of which must succeed + /// for authorization to succeed. + /// public class AuthorizationPolicy { + /// + /// Creates a new instance of . + /// + /// + /// The list of s which must succeed for + /// this policy to be successful. + /// + /// + /// The authentication schemes the are evaluated against. + /// public AuthorizationPolicy(IEnumerable requirements, IEnumerable authenticationSchemes) { if (requirements == null) @@ -29,9 +45,26 @@ public AuthorizationPolicy(IEnumerable requirements, AuthenticationSchemes = new List(authenticationSchemes).AsReadOnly(); } + /// + /// Gets a readonly list of s which must succeed for + /// this policy to be successful. + /// public IReadOnlyList Requirements { get; } + + /// + /// Gets a readonly list of the authentication schemes the + /// are evaluated against. + /// public IReadOnlyList AuthenticationSchemes { get; } + /// + /// Combines the specified into a single policy. + /// + /// The authorization policies to combine. + /// + /// A new which represents the combination of the + /// specified . + /// public static AuthorizationPolicy Combine(params AuthorizationPolicy[] policies) { if (policies == null) @@ -42,6 +75,14 @@ public static AuthorizationPolicy Combine(params AuthorizationPolicy[] policies) return Combine((IEnumerable)policies); } + /// + /// Combines the specified into a single policy. + /// + /// The authorization policies to combine. + /// + /// A new which represents the combination of the + /// specified . + /// public static AuthorizationPolicy Combine(IEnumerable policies) { if (policies == null) @@ -57,59 +98,96 @@ public static AuthorizationPolicy Combine(IEnumerable polic return builder.Build(); } - public static AuthorizationPolicy Combine(AuthorizationOptions options, IEnumerable attributes) + /// + /// Combines the provided by the specified + /// . + /// + /// A which provides the policies to combine. + /// A collection of authorization data used to apply authorization to a resource. + /// + /// A new which represents the combination of the + /// authorization policies provided by the specified . + /// + public static async Task CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable authorizeData) { - if (options == null) + if (policyProvider == null) + { + throw new ArgumentNullException(nameof(policyProvider)); + } + + if (authorizeData == null) { - throw new ArgumentNullException(nameof(options)); + throw new ArgumentNullException(nameof(authorizeData)); } - if (attributes == null) + // Avoid allocating enumerator if the data is known to be empty + var skipEnumeratingData = false; + if (authorizeData is IList dataList) { - throw new ArgumentNullException(nameof(attributes)); + skipEnumeratingData = dataList.Count == 0; } - var policyBuilder = new AuthorizationPolicyBuilder(); - var any = false; - foreach (var authorizeAttribute in attributes.OfType()) + AuthorizationPolicyBuilder policyBuilder = null; + if (!skipEnumeratingData) { - any = true; - var useDefaultPolicy = true; - if (!string.IsNullOrWhiteSpace(authorizeAttribute.Policy)) + foreach (var authorizeDatum in authorizeData) { - var policy = options.GetPolicy(authorizeAttribute.Policy); - if (policy == null) + if (policyBuilder == null) { - throw new InvalidOperationException(Resources.FormatException_AuthorizationPolicyNotFound(authorizeAttribute.Policy)); + policyBuilder = new AuthorizationPolicyBuilder(); } - policyBuilder.Combine(policy); - useDefaultPolicy = false; - } - var rolesSplit = authorizeAttribute.Roles?.Split(','); - if (rolesSplit != null && rolesSplit.Any()) - { - var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()); - policyBuilder.RequireRole(trimmedRolesSplit); - useDefaultPolicy = false; - } - var authTypesSplit = authorizeAttribute.ActiveAuthenticationSchemes?.Split(','); - if (authTypesSplit != null && authTypesSplit.Any()) - { - foreach (var authType in authTypesSplit) + var useDefaultPolicy = true; + if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy)) + { + var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy); + if (policy == null) + { + throw new InvalidOperationException(Resources.FormatException_AuthorizationPolicyNotFound(authorizeDatum.Policy)); + } + policyBuilder.Combine(policy); + useDefaultPolicy = false; + } + + var rolesSplit = authorizeDatum.Roles?.Split(','); + if (rolesSplit != null && rolesSplit.Any()) + { + var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()); + policyBuilder.RequireRole(trimmedRolesSplit); + useDefaultPolicy = false; + } + + var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(','); + if (authTypesSplit != null && authTypesSplit.Any()) { - if (!string.IsNullOrWhiteSpace(authType)) + foreach (var authType in authTypesSplit) { - policyBuilder.AuthenticationSchemes.Add(authType.Trim()); + if (!string.IsNullOrWhiteSpace(authType)) + { + policyBuilder.AuthenticationSchemes.Add(authType.Trim()); + } } } + + if (useDefaultPolicy) + { + policyBuilder.Combine(await policyProvider.GetDefaultPolicyAsync()); + } } - if (useDefaultPolicy) + } + + var requiredPolicy = await policyProvider.GetRequiredPolicyAsync(); + if (requiredPolicy != null) + { + if (policyBuilder == null) { - policyBuilder.Combine(options.DefaultPolicy); + policyBuilder = new AuthorizationPolicyBuilder(); } + + policyBuilder.Combine(requiredPolicy); } - return any ? policyBuilder.Build() : null; + + return policyBuilder?.Build(); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.cs index 965ffe02e..2f483e37c 100644 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.cs +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.cs @@ -9,21 +9,47 @@ namespace Microsoft.AspNetCore.Authorization { + /// + /// Used for building policies during application startup. + /// public class AuthorizationPolicyBuilder { + /// + /// Creates a new instance of + /// + /// An array of authentication schemes the policy should be evaluated against. public AuthorizationPolicyBuilder(params string[] authenticationSchemes) { AddAuthenticationSchemes(authenticationSchemes); } + /// + /// Creates a new instance of . + /// + /// The to build. public AuthorizationPolicyBuilder(AuthorizationPolicy policy) { Combine(policy); } + /// + /// Gets or sets a list of s which must succeed for + /// this policy to be successful. + /// public IList Requirements { get; set; } = new List(); + + /// + /// Gets or sets a list authentication schemes the + /// are evaluated against. + /// public IList AuthenticationSchemes { get; set; } = new List(); + /// + /// Adds the specified authentication to the + /// for this instance. + /// + /// The schemes to add. + /// A reference to this instance after the operation has completed. public AuthorizationPolicyBuilder AddAuthenticationSchemes(params string[] schemes) { foreach (var authType in schemes) @@ -33,6 +59,12 @@ public AuthorizationPolicyBuilder AddAuthenticationSchemes(params string[] schem return this; } + /// + /// Adds the specified to the + /// for this instance. + /// + /// The authorization requirements to add. + /// A reference to this instance after the operation has completed. public AuthorizationPolicyBuilder AddRequirements(params IAuthorizationRequirement[] requirements) { foreach (var req in requirements) @@ -42,6 +74,11 @@ public AuthorizationPolicyBuilder AddRequirements(params IAuthorizationRequireme return this; } + /// + /// Combines the specified into the current instance. + /// + /// The to combine. + /// A reference to this instance after the operation has completed. public AuthorizationPolicyBuilder Combine(AuthorizationPolicy policy) { if (policy == null) @@ -54,27 +91,47 @@ public AuthorizationPolicyBuilder Combine(AuthorizationPolicy policy) return this; } - public AuthorizationPolicyBuilder RequireClaim(string claimType, params string[] requiredValues) + /// + /// Adds a + /// to the current instance. + /// + /// The claim type required. + /// Values the claim must process one or more of for evaluation to succeed. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder RequireClaim(string claimType, params string[] allowedValues) { if (claimType == null) { throw new ArgumentNullException(nameof(claimType)); } - return RequireClaim(claimType, (IEnumerable)requiredValues); + return RequireClaim(claimType, (IEnumerable)allowedValues); } - public AuthorizationPolicyBuilder RequireClaim(string claimType, IEnumerable requiredValues) + /// + /// Adds a + /// to the current instance. + /// + /// The claim type required. + /// Values the claim must process one or more of for evaluation to succeed. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder RequireClaim(string claimType, IEnumerable allowedValues) { if (claimType == null) { throw new ArgumentNullException(nameof(claimType)); } - Requirements.Add(new ClaimsAuthorizationRequirement(claimType, requiredValues)); + Requirements.Add(new ClaimsAuthorizationRequirement(claimType, allowedValues)); return this; } + /// + /// Adds a + /// to the current instance. + /// + /// The claim type required, which no restrictions on claim value. + /// A reference to this instance after the operation has completed. public AuthorizationPolicyBuilder RequireClaim(string claimType) { if (claimType == null) @@ -86,6 +143,12 @@ public AuthorizationPolicyBuilder RequireClaim(string claimType) return this; } + /// + /// Adds a + /// to the current instance. + /// + /// The allowed roles. + /// A reference to this instance after the operation has completed. public AuthorizationPolicyBuilder RequireRole(params string[] roles) { if (roles == null) @@ -96,6 +159,12 @@ public AuthorizationPolicyBuilder RequireRole(params string[] roles) return RequireRole((IEnumerable)roles); } + /// + /// Adds a + /// to the current instance. + /// + /// The allowed roles. + /// A reference to this instance after the operation has completed. public AuthorizationPolicyBuilder RequireRole(IEnumerable roles) { if (roles == null) @@ -107,6 +176,12 @@ public AuthorizationPolicyBuilder RequireRole(IEnumerable roles) return this; } + /// + /// Adds a + /// to the current instance. + /// + /// The user name the current user must possess. + /// A reference to this instance after the operation has completed. public AuthorizationPolicyBuilder RequireUserName(string userName) { if (userName == null) @@ -118,6 +193,10 @@ public AuthorizationPolicyBuilder RequireUserName(string userName) return this; } + /// + /// Adds a to the current instance. + /// + /// A reference to this instance after the operation has completed. public AuthorizationPolicyBuilder RequireAuthenticatedUser() { Requirements.Add(new DenyAnonymousAuthorizationRequirement()); @@ -125,40 +204,47 @@ public AuthorizationPolicyBuilder RequireAuthenticatedUser() } /// - /// Requires that this Function returns true + /// Adds an to the current instance. /// - /// Function that must return true - /// - public AuthorizationPolicyBuilder RequireAssertion(Func assert) + /// The handler to evaluate during authorization. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder RequireAssertion(Func handler) { - if (assert == null) + if (handler == null) { - throw new ArgumentNullException(nameof(assert)); + throw new ArgumentNullException(nameof(handler)); } - Requirements.Add(new AssertionRequirement(assert)); + Requirements.Add(new AssertionRequirement(handler)); return this; } /// - /// Requires that this Function returns true + /// Adds an to the current instance. /// - /// Function that must return true - /// - public AuthorizationPolicyBuilder RequireAssertion(Func> assert) + /// The handler to evaluate during authorization. + /// A reference to this instance after the operation has completed. + public AuthorizationPolicyBuilder RequireAssertion(Func> handler) { - if (assert == null) + if (handler == null) { - throw new ArgumentNullException(nameof(assert)); + throw new ArgumentNullException(nameof(handler)); } - Requirements.Add(new AssertionRequirement(assert)); + Requirements.Add(new AssertionRequirement(handler)); return this; } + /// + /// Builds a new from the requirements + /// in this instance. + /// + /// + /// A new built from the requirements in this instance. + /// public AuthorizationPolicy Build() { return new AuthorizationPolicy(Requirements, AuthenticationSchemes.Distinct()); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizationResult.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizationResult.cs new file mode 100644 index 000000000..46dca35fb --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationResult.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Encapsulates the result of . + /// + public class AuthorizationResult + { + private AuthorizationResult() { } + + /// + /// True if authorization was successful. + /// + public bool Succeeded { get; private set; } + + /// + /// Contains information about why authorization failed. + /// + public AuthorizationFailure Failure { get; private set; } + + /// + /// Returns a successful result. + /// + /// A successful result. + public static AuthorizationResult Success() => new AuthorizationResult { Succeeded = true }; + + public static AuthorizationResult Failed(AuthorizationFailure failure) => new AuthorizationResult { Failure = failure }; + + public static AuthorizationResult Failed() => new AuthorizationResult { Failure = AuthorizationFailure.ExplicitFail() }; + + } +} diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs index c92be6232..c089fba28 100644 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceCollectionExtensions.cs @@ -27,6 +27,9 @@ public static IServiceCollection AddAuthorization(this IServiceCollection servic services.TryAdd(ServiceDescriptor.Transient()); services.TryAdd(ServiceDescriptor.Transient()); + services.TryAdd(ServiceDescriptor.Transient()); + services.TryAdd(ServiceDescriptor.Transient()); + services.TryAdd(ServiceDescriptor.Transient()); services.TryAddEnumerable(ServiceDescriptor.Transient()); return services; } diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceExtensions.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceExtensions.cs index 8979c5c63..866b5dbc5 100644 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceExtensions.cs +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationServiceExtensions.cs @@ -8,17 +8,23 @@ namespace Microsoft.AspNetCore.Authorization { + /// + /// Extension methods for . + /// public static class AuthorizationServiceExtensions { /// /// Checks if a user meets a specific requirement for the specified resource /// - /// The . - /// - /// - /// - /// - public static Task AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, object resource, IAuthorizationRequirement requirement) + /// The providing authorization. + /// The user to evaluate the policy against. + /// The resource to evaluate the policy against. + /// The requirement to evaluate the policy against. + /// + /// A flag indicating whether requirement evaluation has succeeded or failed. + /// This value is true when the user fulfills the policy, otherwise false. + /// + public static Task AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, object resource, IAuthorizationRequirement requirement) { if (service == null) { @@ -34,14 +40,17 @@ public static Task AuthorizeAsync(this IAuthorizationService service, Clai } /// - /// Checks if a user meets a specific authorization policy + /// Checks if a user meets a specific authorization policy against the specified resource. /// - /// The authorization service. - /// The user to check the policy against. - /// The resource the policy should be checked with. - /// The policy to check against a specific context. - /// true when the user fulfills the policy, false otherwise. - public static Task AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, object resource, AuthorizationPolicy policy) + /// The providing authorization. + /// The user to evaluate the policy against. + /// The resource to evaluate the policy against. + /// The policy to evaluate. + /// + /// A flag indicating whether policy evaluation has succeeded or failed. + /// This value is true when the user fulfills the policy, otherwise false. + /// + public static Task AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, object resource, AuthorizationPolicy policy) { if (service == null) { @@ -53,17 +62,20 @@ public static Task AuthorizeAsync(this IAuthorizationService service, Clai throw new ArgumentNullException(nameof(policy)); } - return service.AuthorizeAsync(user, resource, policy.Requirements.ToArray()); + return service.AuthorizeAsync(user, resource, policy.Requirements); } /// - /// Checks if a user meets a specific authorization policy + /// Checks if a user meets a specific authorization policy against the specified resource. /// - /// The authorization service. - /// The user to check the policy against. - /// The policy to check against a specific context. - /// true when the user fulfills the policy, false otherwise. - public static Task AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, AuthorizationPolicy policy) + /// The providing authorization. + /// The user to evaluate the policy against. + /// The policy to evaluate. + /// + /// A flag indicating whether policy evaluation has succeeded or failed. + /// This value is true when the user fulfills the policy, otherwise false. + /// + public static Task AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, AuthorizationPolicy policy) { if (service == null) { @@ -79,13 +91,16 @@ public static Task AuthorizeAsync(this IAuthorizationService service, Clai } /// - /// Checks if a user meets a specific authorization policy + /// Checks if a user meets a specific authorization policy against the specified resource. /// - /// The authorization service. - /// The user to check the policy against. - /// The name of the policy to check against a specific context. - /// true when the user fulfills the policy, false otherwise. - public static Task AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, string policyName) + /// The providing authorization. + /// The user to evaluate the policy against. + /// The name of the policy to evaluate. + /// + /// A flag indicating whether policy evaluation has succeeded or failed. + /// This value is true when the user fulfills the policy, otherwise false. + /// + public static Task AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, string policyName) { if (service == null) { diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs index cdfab44f1..b1f0f8bae 100644 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs @@ -25,14 +25,19 @@ public AuthorizeAttribute(string policy) Policy = policy; } - /// + /// + /// Gets or sets the policy name that determines access to the resource. + /// public string Policy { get; set; } - /// - // REVIEW: can we get rid of the , deliminated in Roles/AuthTypes + /// + /// Gets or sets a comma delimited list of roles that are allowed to access the resource. + /// public string Roles { get; set; } - /// - public string ActiveAuthenticationSchemes { get; set; } + /// + /// Gets or sets a comma delimited list of schemes from which user information is constructed. + /// + public string AuthenticationSchemes { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationEvaluator.cs b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationEvaluator.cs new file mode 100644 index 000000000..4bbc283be --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationEvaluator.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Determines whether an authorization request was successful or not. + /// + public class DefaultAuthorizationEvaluator : IAuthorizationEvaluator + { + /// + /// Determines whether the authorization result was successful or not. + /// + /// The authorization information. + /// The . + public AuthorizationResult Evaluate(AuthorizationHandlerContext context) + => context.HasSucceeded + ? AuthorizationResult.Success() + : AuthorizationResult.Failed(context.HasFailed + ? AuthorizationFailure.ExplicitFail() + : AuthorizationFailure.Failed(context.PendingRequirements)); + } +} diff --git a/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerContextFactory.cs b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerContextFactory.cs new file mode 100644 index 000000000..2dae5e5e7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerContextFactory.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// A type used to provide a used for authorization. + /// + public class DefaultAuthorizationHandlerContextFactory : IAuthorizationHandlerContextFactory + { + /// + /// Creates a used for authorization. + /// + /// The requirements to evaluate. + /// The user to evaluate the requirements against. + /// + /// An optional resource the policy should be checked with. + /// If a resource is not required for policy evaluation you may pass null as the value. + /// + /// The . + public virtual AuthorizationHandlerContext CreateContext(IEnumerable requirements, ClaimsPrincipal user, object resource) + { + return new AuthorizationHandlerContext(requirements, user, resource); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerProvider.cs b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerProvider.cs new file mode 100644 index 000000000..d297d4cdc --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationHandlerProvider.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// The default implementation of a handler provider, + /// which provides the s for an authorization request. + /// + public class DefaultAuthorizationHandlerProvider : IAuthorizationHandlerProvider + { + private readonly IEnumerable _handlers; + + /// + /// Creates a new instance of . + /// + /// The s. + public DefaultAuthorizationHandlerProvider(IEnumerable handlers) + { + if (handlers == null) + { + throw new ArgumentNullException(nameof(handlers)); + } + + _handlers = handlers; + } + + public Task> GetHandlersAsync(AuthorizationHandlerContext context) + => Task.FromResult(_handlers); + } +} diff --git a/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs index 97b806e87..f5e665273 100644 --- a/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs +++ b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs @@ -8,12 +8,19 @@ namespace Microsoft.AspNetCore.Authorization { /// - /// A type which can provide a for a particular name. + /// The default implementation of a policy provider, + /// which provides a for a particular name. /// public class DefaultAuthorizationPolicyProvider : IAuthorizationPolicyProvider { private readonly AuthorizationOptions _options; + private Task _cachedDefaultPolicy; + private Task _cachedRequiredPolicy; + /// + /// Creates a new instance of . + /// + /// The options used to configure this instance. public DefaultAuthorizationPolicyProvider(IOptions options) { if (options == null) @@ -22,15 +29,46 @@ public DefaultAuthorizationPolicyProvider(IOptions options } _options = options.Value; - } + } + + /// + /// Gets the default authorization policy. + /// + /// The default authorization policy. + public Task GetDefaultPolicyAsync() + { + return GetCachedPolicy(ref _cachedDefaultPolicy, _options.DefaultPolicy); + } + + /// + /// Gets the required authorization policy. + /// + /// The required authorization policy. + public Task GetRequiredPolicyAsync() + { + return GetCachedPolicy(ref _cachedRequiredPolicy, _options.RequiredPolicy); + } + + private Task GetCachedPolicy(ref Task cachedPolicy, AuthorizationPolicy currentPolicy) + { + var local = cachedPolicy; + if (local == null || local.Result != currentPolicy) + { + cachedPolicy = local = Task.FromResult(currentPolicy); + } + return local; + } /// /// Gets a from the given /// - /// - /// + /// The policy name to retrieve. + /// The named . public virtual Task GetPolicyAsync(string policyName) { + // MVC caches policies specifically for this class, so this method MUST return the same policy per + // policyName for every request or it could allow undesired access. It also must return synchronously. + // A change to either of these behaviors would require shipping a patch of MVC as well. return Task.FromResult(_options.GetPolicy(policyName)); } } diff --git a/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs index 39d35d0dd..bc5d571c4 100644 --- a/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs +++ b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationService.cs @@ -3,22 +3,41 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Security.Claims; using System.Security.Principal; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Authorization { + /// + /// The default implementation of an . + /// public class DefaultAuthorizationService : IAuthorizationService { + private readonly AuthorizationOptions _options; + private readonly IAuthorizationHandlerContextFactory _contextFactory; + private readonly IAuthorizationHandlerProvider _handlers; + private readonly IAuthorizationEvaluator _evaluator; private readonly IAuthorizationPolicyProvider _policyProvider; - private readonly IList _handlers; private readonly ILogger _logger; - public DefaultAuthorizationService(IAuthorizationPolicyProvider policyProvider, IEnumerable handlers, ILogger logger) + /// + /// Creates a new instance of . + /// + /// The used to provide policies. + /// The handlers used to fulfill s. + /// The logger used to log messages, warnings and errors. + /// The used to create the context to handle the authorization. + /// The used to determine if authorization was successful. + /// The used. + public DefaultAuthorizationService(IAuthorizationPolicyProvider policyProvider, IAuthorizationHandlerProvider handlers, ILogger logger, IAuthorizationHandlerContextFactory contextFactory, IAuthorizationEvaluator evaluator, IOptions options) { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } if (policyProvider == null) { throw new ArgumentNullException(nameof(policyProvider)); @@ -31,60 +50,74 @@ public DefaultAuthorizationService(IAuthorizationPolicyProvider policyProvider, { throw new ArgumentNullException(nameof(logger)); } + if (contextFactory == null) + { + throw new ArgumentNullException(nameof(contextFactory)); + } + if (evaluator == null) + { + throw new ArgumentNullException(nameof(evaluator)); + } - _handlers = handlers.ToArray(); + _options = options.Value; + _handlers = handlers; _policyProvider = policyProvider; _logger = logger; + _evaluator = evaluator; + _contextFactory = contextFactory; } - public async Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements) + /// + /// Checks if a user meets a specific set of requirements for the specified resource. + /// + /// The user to evaluate the requirements against. + /// The resource to evaluate the requirements against. + /// The requirements to evaluate. + /// + /// A flag indicating whether authorization has succeeded. + /// This value is true when the user fulfills the policy otherwise false. + /// + public async Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements) { if (requirements == null) { throw new ArgumentNullException(nameof(requirements)); } - var authContext = new AuthorizationContext(requirements, user, resource); - foreach (var handler in _handlers) + var authContext = _contextFactory.CreateContext(requirements, user, resource); + var handlers = await _handlers.GetHandlersAsync(authContext); + foreach (var handler in handlers) { await handler.HandleAsync(authContext); + if (!_options.InvokeHandlersAfterFailure && authContext.HasFailed) + { + break; + } } - if (authContext.HasSucceeded) + var result = _evaluator.Evaluate(authContext); + if (result.Succeeded) { - _logger.UserAuthorizationSucceeded(GetUserNameForLogging(user)); - return true; + _logger.UserAuthorizationSucceeded(); } else { - _logger.UserAuthorizationFailed(GetUserNameForLogging(user)); - return false; - } - } - - private string GetUserNameForLogging(ClaimsPrincipal user) - { - var identity = user?.Identity; - if (identity != null) - { - var name = identity.Name; - if (name != null) - { - return name; - } - return GetClaimValue(identity, "sub") - ?? GetClaimValue(identity, ClaimTypes.Name) - ?? GetClaimValue(identity, ClaimTypes.NameIdentifier); + _logger.UserAuthorizationFailed(); } - return null; - } - - private static string GetClaimValue(IIdentity identity, string claimsType) - { - return (identity as ClaimsIdentity)?.FindFirst(claimsType)?.Value; + return result; } - public async Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) + /// + /// Checks if a user meets a specific authorization policy. + /// + /// The user to check the policy against. + /// The resource the policy should be checked with. + /// The name of the policy to check against a specific context. + /// + /// A flag indicating whether authorization has succeeded. + /// This value is true when the user fulfills the policy otherwise false. + /// + public async Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) { if (policyName == null) { diff --git a/src/Microsoft.AspNetCore.Authorization/IAllowAnonymous.cs b/src/Microsoft.AspNetCore.Authorization/IAllowAnonymous.cs index 7593e0ad3..8531c3daa 100644 --- a/src/Microsoft.AspNetCore.Authorization/IAllowAnonymous.cs +++ b/src/Microsoft.AspNetCore.Authorization/IAllowAnonymous.cs @@ -3,6 +3,9 @@ namespace Microsoft.AspNetCore.Authorization { + /// + /// Marker interface to enable the . + /// public interface IAllowAnonymous { } diff --git a/src/Microsoft.AspNetCore.Authorization/IAuthorizationEvaluator.cs b/src/Microsoft.AspNetCore.Authorization/IAuthorizationEvaluator.cs new file mode 100644 index 000000000..baa6f828c --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization/IAuthorizationEvaluator.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// Determines whether an authorization request was successful or not. + /// + public interface IAuthorizationEvaluator + { + /// + /// Determines whether the authorization result was successful or not. + /// + /// The authorization information. + /// The . + AuthorizationResult Evaluate(AuthorizationHandlerContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandler.cs b/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandler.cs index fc8ba6e7f..afe9e43f0 100644 --- a/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandler.cs +++ b/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandler.cs @@ -5,8 +5,15 @@ namespace Microsoft.AspNetCore.Authorization { + /// + /// Classes implementing this interface are able to make a decision if authorization is allowed. + /// public interface IAuthorizationHandler { - Task HandleAsync(AuthorizationContext context); + /// + /// Makes a decision if authorization is allowed. + /// + /// The authorization information. + Task HandleAsync(AuthorizationHandlerContext context); } } diff --git a/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerContextFactory.cs b/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerContextFactory.cs new file mode 100644 index 000000000..272109eea --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerContextFactory.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// A type used to provide a used for authorization. + /// + public interface IAuthorizationHandlerContextFactory + { + /// + /// Creates a used for authorization. + /// + /// The requirements to evaluate. + /// The user to evaluate the requirements against. + /// + /// An optional resource the policy should be checked with. + /// If a resource is not required for policy evaluation you may pass null as the value. + /// + /// The . + AuthorizationHandlerContext CreateContext(IEnumerable requirements, ClaimsPrincipal user, object resource); + } +} diff --git a/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerProvider.cs b/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerProvider.cs new file mode 100644 index 000000000..7f0d9f5d3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization/IAuthorizationHandlerProvider.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authorization +{ + /// + /// A type which can provide the s for an authorization request. + /// + public interface IAuthorizationHandlerProvider + { + /// + /// Return the handlers that will be called for the authorization request. + /// + /// The . + /// The list of handlers. + Task> GetHandlersAsync(AuthorizationHandlerContext context); + } +} diff --git a/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs b/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs index 1a0dbace6..4560d11e0 100644 --- a/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs +++ b/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs @@ -13,8 +13,20 @@ public interface IAuthorizationPolicyProvider /// /// Gets a from the given /// - /// - /// + /// The policy name to retrieve. + /// The named . Task GetPolicyAsync(string policyName); + + /// + /// Gets the default authorization policy. + /// + /// The default authorization policy. + Task GetDefaultPolicyAsync(); + + /// + /// Gets the required authorization policy. + /// + /// The required authorization policy. + Task GetRequiredPolicyAsync(); } } diff --git a/src/Microsoft.AspNetCore.Authorization/IAuthorizationRequirement.cs b/src/Microsoft.AspNetCore.Authorization/IAuthorizationRequirement.cs index 800789a8c..0bdcaff86 100644 --- a/src/Microsoft.AspNetCore.Authorization/IAuthorizationRequirement.cs +++ b/src/Microsoft.AspNetCore.Authorization/IAuthorizationRequirement.cs @@ -3,6 +3,9 @@ namespace Microsoft.AspNetCore.Authorization { + /// + /// Represents an authorization requirement. + /// public interface IAuthorizationRequirement { } diff --git a/src/Microsoft.AspNetCore.Authorization/IAuthorizationService.cs b/src/Microsoft.AspNetCore.Authorization/IAuthorizationService.cs index e3b9fec91..8976425ba 100644 --- a/src/Microsoft.AspNetCore.Authorization/IAuthorizationService.cs +++ b/src/Microsoft.AspNetCore.Authorization/IAuthorizationService.cs @@ -15,19 +15,40 @@ public interface IAuthorizationService /// /// Checks if a user meets a specific set of requirements for the specified resource /// - /// - /// - /// - /// - Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements); + /// The user to evaluate the requirements against. + /// + /// An optional resource the policy should be checked with. + /// If a resource is not required for policy evaluation you may pass null as the value. + /// + /// The requirements to evaluate. + /// + /// A flag indicating whether authorization has succeeded. + /// This value is true when the user fulfills the policy; otherwise false. + /// + /// + /// Resource is an optional parameter and may be null. Please ensure that you check it is not + /// null before acting upon it. + /// + Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements); /// /// Checks if a user meets a specific authorization policy /// /// The user to check the policy against. - /// The resource the policy should be checked with. + /// + /// An optional resource the policy should be checked with. + /// If a resource is not required for policy evaluation you may pass null as the value. + /// /// The name of the policy to check against a specific context. - /// true when the user fulfills the policy, false otherwise. - Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName); + /// + /// A flag indicating whether authorization has succeeded. + /// Returns a flag indicating whether the user, and optional resource has fulfilled the policy. + /// true when the policy has been fulfilled; otherwise false. + /// + /// + /// Resource is an optional parameter and may be null. Please ensure that you check it is not + /// null before acting upon it. + /// + Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization/IAuthorizeData.cs b/src/Microsoft.AspNetCore.Authorization/IAuthorizeData.cs index 3371134b4..1196db82d 100644 --- a/src/Microsoft.AspNetCore.Authorization/IAuthorizeData.cs +++ b/src/Microsoft.AspNetCore.Authorization/IAuthorizeData.cs @@ -14,10 +14,13 @@ public interface IAuthorizeData string Policy { get; set; } /// - /// Gets or sets a comma-separated list of roles that are allowed to access the resource. + /// Gets or sets a comma delimited list of roles that are allowed to access the resource. /// string Roles { get; set; } - string ActiveAuthenticationSchemes { get; set; } + /// + /// Gets or sets a comma delimited list of schemes from which user information is constructed. + /// + string AuthenticationSchemes { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Authorization/Infrastructure/AssertionRequirement.cs b/src/Microsoft.AspNetCore.Authorization/Infrastructure/AssertionRequirement.cs index 0cc1751a4..5fa452b73 100644 --- a/src/Microsoft.AspNetCore.Authorization/Infrastructure/AssertionRequirement.cs +++ b/src/Microsoft.AspNetCore.Authorization/Infrastructure/AssertionRequirement.cs @@ -6,34 +6,50 @@ namespace Microsoft.AspNetCore.Authorization.Infrastructure { + /// + /// Implements an and + /// that takes a user specified assertion. + /// public class AssertionRequirement : IAuthorizationHandler, IAuthorizationRequirement { /// - /// Function that is called to handle this requirement + /// Function that is called to handle this requirement. /// - public Func> Handler { get; } + public Func> Handler { get; } - public AssertionRequirement(Func assert) + /// + /// Creates a new instance of . + /// + /// Function that is called to handle this requirement. + public AssertionRequirement(Func handler) { - if (assert == null) + if (handler == null) { - throw new ArgumentNullException(nameof(assert)); + throw new ArgumentNullException(nameof(handler)); } - Handler = context => Task.FromResult(assert(context)); + Handler = context => Task.FromResult(handler(context)); } - public AssertionRequirement(Func> assert) + /// + /// Creates a new instance of . + /// + /// Function that is called to handle this requirement. + public AssertionRequirement(Func> handler) { - if (assert == null) + if (handler == null) { - throw new ArgumentNullException(nameof(assert)); + throw new ArgumentNullException(nameof(handler)); } - Handler = assert; + Handler = handler; } - public async Task HandleAsync(AuthorizationContext context) + /// + /// Calls to see if authorization is allowed. + /// + /// The authorization information. + public async Task HandleAsync(AuthorizationHandlerContext context) { if (await Handler(context)) { diff --git a/src/Microsoft.AspNetCore.Authorization/Infrastructure/ClaimsAuthorizationRequirement.cs b/src/Microsoft.AspNetCore.Authorization/Infrastructure/ClaimsAuthorizationRequirement.cs index bff54954d..93b1deea6 100644 --- a/src/Microsoft.AspNetCore.Authorization/Infrastructure/ClaimsAuthorizationRequirement.cs +++ b/src/Microsoft.AspNetCore.Authorization/Infrastructure/ClaimsAuthorizationRequirement.cs @@ -4,13 +4,23 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace Microsoft.AspNetCore.Authorization.Infrastructure { - // Must contain a claim with the specified name, and at least one of the required values - // If AllowedValues is null or empty, that means any claim is valid + /// + /// Implements an and + /// which requires at least one instance of the specified claim type, and, if allowed values are specified, + /// the claim value must be any of the allowed values. + /// public class ClaimsAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement { + /// + /// Creates a new instance of . + /// + /// The claim type that must be present. + /// The optional list of claim values, which, if present, + /// the claim must match. public ClaimsAuthorizationRequirement(string claimType, IEnumerable allowedValues) { if (claimType == null) @@ -22,10 +32,23 @@ public ClaimsAuthorizationRequirement(string claimType, IEnumerable allo AllowedValues = allowedValues; } + /// + /// Gets the claim type that must be present. + /// public string ClaimType { get; } + + /// + /// Gets the optional list of claim values, which, if present, + /// the claim must match. + /// public IEnumerable AllowedValues { get; } - protected override void Handle(AuthorizationContext context, ClaimsAuthorizationRequirement requirement) + /// + /// Makes a decision if authorization is allowed based on the claims requirements specified. + /// + /// The authorization context. + /// The requirement to evaluate. + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ClaimsAuthorizationRequirement requirement) { if (context.User != null) { @@ -44,6 +67,7 @@ protected override void Handle(AuthorizationContext context, ClaimsAuthorization context.Succeed(requirement); } } + return Task.CompletedTask; } } } diff --git a/src/Microsoft.AspNetCore.Authorization/Infrastructure/DenyAnonymousAuthorizationRequirement.cs b/src/Microsoft.AspNetCore.Authorization/Infrastructure/DenyAnonymousAuthorizationRequirement.cs index 44ab475de..e88cce7aa 100644 --- a/src/Microsoft.AspNetCore.Authorization/Infrastructure/DenyAnonymousAuthorizationRequirement.cs +++ b/src/Microsoft.AspNetCore.Authorization/Infrastructure/DenyAnonymousAuthorizationRequirement.cs @@ -2,12 +2,22 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Linq; +using System.Threading.Tasks; namespace Microsoft.AspNetCore.Authorization.Infrastructure { + /// + /// Implements an and + /// which requires the current user must be authenticated. + /// public class DenyAnonymousAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement { - protected override void Handle(AuthorizationContext context, DenyAnonymousAuthorizationRequirement requirement) + /// + /// Makes a decision if authorization is allowed based on a specific requirement. + /// + /// The authorization context. + /// The requirement to evaluate. + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyAnonymousAuthorizationRequirement requirement) { var user = context.User; var userIsAnonymous = @@ -17,6 +27,7 @@ protected override void Handle(AuthorizationContext context, DenyAnonymousAuthor { context.Succeed(requirement); } + return Task.CompletedTask; } } } diff --git a/src/Microsoft.AspNetCore.Authorization/Infrastructure/NameAuthorizationRequirement.cs b/src/Microsoft.AspNetCore.Authorization/Infrastructure/NameAuthorizationRequirement.cs index a4f2455a6..02ab946fa 100644 --- a/src/Microsoft.AspNetCore.Authorization/Infrastructure/NameAuthorizationRequirement.cs +++ b/src/Microsoft.AspNetCore.Authorization/Infrastructure/NameAuthorizationRequirement.cs @@ -3,14 +3,20 @@ using System; using System.Linq; +using System.Threading.Tasks; namespace Microsoft.AspNetCore.Authorization.Infrastructure { /// - /// Requirement that ensures a specific Name + /// Implements an and + /// which requires the current user name must match the specified value. /// public class NameAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement { + /// + /// Constructs a new instance of . + /// + /// The required name that the current user must have. public NameAuthorizationRequirement(string requiredName) { if (requiredName == null) @@ -21,18 +27,26 @@ public NameAuthorizationRequirement(string requiredName) RequiredName = requiredName; } + /// + /// Gets the required name that the current user must have. + /// public string RequiredName { get; } - protected override void Handle(AuthorizationContext context, NameAuthorizationRequirement requirement) + /// + /// Makes a decision if authorization is allowed based on a specific requirement. + /// + /// The authorization context. + /// The requirement to evaluate. + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NameAuthorizationRequirement requirement) { if (context.User != null) { - // REVIEW: Do we need to do normalization? casing/loc? if (context.User.Identities.Any(i => string.Equals(i.Name, requirement.RequiredName))) { context.Succeed(requirement); } } + return Task.CompletedTask; } } } diff --git a/src/Microsoft.AspNetCore.Authorization/Infrastructure/OperationAuthorizationRequirement.cs b/src/Microsoft.AspNetCore.Authorization/Infrastructure/OperationAuthorizationRequirement.cs index 455d66ff2..c3f16356d 100644 --- a/src/Microsoft.AspNetCore.Authorization/Infrastructure/OperationAuthorizationRequirement.cs +++ b/src/Microsoft.AspNetCore.Authorization/Infrastructure/OperationAuthorizationRequirement.cs @@ -3,8 +3,15 @@ namespace Microsoft.AspNetCore.Authorization.Infrastructure { + /// + /// A helper class to provide a useful which + /// contains a name. + /// public class OperationAuthorizationRequirement : IAuthorizationRequirement { + /// + /// The name of this instance of . + /// public string Name { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Authorization/Infrastructure/PassThroughAuthorizationHandler.cs b/src/Microsoft.AspNetCore.Authorization/Infrastructure/PassThroughAuthorizationHandler.cs index 480e2de0c..6f0b8293f 100644 --- a/src/Microsoft.AspNetCore.Authorization/Infrastructure/PassThroughAuthorizationHandler.cs +++ b/src/Microsoft.AspNetCore.Authorization/Infrastructure/PassThroughAuthorizationHandler.cs @@ -6,9 +6,17 @@ namespace Microsoft.AspNetCore.Authorization.Infrastructure { + /// + /// Infrastructure class which allows an to + /// be its own . + /// public class PassThroughAuthorizationHandler : IAuthorizationHandler { - public async Task HandleAsync(AuthorizationContext context) + /// + /// Makes a decision if authorization is allowed. + /// + /// The authorization context. + public async Task HandleAsync(AuthorizationHandlerContext context) { foreach (var handler in context.Requirements.OfType()) { diff --git a/src/Microsoft.AspNetCore.Authorization/Infrastructure/RolesAuthorizationRequirement.cs b/src/Microsoft.AspNetCore.Authorization/Infrastructure/RolesAuthorizationRequirement.cs index 217e3ea0c..811e17aac 100644 --- a/src/Microsoft.AspNetCore.Authorization/Infrastructure/RolesAuthorizationRequirement.cs +++ b/src/Microsoft.AspNetCore.Authorization/Infrastructure/RolesAuthorizationRequirement.cs @@ -4,13 +4,20 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace Microsoft.AspNetCore.Authorization.Infrastructure { - // Must belong to with one of specified roles - // If AllowedRoles is null or empty, that means any role is valid + /// + /// Implements an and + /// which requires at least one role claim whose value must be any of the allowed roles. + /// public class RolesAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement { + /// + /// Creates a new instance of . + /// + /// A collection of allowed roles. public RolesAuthorizationRequirement(IEnumerable allowedRoles) { if (allowedRoles == null) @@ -25,9 +32,18 @@ public RolesAuthorizationRequirement(IEnumerable allowedRoles) AllowedRoles = allowedRoles; } + /// + /// Gets the collection of allowed roles. + /// public IEnumerable AllowedRoles { get; } - protected override void Handle(AuthorizationContext context, RolesAuthorizationRequirement requirement) + /// + /// Makes a decision if authorization is allowed based on a specific requirement. + /// + /// The authorization context. + /// The requirement to evaluate. + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement) { if (context.User != null) { @@ -45,6 +61,7 @@ protected override void Handle(AuthorizationContext context, RolesAuthorizationR context.Succeed(requirement); } } + return Task.CompletedTask; } } diff --git a/src/Microsoft.AspNetCore.Authorization/LoggingExtensions.cs b/src/Microsoft.AspNetCore.Authorization/LoggingExtensions.cs index 1d524dd74..386df85e0 100644 --- a/src/Microsoft.AspNetCore.Authorization/LoggingExtensions.cs +++ b/src/Microsoft.AspNetCore.Authorization/LoggingExtensions.cs @@ -7,29 +7,25 @@ namespace Microsoft.Extensions.Logging { internal static class LoggingExtensions { - private static Action _userAuthorizationFailed; - private static Action _userAuthorizationSucceeded; + private static Action _userAuthorizationFailed; + private static Action _userAuthorizationSucceeded; static LoggingExtensions() { - _userAuthorizationSucceeded = LoggerMessage.Define( + _userAuthorizationSucceeded = LoggerMessage.Define( eventId: 1, logLevel: LogLevel.Information, - formatString: "Authorization was successful for user: {UserName}."); - _userAuthorizationFailed = LoggerMessage.Define( + formatString: "Authorization was successful."); + _userAuthorizationFailed = LoggerMessage.Define( eventId: 2, logLevel: LogLevel.Information, - formatString: "Authorization failed for user: {UserName}."); + formatString: "Authorization failed."); } - public static void UserAuthorizationSucceeded(this ILogger logger, string userName) - { - _userAuthorizationSucceeded(logger, userName, null); - } + public static void UserAuthorizationSucceeded(this ILogger logger) + => _userAuthorizationSucceeded(logger, null); - public static void UserAuthorizationFailed(this ILogger logger, string userName) - { - _userAuthorizationFailed(logger, userName, null); - } + public static void UserAuthorizationFailed(this ILogger logger) + => _userAuthorizationFailed(logger, null); } } diff --git a/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj b/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj new file mode 100644 index 000000000..9192ac8cf --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj @@ -0,0 +1,20 @@ + + + + ASP.NET Core authorization classes. +Commonly used types: +Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute +Microsoft.AspNetCore.Authorization.AuthorizeAttribute + netcoreapp3.0 + $(NoWarn);CS1591 + true + aspnetcore;authorization + false + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.xproj b/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.xproj deleted file mode 100644 index c6a57567b..000000000 --- a/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.xproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 6ab3e514-5894-4131-9399-dc7d5284addb - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Authorization/Properties/AssemblyInfo.cs deleted file mode 100644 index 76feceeff..000000000 --- a/src/Microsoft.AspNetCore.Authorization/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Reflection; -using System.Resources; - -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: AssemblyCompany("Microsoft Corporation.")] -[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Authorization/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Authorization/Properties/Resources.Designer.cs index 116001e65..c83fa9ea5 100644 --- a/src/Microsoft.AspNetCore.Authorization/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Authorization/Properties/Resources.Designer.cs @@ -15,48 +15,42 @@ private static readonly ResourceManager _resourceManager /// internal static string Exception_AuthorizationPolicyEmpty { - get { return GetString("Exception_AuthorizationPolicyEmpty"); } + get => GetString("Exception_AuthorizationPolicyEmpty"); } /// /// AuthorizationPolicy must have at least one requirement. /// internal static string FormatException_AuthorizationPolicyEmpty() - { - return GetString("Exception_AuthorizationPolicyEmpty"); - } + => GetString("Exception_AuthorizationPolicyEmpty"); /// /// The AuthorizationPolicy named: '{0}' was not found. /// internal static string Exception_AuthorizationPolicyNotFound { - get { return GetString("Exception_AuthorizationPolicyNotFound"); } + get => GetString("Exception_AuthorizationPolicyNotFound"); } /// /// The AuthorizationPolicy named: '{0}' was not found. /// internal static string FormatException_AuthorizationPolicyNotFound(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("Exception_AuthorizationPolicyNotFound"), p0); - } + => string.Format(CultureInfo.CurrentCulture, GetString("Exception_AuthorizationPolicyNotFound"), p0); /// /// At least one role must be specified. /// internal static string Exception_RoleRequirementEmpty { - get { return GetString("Exception_RoleRequirementEmpty"); } + get => GetString("Exception_RoleRequirementEmpty"); } /// /// At least one role must be specified. /// internal static string FormatException_RoleRequirementEmpty() - { - return GetString("Exception_RoleRequirementEmpty"); - } + => GetString("Exception_RoleRequirementEmpty"); private static string GetString(string name, params string[] formatterNames) { diff --git a/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json b/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json new file mode 100644 index 000000000..9910c93f6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json @@ -0,0 +1,1947 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Authorization, Version=2.0.3.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.AuthorizationServiceCollectionExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddAuthorization", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddAuthorization", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "configure", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.Attribute", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAllowAnonymous" + ], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationFailure", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_FailCalled", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FailedRequirements", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ExplicitFail", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationFailure", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Failed", + "Parameters": [ + { + "Name": "failed", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationFailure", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationHandler", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRequirementAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + }, + { + "Name": "requirement", + "Type": "T0" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TRequirement", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationHandler", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRequirementAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + }, + { + "Name": "requirement", + "Type": "T0" + }, + { + "Name": "resource", + "Type": "T1" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TRequirement", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ] + }, + { + "ParameterName": "TResource", + "ParameterPosition": 1, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Requirements", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_User", + "Parameters": [], + "ReturnType": "System.Security.Claims.ClaimsPrincipal", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Resource", + "Parameters": [], + "ReturnType": "System.Object", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_PendingRequirements", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasFailed", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasSucceeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Fail", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Succeed", + "Parameters": [ + { + "Name": "requirement", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "requirements", + "Type": "System.Collections.Generic.IEnumerable" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_InvokeHandlersAfterFailure", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_InvokeHandlersAfterFailure", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DefaultPolicy", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultPolicy", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddPolicy", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddPolicy", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "configurePolicy", + "Type": "System.Action" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetPolicy", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Requirements", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IReadOnlyList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthenticationSchemes", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IReadOnlyList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Combine", + "Parameters": [ + { + "Name": "policies", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Combine", + "Parameters": [ + { + "Name": "policies", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "CombineAsync", + "Parameters": [ + { + "Name": "policyProvider", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider" + }, + { + "Name": "authorizeData", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "requirements", + "Type": "System.Collections.Generic.IEnumerable" + }, + { + "Name": "authenticationSchemes", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Requirements", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Requirements", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthenticationSchemes", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthenticationSchemes", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddAuthenticationSchemes", + "Parameters": [ + { + "Name": "schemes", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddRequirements", + "Parameters": [ + { + "Name": "requirements", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Combine", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireClaim", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "requiredValues", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireClaim", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "requiredValues", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireClaim", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireRole", + "Parameters": [ + { + "Name": "roles", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireRole", + "Parameters": [ + { + "Name": "roles", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireUserName", + "Parameters": [ + { + "Name": "userName", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireAuthenticatedUser", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireAssertion", + "Parameters": [ + { + "Name": "handler", + "Type": "System.Func" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "RequireAssertion", + "Parameters": [ + { + "Name": "handler", + "Type": "System.Func>" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Build", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "authenticationSchemes", + "Type": "System.String[]", + "IsParams": true + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationResult", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Succeeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Failure", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationFailure", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Success", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Failed", + "Parameters": [ + { + "Name": "failure", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationFailure" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Failed", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizationServiceExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "service", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + }, + { + "Name": "requirement", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "service", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + }, + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "service", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "policy", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationPolicy" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "service", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationService" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.AuthorizeAttribute", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.Attribute", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizeData" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Policy", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Policy", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Roles", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Roles", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthenticationSchemes", + "Parameters": [], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthenticationSchemes", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ActiveAuthenticationSchemes", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ActiveAuthenticationSchemes", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "policy", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationEvaluator", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Evaluate", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationHandlerContextFactory", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory" + ], + "Members": [ + { + "Kind": "Method", + "Name": "CreateContext", + "Parameters": [ + { + "Name": "requirements", + "Type": "System.Collections.Generic.IEnumerable" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationHandlerProvider", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetHandlersAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task>", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "handlers", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationPolicyProvider", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider" + ], + "Members": [ + { + "Kind": "Method", + "Name": "GetDefaultPolicyAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetPolicyAsync", + "Parameters": [ + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.DefaultAuthorizationService", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationService" + ], + "Members": [ + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + }, + { + "Name": "requirements", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + }, + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationService", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "policyProvider", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider" + }, + { + "Name": "handlers", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILogger" + }, + { + "Name": "contextFactory", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory" + }, + { + "Name": "evaluator", + "Type": "Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator" + }, + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAllowAnonymous", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationEvaluator", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Evaluate", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationResult", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "HandleAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerContextFactory", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CreateContext", + "Parameters": [ + { + "Name": "requirements", + "Type": "System.Collections.Generic.IEnumerable" + }, + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationHandlerProvider", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetHandlersAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task>", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationPolicyProvider", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "GetPolicyAsync", + "Parameters": [ + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetDefaultPolicyAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizationService", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + }, + { + "Name": "requirements", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AuthorizeAsync", + "Parameters": [ + { + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "resource", + "Type": "System.Object" + }, + { + "Name": "policyName", + "Type": "System.String" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.IAuthorizeData", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Policy", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Policy", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Roles", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Roles", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AuthenticationSchemes", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_AuthenticationSchemes", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.AssertionRequirement", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationHandler", + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Handler", + "Parameters": [], + "ReturnType": "System.Func>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "handler", + "Type": "System.Func" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "handler", + "Type": "System.Func>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authorization.AuthorizationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ClaimType", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AllowedValues", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRequirementAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + }, + { + "Name": "requirement", + "Type": "Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "claimType", + "Type": "System.String" + }, + { + "Name": "allowedValues", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.DenyAnonymousAuthorizationRequirement", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authorization.AuthorizationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleRequirementAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + }, + { + "Name": "requirement", + "Type": "Microsoft.AspNetCore.Authorization.Infrastructure.DenyAnonymousAuthorizationRequirement" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.NameAuthorizationRequirement", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authorization.AuthorizationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequiredName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRequirementAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + }, + { + "Name": "requirement", + "Type": "Microsoft.AspNetCore.Authorization.Infrastructure.NameAuthorizationRequirement" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "requiredName", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.OperationAuthorizationRequirement", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Name", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Name", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.PassThroughAuthorizationHandler", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationHandler" + ], + "Members": [ + { + "Kind": "Method", + "Name": "HandleAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authorization.IAuthorizationHandler", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Authorization.Infrastructure.RolesAuthorizationRequirement", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authorization.AuthorizationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authorization.IAuthorizationRequirement" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_AllowedRoles", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HandleRequirementAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext" + }, + { + "Name": "requirement", + "Type": "Microsoft.AspNetCore.Authorization.Infrastructure.RolesAuthorizationRequirement" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "allowedRoles", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authorization/project.json b/src/Microsoft.AspNetCore.Authorization/project.json deleted file mode 100644 index 0d14ab8ad..000000000 --- a/src/Microsoft.AspNetCore.Authorization/project.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "ASP.NET Core authorization classes.\r\nCommonly used types:\r\nMicrosoft.AspNetCore.Authorization.AllowAnonymousAttribute\r\nMicrosoft.AspNetCore.Authorization.AuthorizeAttribute", - "tags": [ - "aspnetcore", - "authorization" - ], - "repository": { - "type": "git", - "url": "git://github.com/aspnet/security" - }, - "compilationOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk", - "nowarn": [ - "CS1591" - ], - "xmlDoc": true - }, - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", - "Microsoft.Extensions.Options": "1.0.0-*" - }, - "frameworks": { - "net451": { - "frameworkAssemblies": { - "System.Runtime": { - "type": "build" - } - } - }, - "netstandard1.3": { - "dependencies": { - "System.Security.Claims": "4.0.1-*" - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs b/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs index 1b13251f7..bbb4899c0 100644 --- a/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs +++ b/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs @@ -19,5 +19,8 @@ public AppendCookieContext(HttpContext context, CookieOptions options, string na public CookieOptions CookieOptions { get; } public string CookieName { get; set; } public string CookieValue { get; set; } + public bool IsConsentNeeded { get; internal set; } + public bool HasConsent { get; internal set; } + public bool IssueCookie { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyAppBuilderExtensions.cs index bb5700fc6..1564193b9 100644 --- a/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyAppBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyAppBuilderExtensions.cs @@ -13,9 +13,9 @@ namespace Microsoft.AspNetCore.Builder public static class CookiePolicyAppBuilderExtensions { /// - /// Adds the middleware to the specified , which enables cookie policy capabilities. + /// Adds the handler to the specified , which enables cookie policy capabilities. /// - /// The to add the middleware to. + /// The to add the handler to. /// A reference to this instance after the operation has completed. public static IApplicationBuilder UseCookiePolicy(this IApplicationBuilder app) { @@ -28,10 +28,10 @@ public static IApplicationBuilder UseCookiePolicy(this IApplicationBuilder app) } /// - /// Adds the middleware to the specified , which enables cookie policy capabilities. + /// Adds the handler to the specified , which enables cookie policy capabilities. /// - /// The to add the middleware to. - /// A that specifies options for the middleware. + /// The to add the handler to. + /// A that specifies options for the handler. /// A reference to this instance after the operation has completed. public static IApplicationBuilder UseCookiePolicy(this IApplicationBuilder app, CookiePolicyOptions options) { diff --git a/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs b/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs index 7b52a5880..1a810b7d5 100644 --- a/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs +++ b/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs @@ -6,6 +6,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.CookiePolicy @@ -13,13 +15,20 @@ namespace Microsoft.AspNetCore.CookiePolicy public class CookiePolicyMiddleware { private readonly RequestDelegate _next; + private readonly ILogger _logger; - public CookiePolicyMiddleware( - RequestDelegate next, - IOptions options) + public CookiePolicyMiddleware(RequestDelegate next, IOptions options, ILoggerFactory factory) + { + Options = options.Value; + _next = next ?? throw new ArgumentNullException(nameof(next)); + _logger = factory.CreateLogger(); + } + + public CookiePolicyMiddleware(RequestDelegate next, IOptions options) { Options = options.Value; _next = next; + _logger = NullLogger.Instance; } public CookiePolicyOptions Options { get; set; } @@ -27,141 +36,21 @@ public CookiePolicyMiddleware( public Task Invoke(HttpContext context) { var feature = context.Features.Get() ?? new ResponseCookiesFeature(context.Features); - context.Features.Set(new CookiesWrapperFeature(context, Options, feature)); + var wrapper = new ResponseCookiesWrapper(context, Options, feature, _logger); + context.Features.Set(new CookiesWrapperFeature(wrapper)); + context.Features.Set(wrapper); + return _next(context); } private class CookiesWrapperFeature : IResponseCookiesFeature { - public CookiesWrapperFeature(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature) - { - Wrapper = new CookiesWrapper(context, options, feature); - } - - public IResponseCookies Wrapper { get; } - - public IResponseCookies Cookies - { - get - { - return Wrapper; - } - } - } - - private class CookiesWrapper : IResponseCookies - { - public CookiesWrapper(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature) - { - Context = context; - Feature = feature; - Policy = options; - } - - public HttpContext Context { get; } - - public IResponseCookiesFeature Feature { get; } - - public IResponseCookies Cookies + public CookiesWrapperFeature(ResponseCookiesWrapper wrapper) { - get - { - return Feature.Cookies; - } + Cookies = wrapper; } - public CookiePolicyOptions Policy { get; } - - private bool PolicyRequiresCookieOptions() - { - return Policy.HttpOnly != HttpOnlyPolicy.None || Policy.Secure != SecurePolicy.None; - } - - public void Append(string key, string value) - { - if (PolicyRequiresCookieOptions() || Policy.OnAppendCookie != null) - { - Append(key, value, new CookieOptions()); - } - else - { - Cookies.Append(key, value); - } - } - - public void Append(string key, string value, CookieOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - ApplyPolicy(options); - if (Policy.OnAppendCookie != null) - { - var context = new AppendCookieContext(Context, options, key, value); - Policy.OnAppendCookie(context); - key = context.CookieName; - value = context.CookieValue; - } - Cookies.Append(key, value, options); - } - - public void Delete(string key) - { - if (PolicyRequiresCookieOptions() || Policy.OnDeleteCookie != null) - { - Delete(key, new CookieOptions()); - } - else - { - Cookies.Delete(key); - } - } - - public void Delete(string key, CookieOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - ApplyPolicy(options); - if (Policy.OnDeleteCookie != null) - { - var context = new DeleteCookieContext(Context, options, key); - Policy.OnDeleteCookie(context); - key = context.CookieName; - } - Cookies.Delete(key, options); - } - - private void ApplyPolicy(CookieOptions options) - { - switch (Policy.Secure) - { - case SecurePolicy.Always: - options.Secure = true; - break; - case SecurePolicy.SameAsRequest: - options.Secure = Context.Request.IsHttps; - break; - case SecurePolicy.None: - break; - default: - throw new InvalidOperationException(); - } - switch (Policy.HttpOnly) - { - case HttpOnlyPolicy.Always: - options.HttpOnly = true; - break; - case HttpOnlyPolicy.None: - break; - default: - throw new InvalidOperationException(); - } - } + public IResponseCookies Cookies { get; } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs b/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs index 8201b5863..32d047297 100644 --- a/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs +++ b/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.CookiePolicy; +using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Builder { @@ -11,14 +12,32 @@ namespace Microsoft.AspNetCore.Builder /// public class CookiePolicyOptions { + /// + /// Affects the cookie's same site attribute. + /// + public SameSiteMode MinimumSameSitePolicy { get; set; } = SameSiteMode.Lax; + /// /// Affects whether cookies must be HttpOnly. /// public HttpOnlyPolicy HttpOnly { get; set; } = HttpOnlyPolicy.None; + /// /// Affects whether cookies must be Secure. /// - public SecurePolicy Secure { get; set; } = SecurePolicy.None; + public CookieSecurePolicy Secure { get; set; } = CookieSecurePolicy.None; + + public CookieBuilder ConsentCookie { get; set; } = new CookieBuilder() + { + Name = ".AspNet.Consent", + Expiration = TimeSpan.FromDays(365), + IsEssential = true, + }; + + /// + /// Checks if consent policies should be evaluated on this request. The default is false. + /// + public Func CheckConsentNeeded { get; set; } /// /// Called when a cookie is appended. diff --git a/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs b/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs index f0693bf71..fd79ea8d4 100644 --- a/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs +++ b/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs @@ -17,5 +17,8 @@ public DeleteCookieContext(HttpContext context, CookieOptions options, string na public HttpContext Context { get; } public CookieOptions CookieOptions { get; } public string CookieName { get; set; } + public bool IsConsentNeeded { get; internal set; } + public bool HasConsent { get; internal set; } + public bool IssueCookie { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.CookiePolicy/LoggingExtensions.cs b/src/Microsoft.AspNetCore.CookiePolicy/LoggingExtensions.cs new file mode 100644 index 000000000..21b04facc --- /dev/null +++ b/src/Microsoft.AspNetCore.CookiePolicy/LoggingExtensions.cs @@ -0,0 +1,105 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging +{ + internal static class LoggingExtensions + { + private static Action _needsConsent; + private static Action _hasConsent; + private static Action _consentGranted; + private static Action _consentWithdrawn; + private static Action _cookieSuppressed; + private static Action _deleteCookieSuppressed; + private static Action _upgradedToSecure; + private static Action _upgradedSameSite; + private static Action _upgradedToHttpOnly; + + static LoggingExtensions() + { + _needsConsent = LoggerMessage.Define( + eventId: 1, + logLevel: LogLevel.Trace, + formatString: "Needs consent: {needsConsent}."); + _hasConsent = LoggerMessage.Define( + eventId: 2, + logLevel: LogLevel.Trace, + formatString: "Has consent: {hasConsent}."); + _consentGranted = LoggerMessage.Define( + eventId: 3, + logLevel: LogLevel.Debug, + formatString: "Consent granted."); + _consentWithdrawn = LoggerMessage.Define( + eventId: 4, + logLevel: LogLevel.Debug, + formatString: "Consent withdrawn."); + _cookieSuppressed = LoggerMessage.Define( + eventId: 5, + logLevel: LogLevel.Debug, + formatString: "Cookie '{key}' suppressed due to consent policy."); + _deleteCookieSuppressed = LoggerMessage.Define( + eventId: 6, + logLevel: LogLevel.Debug, + formatString: "Delete cookie '{key}' suppressed due to developer policy."); + _upgradedToSecure = LoggerMessage.Define( + eventId: 7, + logLevel: LogLevel.Debug, + formatString: "Cookie '{key}' upgraded to 'secure'."); + _upgradedSameSite = LoggerMessage.Define( + eventId: 8, + logLevel: LogLevel.Debug, + formatString: "Cookie '{key}' same site mode upgraded to '{mode}'."); + _upgradedToHttpOnly = LoggerMessage.Define( + eventId: 9, + logLevel: LogLevel.Debug, + formatString: "Cookie '{key}' upgraded to 'httponly'."); + } + + public static void NeedsConsent(this ILogger logger, bool needsConsent) + { + _needsConsent(logger, needsConsent, null); + } + + public static void HasConsent(this ILogger logger, bool hasConsent) + { + _hasConsent(logger, hasConsent, null); + } + + public static void ConsentGranted(this ILogger logger) + { + _consentGranted(logger, null); + } + + public static void ConsentWithdrawn(this ILogger logger) + { + _consentWithdrawn(logger, null); + } + + public static void CookieSuppressed(this ILogger logger, string key) + { + _cookieSuppressed(logger, key, null); + } + + public static void DeleteCookieSuppressed(this ILogger logger, string key) + { + _deleteCookieSuppressed(logger, key, null); + } + + public static void CookieUpgradedToSecure(this ILogger logger, string key) + { + _upgradedToSecure(logger, key, null); + } + + public static void CookieSameSiteUpgraded(this ILogger logger, string key, string mode) + { + _upgradedSameSite(logger, key, mode, null); + } + + public static void CookieUpgradedToHttpOnly(this ILogger logger, string key) + { + _upgradedToHttpOnly(logger, key, null); + } + } +} diff --git a/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.csproj b/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.csproj new file mode 100644 index 000000000..81760d50a --- /dev/null +++ b/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.csproj @@ -0,0 +1,17 @@ + + + + ASP.NET Core cookie policy classes to control the behavior of cookies. + netcoreapp3.0 + $(NoWarn);CS1591 + true + aspnetcore + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.xproj b/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.xproj deleted file mode 100644 index 5ffad968d..000000000 --- a/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.xproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 86183dc3-02a8-4a68-8b60-71ecec066e79 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.CookiePolicy/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.CookiePolicy/Properties/AssemblyInfo.cs deleted file mode 100644 index 76feceeff..000000000 --- a/src/Microsoft.AspNetCore.CookiePolicy/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Reflection; -using System.Resources; - -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: NeutralResourcesLanguage("en-us")] -[assembly: AssemblyCompany("Microsoft Corporation.")] -[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs b/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs new file mode 100644 index 000000000..126c4d7bd --- /dev/null +++ b/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs @@ -0,0 +1,281 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.CookiePolicy +{ + internal class ResponseCookiesWrapper : IResponseCookies, ITrackingConsentFeature + { + private const string ConsentValue = "yes"; + private readonly ILogger _logger; + private bool? _isConsentNeeded; + private bool? _hasConsent; + + public ResponseCookiesWrapper(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature, ILogger logger) + { + Context = context; + Feature = feature; + Options = options; + _logger = logger; + } + + private HttpContext Context { get; } + + private IResponseCookiesFeature Feature { get; } + + private IResponseCookies Cookies => Feature.Cookies; + + private CookiePolicyOptions Options { get; } + + public bool IsConsentNeeded + { + get + { + if (!_isConsentNeeded.HasValue) + { + _isConsentNeeded = Options.CheckConsentNeeded == null ? false + : Options.CheckConsentNeeded(Context); + _logger.NeedsConsent(_isConsentNeeded.Value); + } + + return _isConsentNeeded.Value; + } + } + + public bool HasConsent + { + get + { + if (!_hasConsent.HasValue) + { + var cookie = Context.Request.Cookies[Options.ConsentCookie.Name]; + _hasConsent = string.Equals(cookie, ConsentValue, StringComparison.Ordinal); + _logger.HasConsent(_hasConsent.Value); + } + + return _hasConsent.Value; + } + } + + public bool CanTrack => !IsConsentNeeded || HasConsent; + + public void GrantConsent() + { + if (!HasConsent && !Context.Response.HasStarted) + { + var cookieOptions = Options.ConsentCookie.Build(Context); + // Note policy will be applied. We don't want to bypass policy because we want HttpOnly, Secure, etc. to apply. + Append(Options.ConsentCookie.Name, ConsentValue, cookieOptions); + _logger.ConsentGranted(); + } + _hasConsent = true; + } + + public void WithdrawConsent() + { + if (HasConsent && !Context.Response.HasStarted) + { + var cookieOptions = Options.ConsentCookie.Build(Context); + // Note policy will be applied. We don't want to bypass policy because we want HttpOnly, Secure, etc. to apply. + Delete(Options.ConsentCookie.Name, cookieOptions); + _logger.ConsentWithdrawn(); + } + _hasConsent = false; + } + + // Note policy will be applied. We don't want to bypass policy because we want HttpOnly, Secure, etc. to apply. + public string CreateConsentCookie() + { + var key = Options.ConsentCookie.Name; + var value = ConsentValue; + var options = Options.ConsentCookie.Build(Context); + ApplyAppendPolicy(ref key, ref value, options); + + var setCookieHeaderValue = new Net.Http.Headers.SetCookieHeaderValue( + Uri.EscapeDataString(key), + Uri.EscapeDataString(value)) + { + Domain = options.Domain, + Path = options.Path, + Expires = options.Expires, + MaxAge = options.MaxAge, + Secure = options.Secure, + SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite, + HttpOnly = options.HttpOnly + }; + + return setCookieHeaderValue.ToString(); + } + + private bool CheckPolicyRequired() + { + return !CanTrack + || Options.MinimumSameSitePolicy != SameSiteMode.None + || Options.HttpOnly != HttpOnlyPolicy.None + || Options.Secure != CookieSecurePolicy.None; + } + + public void Append(string key, string value) + { + if (CheckPolicyRequired() || Options.OnAppendCookie != null) + { + Append(key, value, new CookieOptions()); + } + else + { + Cookies.Append(key, value); + } + } + + public void Append(string key, string value, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (ApplyAppendPolicy(ref key, ref value, options)) + { + Cookies.Append(key, value, options); + } + else + { + _logger.CookieSuppressed(key); + } + } + + private bool ApplyAppendPolicy(ref string key, ref string value, CookieOptions options) + { + var issueCookie = CanTrack || options.IsEssential; + ApplyPolicy(key, options); + if (Options.OnAppendCookie != null) + { + var context = new AppendCookieContext(Context, options, key, value) + { + IsConsentNeeded = IsConsentNeeded, + HasConsent = HasConsent, + IssueCookie = issueCookie, + }; + Options.OnAppendCookie(context); + + key = context.CookieName; + value = context.CookieValue; + issueCookie = context.IssueCookie; + } + + return issueCookie; + } + + public void Delete(string key) + { + if (CheckPolicyRequired() || Options.OnDeleteCookie != null) + { + Delete(key, new CookieOptions()); + } + else + { + Cookies.Delete(key); + } + } + + public void Delete(string key, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + // Assume you can always delete cookies unless directly overridden in the user event. + var issueCookie = true; + ApplyPolicy(key, options); + if (Options.OnDeleteCookie != null) + { + var context = new DeleteCookieContext(Context, options, key) + { + IsConsentNeeded = IsConsentNeeded, + HasConsent = HasConsent, + IssueCookie = issueCookie, + }; + Options.OnDeleteCookie(context); + + key = context.CookieName; + issueCookie = context.IssueCookie; + } + + if (issueCookie) + { + Cookies.Delete(key, options); + } + else + { + _logger.DeleteCookieSuppressed(key); + } + } + + private void ApplyPolicy(string key, CookieOptions options) + { + switch (Options.Secure) + { + case CookieSecurePolicy.Always: + if (!options.Secure) + { + options.Secure = true; + _logger.CookieUpgradedToSecure(key); + } + break; + case CookieSecurePolicy.SameAsRequest: + // Never downgrade a cookie + if (Context.Request.IsHttps && !options.Secure) + { + options.Secure = true; + _logger.CookieUpgradedToSecure(key); + } + break; + case CookieSecurePolicy.None: + break; + default: + throw new InvalidOperationException(); + } + switch (Options.MinimumSameSitePolicy) + { + case SameSiteMode.None: + break; + case SameSiteMode.Lax: + if (options.SameSite == SameSiteMode.None) + { + options.SameSite = SameSiteMode.Lax; + _logger.CookieSameSiteUpgraded(key, "lax"); + } + break; + case SameSiteMode.Strict: + if (options.SameSite != SameSiteMode.Strict) + { + options.SameSite = SameSiteMode.Strict; + _logger.CookieSameSiteUpgraded(key, "strict"); + } + break; + default: + throw new InvalidOperationException($"Unrecognized {nameof(SameSiteMode)} value {Options.MinimumSameSitePolicy.ToString()}"); + } + switch (Options.HttpOnly) + { + case HttpOnlyPolicy.Always: + if (!options.HttpOnly) + { + options.HttpOnly = true; + _logger.CookieUpgradedToHttpOnly(key); + } + break; + case HttpOnlyPolicy.None: + break; + default: + throw new InvalidOperationException($"Unrecognized {nameof(HttpOnlyPolicy)} value {Options.HttpOnly.ToString()}"); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.CookiePolicy/SecurePolicy.cs b/src/Microsoft.AspNetCore.CookiePolicy/SecurePolicy.cs deleted file mode 100644 index c0dd639f1..000000000 --- a/src/Microsoft.AspNetCore.CookiePolicy/SecurePolicy.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.CookiePolicy -{ - public enum SecurePolicy - { - None, - Always, - SameAsRequest - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json b/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json new file mode 100644 index 000000000..01a16c57a --- /dev/null +++ b/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json @@ -0,0 +1,548 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.CookiePolicy, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Builder.CookiePolicyAppBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseCookiePolicy", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseCookiePolicy", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Builder.CookiePolicyOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.CookiePolicyOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_MinimumSameSitePolicy", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.SameSiteMode", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MinimumSameSitePolicy", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.SameSiteMode" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HttpOnly", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HttpOnly", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Secure", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieSecurePolicy", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Secure", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieSecurePolicy" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ConsentCookie", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ConsentCookie", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.CookieBuilder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CheckConsentNeeded", + "Parameters": [], + "ReturnType": "System.Func", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CheckConsentNeeded", + "Parameters": [ + { + "Name": "value", + "Type": "System.Func" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnAppendCookie", + "Parameters": [], + "ReturnType": "System.Action", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnAppendCookie", + "Parameters": [ + { + "Name": "value", + "Type": "System.Action" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OnDeleteCookie", + "Parameters": [], + "ReturnType": "System.Action", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OnDeleteCookie", + "Parameters": [ + { + "Name": "value", + "Type": "System.Action" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.CookiePolicy.AppendCookieContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Context", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieOptions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieValue", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieValue", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsConsentNeeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasConsent", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IssueCookie", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IssueCookie", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.CookiePolicy.CookiePolicyMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Options", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Builder.CookiePolicyOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Options", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Builder.CookiePolicyOptions" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + }, + { + "Name": "factory", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.CookiePolicy.DeleteCookieContext", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Context", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieOptions", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IsConsentNeeded", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasConsent", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_IssueCookie", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_IssueCookie", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Http.CookieOptions" + }, + { + "Name": "name", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "None", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "Always", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.CookiePolicy/project.json b/src/Microsoft.AspNetCore.CookiePolicy/project.json deleted file mode 100644 index 72a86bf80..000000000 --- a/src/Microsoft.AspNetCore.CookiePolicy/project.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "ASP.NET Core cookie policy classes to control the behavior of cookies.", - "tags": [ - "aspnetcore" - ], - "repository": { - "type": "git", - "url": "git://github.com/aspnet/security" - }, - "compilationOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk", - "nowarn": [ - "CS1591" - ], - "xmlDoc": true - }, - "dependencies": { - "Microsoft.AspNetCore.Http": "1.0.0-*", - "Microsoft.Extensions.Options": "1.0.0-*" - }, - "frameworks": { - "net451": {}, - "netstandard1.3": {} - } -} \ No newline at end of file diff --git a/src/Microsoft.Owin.Security.Interop/ChunkingCookieManager.cs b/src/Microsoft.Owin.Security.Interop/ChunkingCookieManager.cs new file mode 100644 index 000000000..1ae4f00cd --- /dev/null +++ b/src/Microsoft.Owin.Security.Interop/ChunkingCookieManager.cs @@ -0,0 +1,280 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.Owin.Infrastructure; + +namespace Microsoft.Owin.Security.Interop +{ + // This MUST be kept in sync with Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager + /// + /// This handles cookies that are limited by per cookie length. It breaks down long cookies for responses, and reassembles them + /// from requests. + /// + public class ChunkingCookieManager : ICookieManager + { + private const string ChunkKeySuffix = "C"; + private const string ChunkCountPrefix = "chunks-"; + + public ChunkingCookieManager() + { + // Lowest common denominator. Safari has the lowest known limit (4093), and we leave little extra just in case. + // See http://browsercookielimits.x64.me/. + // Leave at least 20 in case CookiePolicy tries to add 'secure' and/or 'httponly'. + ChunkSize = 4070; + } + + /// + /// The maximum size of cookie to send back to the client. If a cookie exceeds this size it will be broken down into multiple + /// cookies. Set this value to null to disable this behavior. The default is 4090 characters, which is supported by all + /// common browsers. + /// + /// Note that browsers may also have limits on the total size of all cookies per domain, and on the number of cookies per domain. + /// + public int? ChunkSize { get; set; } + + /// + /// Throw if not all chunks of a cookie are available on a request for re-assembly. + /// + public bool ThrowForPartialCookies { get; set; } + + // Parse the "chunks-XX" to determine how many chunks there should be. + private static int ParseChunksCount(string value) + { + if (value != null && value.StartsWith(ChunkCountPrefix, StringComparison.Ordinal)) + { + var chunksCountString = value.Substring(ChunkCountPrefix.Length); + int chunksCount; + if (int.TryParse(chunksCountString, NumberStyles.None, CultureInfo.InvariantCulture, out chunksCount)) + { + return chunksCount; + } + } + return 0; + } + + /// + /// Get the reassembled cookie. Non chunked cookies are returned normally. + /// Cookies with missing chunks just have their "chunks-XX" header returned. + /// + /// + /// + /// The reassembled cookie, if any, or null. + public string GetRequestCookie(IOwinContext context, string key) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var requestCookies = context.Request.Cookies; + var value = requestCookies[key]; + var chunksCount = ParseChunksCount(value); + if (chunksCount > 0) + { + var chunks = new string[chunksCount]; + for (var chunkId = 1; chunkId <= chunksCount; chunkId++) + { + var chunk = requestCookies[key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture)]; + if (string.IsNullOrEmpty(chunk)) + { + if (ThrowForPartialCookies) + { + var totalSize = 0; + for (int i = 0; i < chunkId - 1; i++) + { + totalSize += chunks[i].Length; + } + throw new FormatException( + string.Format(CultureInfo.CurrentCulture, + "The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded.", + chunkId - 1, chunksCount, totalSize)); + } + // Missing chunk, abort by returning the original cookie value. It may have been a false positive? + return value; + } + + chunks[chunkId - 1] = chunk; + } + + return string.Join(string.Empty, chunks); + } + return value; + } + + /// + /// Appends a new response cookie to the Set-Cookie header. If the cookie is larger than the given size limit + /// then it will be broken down into multiple cookies as follows: + /// Set-Cookie: CookieName=chunks-3; path=/ + /// Set-Cookie: CookieNameC1=Segment1; path=/ + /// Set-Cookie: CookieNameC2=Segment2; path=/ + /// Set-Cookie: CookieNameC3=Segment3; path=/ + /// + /// + /// + /// + /// + public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var domainHasValue = !string.IsNullOrEmpty(options.Domain); + var pathHasValue = !string.IsNullOrEmpty(options.Path); + var expiresHasValue = options.Expires.HasValue; + + var templateLength = key.Length + "=".Length + + (domainHasValue ? "; domain=".Length + options.Domain.Length : 0) + + (pathHasValue ? "; path=".Length + options.Path.Length : 0) + + (expiresHasValue ? "; expires=ddd, dd-MMM-yyyy HH:mm:ss GMT".Length : 0) + + (options.Secure ? "; secure".Length : 0) + + (options.HttpOnly ? "; HttpOnly".Length : 0); + + // Normal cookie + var responseCookies = context.Response.Cookies; + if (!ChunkSize.HasValue || ChunkSize.Value > templateLength + value.Length) + { + responseCookies.Append(key, value, options); + } + else if (ChunkSize.Value < templateLength + 10) + { + // 10 is the minimum data we want to put in an individual cookie, including the cookie chunk identifier "CXX". + // No room for data, we can't chunk the options and name + throw new InvalidOperationException("The cookie key and options are larger than ChunksSize, leaving no room for data."); + } + else + { + // Break the cookie down into multiple cookies. + // Key = CookieName, value = "Segment1Segment2Segment2" + // Set-Cookie: CookieName=chunks-3; path=/ + // Set-Cookie: CookieNameC1="Segment1"; path=/ + // Set-Cookie: CookieNameC2="Segment2"; path=/ + // Set-Cookie: CookieNameC3="Segment3"; path=/ + var dataSizePerCookie = ChunkSize.Value - templateLength - 3; // Budget 3 chars for the chunkid. + var cookieChunkCount = (int)Math.Ceiling(value.Length * 1.0 / dataSizePerCookie); + + responseCookies.Append(key, ChunkCountPrefix + cookieChunkCount.ToString(CultureInfo.InvariantCulture), options); + + var offset = 0; + for (var chunkId = 1; chunkId <= cookieChunkCount; chunkId++) + { + var remainingLength = value.Length - offset; + var length = Math.Min(dataSizePerCookie, remainingLength); + var segment = value.Substring(offset, length); + offset += length; + + responseCookies.Append(key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture), segment, options); + } + } + } + + /// + /// Deletes the cookie with the given key by setting an expired state. If a matching chunked cookie exists on + /// the request, delete each chunk. + /// + /// + /// + /// + public void DeleteCookie(IOwinContext context, string key, CookieOptions options) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var keys = new List(); + keys.Add(key + "="); + + var requestCookie = context.Request.Cookies[key]; + var chunks = ParseChunksCount(requestCookie); + if (chunks > 0) + { + for (int i = 1; i <= chunks + 1; i++) + { + var subkey = key + ChunkKeySuffix + i.ToString(CultureInfo.InvariantCulture); + keys.Add(subkey + "="); + } + } + + var domainHasValue = !string.IsNullOrEmpty(options.Domain); + var pathHasValue = !string.IsNullOrEmpty(options.Path); + + Func rejectPredicate; + Func predicate = value => keys.Any(k => value.StartsWith(k, StringComparison.OrdinalIgnoreCase)); + if (domainHasValue) + { + rejectPredicate = value => predicate(value) && value.IndexOf("domain=" + options.Domain, StringComparison.OrdinalIgnoreCase) != -1; + } + else if (pathHasValue) + { + rejectPredicate = value => predicate(value) && value.IndexOf("path=" + options.Path, StringComparison.OrdinalIgnoreCase) != -1; + } + else + { + rejectPredicate = value => predicate(value); + } + + var responseHeaders = context.Response.Headers; + string[] existingValues; + if (responseHeaders.TryGetValue(Constants.Headers.SetCookie, out existingValues) && existingValues != null) + { + responseHeaders.SetValues(Constants.Headers.SetCookie, existingValues.Where(value => !rejectPredicate(value)).ToArray()); + } + + AppendResponseCookie( + context, + key, + string.Empty, + new CookieOptions() + { + Path = options.Path, + Domain = options.Domain, + Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + }); + + for (int i = 1; i <= chunks; i++) + { + AppendResponseCookie( + context, + key + "C" + i.ToString(CultureInfo.InvariantCulture), + string.Empty, + new CookieOptions() + { + Path = options.Path, + Domain = options.Domain, + Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + }); + } + } + } +} diff --git a/src/Microsoft.Owin.Security.Interop/Constants.cs b/src/Microsoft.Owin.Security.Interop/Constants.cs new file mode 100644 index 000000000..1e75761b7 --- /dev/null +++ b/src/Microsoft.Owin.Security.Interop/Constants.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Owin.Security.Interop +{ + internal static class Constants + { + internal static class Headers + { + internal const string SetCookie = "Set-Cookie"; + } + } +} diff --git a/src/Microsoft.Owin.Security.Interop/Microsoft.Owin.Security.Interop.csproj b/src/Microsoft.Owin.Security.Interop/Microsoft.Owin.Security.Interop.csproj new file mode 100644 index 000000000..a12bc6563 --- /dev/null +++ b/src/Microsoft.Owin.Security.Interop/Microsoft.Owin.Security.Interop.csproj @@ -0,0 +1,16 @@ + + + + A compatibility layer for sharing authentication tickets between Microsoft.Owin.Security and Microsoft.AspNetCore.Authentication. + net461 + $(NoWarn);CS1591 + true + aspnetcore;katana;owin;security + + + + + + + + diff --git a/src/Microsoft.Owin.Security.Interop/Microsoft.Owin.Security.Interop.xproj b/src/Microsoft.Owin.Security.Interop/Microsoft.Owin.Security.Interop.xproj deleted file mode 100644 index c6b01504a..000000000 --- a/src/Microsoft.Owin.Security.Interop/Microsoft.Owin.Security.Interop.xproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - a7922dd8-09f1-43e4-938b-cc523ea08898 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/Microsoft.Owin.Security.Interop/Properties/AssemblyInfo.cs b/src/Microsoft.Owin.Security.Interop/Properties/AssemblyInfo.cs index 56cb77fdc..490fa7cb2 100644 --- a/src/Microsoft.Owin.Security.Interop/Properties/AssemblyInfo.cs +++ b/src/Microsoft.Owin.Security.Interop/Properties/AssemblyInfo.cs @@ -1,15 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Reflection; -using System.Resources; using System.Runtime.InteropServices; -[assembly: AssemblyMetadata("Serviceable", "True")] -[assembly: NeutralResourcesLanguage("en-us")] - // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("a7922dd8-09f1-43e4-938b-cc523ea08898")] -[assembly: AssemblyCompany("Microsoft Corporation.")] -[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyProduct("Microsoft ASP.NET Core")] + diff --git a/src/Microsoft.Owin.Security.Interop/baseline.netframework.json b/src/Microsoft.Owin.Security.Interop/baseline.netframework.json new file mode 100644 index 000000000..bfc0c0076 --- /dev/null +++ b/src/Microsoft.Owin.Security.Interop/baseline.netframework.json @@ -0,0 +1,372 @@ +{ + "AssemblyIdentity": "Microsoft.Owin.Security.Interop, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Owin.Security.Interop.AspNetTicketDataFormat", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.Owin.Security.DataHandler.SecureDataFormat", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "protector", + "Type": "Microsoft.Owin.Security.DataProtection.IDataProtector" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Owin.Security.Interop.AspNetTicketSerializer", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Owin.Security.DataHandler.Serializer.IDataSerializer" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Default", + "Parameters": [], + "ReturnType": "Microsoft.Owin.Security.Interop.AspNetTicketSerializer", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Serialize", + "Parameters": [ + { + "Name": "ticket", + "Type": "Microsoft.Owin.Security.AuthenticationTicket" + } + ], + "ReturnType": "System.Byte[]", + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Security.DataHandler.Serializer.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Deserialize", + "Parameters": [ + { + "Name": "data", + "Type": "System.Byte[]" + } + ], + "ReturnType": "Microsoft.Owin.Security.AuthenticationTicket", + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Security.DataHandler.Serializer.IDataSerializer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "ticket", + "Type": "Microsoft.Owin.Security.AuthenticationTicket" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteIdentity", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteClaim", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.BinaryWriter" + }, + { + "Name": "claim", + "Type": "System.Security.Claims.Claim" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Read", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + } + ], + "ReturnType": "Microsoft.Owin.Security.AuthenticationTicket", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadIdentity", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + } + ], + "ReturnType": "System.Security.Claims.ClaimsIdentity", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadClaim", + "Parameters": [ + { + "Name": "reader", + "Type": "System.IO.BinaryReader" + }, + { + "Name": "identity", + "Type": "System.Security.Claims.ClaimsIdentity" + } + ], + "ReturnType": "System.Security.Claims.Claim", + "Virtual": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Owin.Security.Interop.ChunkingCookieManager", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.Owin.Infrastructure.ICookieManager" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_ChunkSize", + "Parameters": [], + "ReturnType": "System.Nullable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ChunkSize", + "Parameters": [ + { + "Name": "value", + "Type": "System.Nullable" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ThrowForPartialCookies", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ThrowForPartialCookies", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetRequestCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.Owin.IOwinContext" + }, + { + "Name": "key", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Infrastructure.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendResponseCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.Owin.IOwinContext" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.Owin.CookieOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Infrastructure.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "DeleteCookie", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.Owin.IOwinContext" + }, + { + "Name": "key", + "Type": "System.String" + }, + { + "Name": "options", + "Type": "Microsoft.Owin.CookieOptions" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Infrastructure.ICookieManager", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.Owin.Security.Interop.DataProtectorShim", + "Visibility": "Public", + "Kind": "Class", + "Sealed": true, + "ImplementedInterfaces": [ + "Microsoft.Owin.Security.DataProtection.IDataProtector" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Protect", + "Parameters": [ + { + "Name": "userData", + "Type": "System.Byte[]" + } + ], + "ReturnType": "System.Byte[]", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Security.DataProtection.IDataProtector", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Unprotect", + "Parameters": [ + { + "Name": "protectedData", + "Type": "System.Byte[]" + } + ], + "ReturnType": "System.Byte[]", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.Owin.Security.DataProtection.IDataProtector", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "protector", + "Type": "Microsoft.AspNetCore.DataProtection.IDataProtector" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.Owin.Security.Interop/project.json b/src/Microsoft.Owin.Security.Interop/project.json deleted file mode 100644 index 691b16c42..000000000 --- a/src/Microsoft.Owin.Security.Interop/project.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "version": "1.0.0-*", - "compilationOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk", - "nowarn": [ "CS1591" ], - "xmlDoc": true - }, - "description": "A compatibility layer for sharing authentication tickets between Microsoft.Owin.Security and Microsoft.AspNetCore.Authentication.", - "tags": [ - "aspnetcore", - "katana", - "owin", - "security" - ], - "dependencies": { - "Microsoft.AspNetCore.DataProtection.Extensions": "1.0.0-*", - "Microsoft.Owin.Security": "3.0.1" - }, - "frameworks": { - "net451": { } - } -} \ No newline at end of file diff --git a/test/Directory.Build.props b/test/Directory.Build.props new file mode 100644 index 000000000..c42d54465 --- /dev/null +++ b/test/Directory.Build.props @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/test/Microsoft.AspNetCore.Authentication.Test/AuthenticationHandlerFacts.cs b/test/Microsoft.AspNetCore.Authentication.Test/AuthenticationHandlerFacts.cs deleted file mode 100644 index aa9aef07c..000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/AuthenticationHandlerFacts.cs +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.IO; -using System.Security.Claims; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Features.Authentication; -using Microsoft.Extensions.Logging; -using Xunit; - -namespace Microsoft.AspNetCore.Authentication -{ - public class AuthenticationHandlerFacts - { - [Fact] - public async Task ShouldHandleSchemeAreDeterminedOnlyByMatchingAuthenticationScheme() - { - var handler = await TestHandler.Create("Alpha"); - var passiveNoMatch = handler.ShouldHandleScheme("Beta", handleAutomatic: false); - - handler = await TestHandler.Create("Alpha"); - var passiveWithMatch = handler.ShouldHandleScheme("Alpha", handleAutomatic: false); - - Assert.False(passiveNoMatch); - Assert.True(passiveWithMatch); - } - - [Fact] - public async Task AutomaticHandlerInAutomaticModeHandlesEmptyChallenges() - { - var handler = await TestAutoHandler.Create("ignored", true); - Assert.True(handler.ShouldHandleScheme(AuthenticationManager.AutomaticScheme, handleAutomatic: true)); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - [InlineData("notmatched")] - public async Task AutomaticHandlerDoesNotHandleSchemes(string scheme) - { - var handler = await TestAutoHandler.Create("ignored", true); - Assert.False(handler.ShouldHandleScheme(scheme, handleAutomatic: true)); - } - - [Fact] - public async Task AutomaticHandlerShouldHandleSchemeWhenSchemeMatches() - { - var handler = await TestAutoHandler.Create("Alpha", true); - Assert.True(handler.ShouldHandleScheme("Alpha", handleAutomatic: true)); - } - - [Fact] - public async Task AutomaticHandlerShouldNotHandleChallengeWhenSchemesNotEmpty() - { - var handler = await TestAutoHandler.Create(null, true); - Assert.False(handler.ShouldHandleScheme("Alpha", handleAutomatic: true)); - } - - [Theory] - [InlineData("Alpha")] - [InlineData("Automatic")] - public async Task AuthHandlerAuthenticateCachesTicket(string scheme) - { - var handler = await CountHandler.Create(scheme); - var context = new AuthenticateContext(scheme); - await handler.AuthenticateAsync(context); - await handler.AuthenticateAsync(context); - Assert.Equal(1, handler.AuthCount); - } - - private class CountOptions : AuthenticationOptions { } - - private class CountHandler : AuthenticationHandler - { - public int AuthCount = 0; - - private CountHandler() { } - - public static async Task Create(string scheme) - { - var handler = new CountHandler(); - var context = new DefaultHttpContext(); - context.Features.Set(new TestResponse()); - await handler.InitializeAsync( - new CountOptions(), context, - new LoggerFactory().CreateLogger("CountHandler"), - UrlEncoder.Default); - handler.Options.AuthenticationScheme = scheme; - handler.Options.AutomaticAuthenticate = true; - return handler; - } - - protected override Task HandleAuthenticateAsync() - { - AuthCount++; - return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), new AuthenticationProperties(), "whatever"))); - } - - } - - private class TestHandler : AuthenticationHandler - { - private TestHandler() { } - - public static async Task Create(string scheme) - { - var handler = new TestHandler(); - var context = new DefaultHttpContext(); - context.Features.Set(new TestResponse()); - await handler.InitializeAsync( - new TestOptions(), context, - new LoggerFactory().CreateLogger("TestHandler"), - UrlEncoder.Default); - handler.Options.AuthenticationScheme = scheme; - return handler; - } - - protected override Task HandleAuthenticateAsync() - { - return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), new AuthenticationProperties(), "whatever"))); - } - } - - private class TestOptions : AuthenticationOptions { } - - private class TestAutoOptions : AuthenticationOptions - { - public TestAutoOptions() - { - AutomaticAuthenticate = true; - } - } - - private class TestAutoHandler : AuthenticationHandler - { - private TestAutoHandler() { } - - public static async Task Create(string scheme, bool auto) - { - var handler = new TestAutoHandler(); - var context = new DefaultHttpContext(); - context.Features.Set(new TestResponse()); - await handler.InitializeAsync( - new TestAutoOptions(), context, - new LoggerFactory().CreateLogger("TestAutoHandler"), - UrlEncoder.Default); - handler.Options.AuthenticationScheme = scheme; - handler.Options.AutomaticAuthenticate = auto; - return handler; - } - - protected override Task HandleAuthenticateAsync() - { - return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), new AuthenticationProperties(), "whatever"))); - } - } - - private class TestResponse : IHttpResponseFeature - { - public Stream Body - { - get - { - throw new NotImplementedException(); - } - - set - { - throw new NotImplementedException(); - } - } - - public bool HasStarted - { - get - { - throw new NotImplementedException(); - } - } - - public IHeaderDictionary Headers - { - get - { - throw new NotImplementedException(); - } - - set - { - throw new NotImplementedException(); - } - } - - public string ReasonPhrase - { - get - { - throw new NotImplementedException(); - } - - set - { - throw new NotImplementedException(); - } - } - - public int StatusCode - { - get - { - throw new NotImplementedException(); - } - - set - { - throw new NotImplementedException(); - } - } - - public void OnCompleted(Func callback, object state) - { - throw new NotImplementedException(); - } - - public void OnStarting(Func callback, object state) - { - } - } - } -} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/AuthenticationMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/AuthenticationMiddlewareTests.cs new file mode 100644 index 000000000..b09f13cab --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/AuthenticationMiddlewareTests.cs @@ -0,0 +1,196 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public class AuthenticationMiddlewareTests + { + [Fact] + public async Task OnlyInvokesCanHandleRequestHandlers() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + }) + .ConfigureServices(services => services.AddAuthentication(o => + { + o.AddScheme("Skip", s => + { + s.HandlerType = typeof(SkipHandler); + }); + // Won't get hit since CanHandleRequests is false + o.AddScheme("throws", s => + { + s.HandlerType = typeof(ThrowsHandler); + }); + o.AddScheme("607", s => + { + s.HandlerType = typeof(SixOhSevenHandler); + }); + // Won't get run since 607 will finish + o.AddScheme("305", s => + { + s.HandlerType = typeof(ThreeOhFiveHandler); + }); + })); + var server = new TestServer(builder); + var response = await server.CreateClient().GetAsync("http://example.com/"); + Assert.Equal(607, (int)response.StatusCode); + } + + private class ThreeOhFiveHandler : StatusCodeHandler { + public ThreeOhFiveHandler() : base(305) { } + } + + private class SixOhSevenHandler : StatusCodeHandler + { + public SixOhSevenHandler() : base(607) { } + } + + private class SevenOhSevenHandler : StatusCodeHandler + { + public SevenOhSevenHandler() : base(707) { } + } + + private class StatusCodeHandler : IAuthenticationRequestHandler + { + private HttpContext _context; + private int _code; + + public StatusCodeHandler(int code) + { + _code = code; + } + + public Task AuthenticateAsync() + { + throw new NotImplementedException(); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task HandleRequestAsync() + { + _context.Response.StatusCode = _code; + return Task.FromResult(true); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + _context = context; + return Task.FromResult(0); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + } + + private class ThrowsHandler : IAuthenticationHandler + { + private HttpContext _context; + + public Task AuthenticateAsync() + { + throw new NotImplementedException(); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task HandleRequestAsync() + { + throw new NotImplementedException(); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + _context = context; + return Task.FromResult(0); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + } + + private class SkipHandler : IAuthenticationRequestHandler + { + private HttpContext _context; + + public Task AuthenticateAsync() + { + throw new NotImplementedException(); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task HandleRequestAsync() + { + return Task.FromResult(false); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + _context = context; + return Task.FromResult(0); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + } + + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/DataHandler/Base64UrlTextEncoderTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/Base64UrlTextEncoderTests.cs similarity index 100% rename from test/Microsoft.AspNetCore.Authentication.Test/DataHandler/Base64UrlTextEncoderTests.cs rename to test/Microsoft.AspNetCore.Authentication.Test/Base64UrlTextEncoderTests.cs diff --git a/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs new file mode 100644 index 000000000..b083e9d76 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs @@ -0,0 +1,112 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication +{ + public class ClaimActionTests + { + [Fact] + public void CanMapSingleValueUserDataToClaim() + { + var userData = new JObject + { + ["name"] = "test" + }; + + var identity = new ClaimsIdentity(); + + var action = new JsonKeyClaimAction("name", "name", "name"); + action.Run(userData, identity, "iss"); + + Assert.Equal("name", identity.FindFirst("name").Type); + Assert.Equal("test", identity.FindFirst("name").Value); + } + + [Fact] + public void CanMapArrayValueUserDataToClaims() + { + var userData = new JObject + { + ["role"] = new JArray { "role1", "role2" } + }; + + var identity = new ClaimsIdentity(); + + var action = new JsonKeyClaimAction("role", "role", "role"); + action.Run(userData, identity, "iss"); + + var roleClaims = identity.FindAll("role").ToList(); + Assert.Equal(2, roleClaims.Count); + Assert.Equal("role", roleClaims[0].Type); + Assert.Equal("role1", roleClaims[0].Value); + Assert.Equal("role", roleClaims[1].Type); + Assert.Equal("role2", roleClaims[1].Value); + } + + [Fact] + public void MapAllSucceeds() + { + var userData = new JObject + { + ["name0"] = "value0", + ["name1"] = "value1", + }; + + var identity = new ClaimsIdentity(); + var action = new MapAllClaimsAction(); + action.Run(userData, identity, "iss"); + + Assert.Equal("name0", identity.FindFirst("name0").Type); + Assert.Equal("value0", identity.FindFirst("name0").Value); + Assert.Equal("name1", identity.FindFirst("name1").Type); + Assert.Equal("value1", identity.FindFirst("name1").Value); + } + + [Fact] + public void MapAllAllowesDulicateKeysWithUniqueValues() + { + var userData = new JObject + { + ["name0"] = "value0", + ["name1"] = "value1", + }; + + var identity = new ClaimsIdentity(); + identity.AddClaim(new Claim("name0", "value2")); + identity.AddClaim(new Claim("name1", "value3")); + var action = new MapAllClaimsAction(); + action.Run(userData, identity, "iss"); + + Assert.Equal(2, identity.FindAll("name0").Count()); + Assert.Equal(2, identity.FindAll("name1").Count()); + } + + [Fact] + public void MapAllSkipsDuplicateValues() + { + var userData = new JObject + { + ["name0"] = "value0", + ["name1"] = "value1", + }; + + var identity = new ClaimsIdentity(); + identity.AddClaim(new Claim("name0", "value0")); + identity.AddClaim(new Claim("name1", "value1")); + var action = new MapAllClaimsAction(); + action.Run(userData, identity, "iss"); + + Assert.Single(identity.FindAll("name0")); + Assert.Single(identity.FindAll("name1")); + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/Cookies/CookieMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/CookieTests.cs similarity index 54% rename from test/Microsoft.AspNetCore.Authentication.Test/Cookies/CookieMiddlewareTests.cs rename to test/Microsoft.AspNetCore.Authentication.Test/CookieTests.cs index 5049f39e5..107fc5db1 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/Cookies/CookieMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/CookieTests.cs @@ -14,20 +14,29 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Microsoft.AspNetCore.Authentication.Cookies { - public class CookieMiddlewareTests + public class CookieTests : SharedAuthenticationTests { + private TestClock _clock = new TestClock(); + + protected override string DefaultScheme => CookieAuthenticationDefaults.AuthenticationScheme; + protected override Type HandlerType => typeof(CookieAuthenticationHandler); + + protected override void RegisterAuth(AuthenticationBuilder services, Action configure) + { + services.AddCookie(configure); + } + [Fact] public async Task NormalRequestPassesThrough() { - var server = CreateServer(new CookieAuthenticationOptions()); + var server = CreateServer(s => { }); var response = await server.CreateClient().GetAsync("http://example.com/normal"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } @@ -35,91 +44,51 @@ public async Task NormalRequestPassesThrough() [Fact] public async Task AjaxLoginRedirectToReturnUrlTurnsInto200WithLocationHeader() { - var server = CreateServer(new CookieAuthenticationOptions - { - AutomaticChallenge = true, - LoginPath = "/login" - }); - - var transaction = await SendAsync(server, "http://example.com/protected?X-Requested-With=XMLHttpRequest"); + var server = CreateServer(o => o.LoginPath = "/login"); + var transaction = await SendAsync(server, "http://example.com/challenge?X-Requested-With=XMLHttpRequest"); Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode); var responded = transaction.Response.Headers.GetValues("Location"); - Assert.Equal(1, responded.Count()); - Assert.True(responded.Single().StartsWith("http://example.com/login")); + Assert.Single(responded); + Assert.StartsWith("http://example.com/login", responded.Single()); } [Fact] public async Task AjaxForbidTurnsInto403WithLocationHeader() { - var server = CreateServer(new CookieAuthenticationOptions - { - AccessDeniedPath = "/denied" - }); - + var server = CreateServer(o => o.AccessDeniedPath = "/denied"); var transaction = await SendAsync(server, "http://example.com/forbid?X-Requested-With=XMLHttpRequest"); Assert.Equal(HttpStatusCode.Forbidden, transaction.Response.StatusCode); var responded = transaction.Response.Headers.GetValues("Location"); - Assert.Equal(1, responded.Count()); - Assert.True(responded.Single().StartsWith("http://example.com/denied")); + Assert.Single(responded); + Assert.StartsWith("http://example.com/denied", responded.Single()); } [Fact] public async Task AjaxLogoutRedirectToReturnUrlTurnsInto200WithLocationHeader() { - var server = CreateServer(new CookieAuthenticationOptions - { - LogoutPath = "/signout" - }); - + var server = CreateServer(o => o.LogoutPath = "/signout"); var transaction = await SendAsync(server, "http://example.com/signout?X-Requested-With=XMLHttpRequest&ReturnUrl=/"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); var responded = transaction.Response.Headers.GetValues("Location"); - Assert.Equal(1, responded.Count()); - Assert.True(responded.Single().StartsWith("/")); + Assert.Single(responded); + Assert.StartsWith("/", responded.Single()); } [Fact] public async Task AjaxChallengeRedirectTurnsInto200WithLocationHeader() { - var server = CreateServer(new CookieAuthenticationOptions()); - + var server = CreateServer(s => { }); var transaction = await SendAsync(server, "http://example.com/challenge?X-Requested-With=XMLHttpRequest&ReturnUrl=/"); Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode); var responded = transaction.Response.Headers.GetValues("Location"); - Assert.Equal(1, responded.Count()); - Assert.True(responded.Single().StartsWith("http://example.com/Account/Login")); - } - - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ProtectedRequestShouldRedirectToLoginOnlyWhenAutomatic(bool auto) - { - var server = CreateServer(new CookieAuthenticationOptions - { - LoginPath = new PathString("/login"), - AutomaticChallenge = auto - }); - - var transaction = await SendAsync(server, "http://example.com/protected"); - - Assert.Equal(auto ? HttpStatusCode.Redirect : HttpStatusCode.Unauthorized, transaction.Response.StatusCode); - if (auto) - { - var location = transaction.Response.Headers.Location; - Assert.Equal("/login", location.LocalPath); - Assert.Equal("?ReturnUrl=%2Fprotected", location.Query); - } + Assert.Single(responded); + Assert.StartsWith("http://example.com/Account/Login", responded.Single()); } [Fact] public async Task ProtectedCustomRequestShouldRedirectToCustomRedirectUri() { - var server = CreateServer(new CookieAuthenticationOptions - { - AutomaticChallenge = true - }); + var server = CreateServer(s => { }); var transaction = await SendAsync(server, "http://example.com/protected/CustomRedirect"); @@ -130,31 +99,33 @@ public async Task ProtectedCustomRequestShouldRedirectToCustomRedirectUri() private Task SignInAsAlice(HttpContext context) { - return context.Authentication.SignInAsync("Cookies", - new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), + var user = new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")); + user.AddClaim(new Claim("marker", "true")); + return context.SignInAsync("Cookies", + new ClaimsPrincipal(user), new AuthenticationProperties()); } private Task SignInAsWrong(HttpContext context) { - return context.Authentication.SignInAsync("Oops", + return context.SignInAsync("Oops", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), new AuthenticationProperties()); } private Task SignOutAsWrong(HttpContext context) { - return context.Authentication.SignOutAsync("Oops"); + return context.SignOutAsync("Oops"); } [Fact] public async Task SignInCausesDefaultCookieToBeCreated() { - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServerWithServices(s => s.AddAuthentication().AddCookie(o => { - LoginPath = new PathString("/login"), - CookieName = "TestCookie" - }, SignInAsAlice); + o.LoginPath = new PathString("/login"); + o.Cookie.Name = "TestCookie"; + }), SignInAsAlice); var transaction = await SendAsync(server, "http://example.com/testpath"); @@ -162,18 +133,36 @@ public async Task SignInCausesDefaultCookieToBeCreated() Assert.StartsWith("TestCookie=", setCookie); Assert.Contains("; path=/", setCookie); Assert.Contains("; httponly", setCookie); + Assert.Contains("; samesite=", setCookie); Assert.DoesNotContain("; expires=", setCookie); Assert.DoesNotContain("; domain=", setCookie); Assert.DoesNotContain("; secure", setCookie); } + [Fact] + public async Task CookieExpirationOptionIsIgnored() + { + var server = CreateServerWithServices(s => s.AddAuthentication().AddCookie(o => + { + o.Cookie.Name = "TestCookie"; + // this is currently ignored. Users should set o.ExpireTimeSpan instead + o.Cookie.Expiration = TimeSpan.FromDays(10); + }), SignInAsAlice); + + var transaction = await SendAsync(server, "http://example.com/testpath"); + + var setCookie = transaction.SetCookie; + Assert.StartsWith("TestCookie=", setCookie); + Assert.DoesNotContain("; expires=", setCookie); + } + [Fact] public async Task SignInWrongAuthTypeThrows() { - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - LoginPath = new PathString("/login"), - CookieName = "TestCookie" + o.LoginPath = new PathString("/login"); + o.Cookie.Name = "TestCookie"; }, SignInAsWrong); await Assert.ThrowsAsync(async () => await SendAsync(server, "http://example.com/testpath")); @@ -182,32 +171,32 @@ public async Task SignInWrongAuthTypeThrows() [Fact] public async Task SignOutWrongAuthTypeThrows() { - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - LoginPath = new PathString("/login"), - CookieName = "TestCookie" + o.LoginPath = new PathString("/login"); + o.Cookie.Name = "TestCookie"; }, SignOutAsWrong); await Assert.ThrowsAsync(async () => await SendAsync(server, "http://example.com/testpath")); } [Theory] - [InlineData(CookieSecureOption.Always, "http://example.com/testpath", true)] - [InlineData(CookieSecureOption.Always, "https://example.com/testpath", true)] - [InlineData(CookieSecureOption.Never, "http://example.com/testpath", false)] - [InlineData(CookieSecureOption.Never, "https://example.com/testpath", false)] - [InlineData(CookieSecureOption.SameAsRequest, "http://example.com/testpath", false)] - [InlineData(CookieSecureOption.SameAsRequest, "https://example.com/testpath", true)] + [InlineData(CookieSecurePolicy.Always, "http://example.com/testpath", true)] + [InlineData(CookieSecurePolicy.Always, "https://example.com/testpath", true)] + [InlineData(CookieSecurePolicy.None, "http://example.com/testpath", false)] + [InlineData(CookieSecurePolicy.None, "https://example.com/testpath", false)] + [InlineData(CookieSecurePolicy.SameAsRequest, "http://example.com/testpath", false)] + [InlineData(CookieSecurePolicy.SameAsRequest, "https://example.com/testpath", true)] public async Task SecureSignInCausesSecureOnlyCookieByDefault( - CookieSecureOption cookieSecureOption, + CookieSecurePolicy cookieSecurePolicy, string requestUri, bool shouldBeSecureOnly) { - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - LoginPath = new PathString("/login"), - CookieName = "TestCookie", - CookieSecure = cookieSecureOption + o.LoginPath = new PathString("/login"); + o.Cookie.Name = "TestCookie"; + o.Cookie.SecurePolicy = cookieSecurePolicy; }, SignInAsAlice); var transaction = await SendAsync(server, requestUri); @@ -226,14 +215,15 @@ public async Task SecureSignInCausesSecureOnlyCookieByDefault( [Fact] public async Task CookieOptionsAlterSetCookieHeader() { - TestServer server1 = CreateServer(new CookieAuthenticationOptions + var server1 = CreateServer(o => { - CookieName = "TestCookie", - CookiePath = "/foo", - CookieDomain = "another.com", - CookieSecure = CookieSecureOption.Always, - CookieHttpOnly = true - }, SignInAsAlice, new Uri("http://example.com/base")); + o.Cookie.Name = "TestCookie"; + o.Cookie.Path = "/foo"; + o.Cookie.Domain = "another.com"; + o.Cookie.SecurePolicy = CookieSecurePolicy.Always; + o.Cookie.SameSite = SameSiteMode.None; + o.Cookie.HttpOnly = true; + }, SignInAsAlice, baseAddress: new Uri("http://example.com/base")); var transaction1 = await SendAsync(server1, "http://example.com/base/testpath"); @@ -243,14 +233,16 @@ public async Task CookieOptionsAlterSetCookieHeader() Assert.Contains(" path=/foo", setCookie1); Assert.Contains(" domain=another.com", setCookie1); Assert.Contains(" secure", setCookie1); + Assert.DoesNotContain(" samesite", setCookie1); Assert.Contains(" httponly", setCookie1); - var server2 = CreateServer(new CookieAuthenticationOptions + var server2 = CreateServer(o => { - CookieName = "SecondCookie", - CookieSecure = CookieSecureOption.Never, - CookieHttpOnly = false - }, SignInAsAlice, new Uri("http://example.com/base")); + o.Cookie.Name = "SecondCookie"; + o.Cookie.SecurePolicy = CookieSecurePolicy.None; + o.Cookie.SameSite = SameSiteMode.Strict; + o.Cookie.HttpOnly = false; + }, SignInAsAlice, baseAddress: new Uri("http://example.com/base")); var transaction2 = await SendAsync(server2, "http://example.com/base/testpath"); @@ -258,6 +250,7 @@ public async Task CookieOptionsAlterSetCookieHeader() Assert.Contains("SecondCookie=", setCookie2); Assert.Contains(" path=/base", setCookie2); + Assert.Contains(" samesite=strict", setCookie2); Assert.DoesNotContain(" domain=", setCookie2); Assert.DoesNotContain(" secure", setCookie2); Assert.DoesNotContain(" httponly", setCookie2); @@ -266,11 +259,7 @@ public async Task CookieOptionsAlterSetCookieHeader() [Fact] public async Task CookieContainsIdentity() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions - { - SystemClock = clock - }, SignInAsAlice); + var server = CreateServer(o => { }, SignInAsAlice); var transaction1 = await SendAsync(server, "http://example.com/testpath"); @@ -282,30 +271,10 @@ public async Task CookieContainsIdentity() [Fact] public async Task CookieAppliesClaimsTransform() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions - { - SystemClock = clock - }, + var server = CreateServer(o => { }, SignInAsAlice, baseAddress: null, - claimsTransform: new ClaimsTransformationOptions - { - Transformer = new ClaimsTransformer - { - OnTransform = context => - { - if (!context.Principal.Identities.Any(i => i.AuthenticationType == "xform")) - { - // REVIEW: Xform runs twice, once on Authenticate, and then once from the middleware - var id = new ClaimsIdentity("xform"); - id.AddClaim(new Claim("xform", "yup")); - context.Principal.AddIdentity(id); - } - return Task.FromResult(context.Principal); - } - } - }); + claimsTransform: true); var transaction1 = await SendAsync(server, "http://example.com/testpath"); @@ -319,23 +288,21 @@ public async Task CookieAppliesClaimsTransform() [Fact] public async Task CookieStopsWorkingAfterExpiration() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - SystemClock = clock, - ExpireTimeSpan = TimeSpan.FromMinutes(10), - SlidingExpiration = false + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; }, SignInAsAlice); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); - clock.Add(TimeSpan.FromMinutes(7)); + _clock.Add(TimeSpan.FromMinutes(7)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); - clock.Add(TimeSpan.FromMinutes(7)); + _clock.Add(TimeSpan.FromMinutes(7)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); @@ -350,27 +317,25 @@ public async Task CookieStopsWorkingAfterExpiration() [Fact] public async Task CookieExpirationCanBeOverridenInSignin() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - SystemClock = clock, - ExpireTimeSpan = TimeSpan.FromMinutes(10), - SlidingExpiration = false + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; }, context => - context.Authentication.SignInAsync("Cookies", + context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), - new AuthenticationProperties() { ExpiresUtc = clock.UtcNow.Add(TimeSpan.FromMinutes(5)) })); + new AuthenticationProperties() { ExpiresUtc = _clock.UtcNow.Add(TimeSpan.FromMinutes(5)) })); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); - clock.Add(TimeSpan.FromMinutes(3)); + _clock.Add(TimeSpan.FromMinutes(3)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); - clock.Add(TimeSpan.FromMinutes(3)); + _clock.Add(TimeSpan.FromMinutes(3)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); @@ -385,27 +350,25 @@ public async Task CookieExpirationCanBeOverridenInSignin() [Fact] public async Task ExpiredCookieWithValidatorStillExpired() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - SystemClock = clock, - ExpireTimeSpan = TimeSpan.FromMinutes(10), - Events = new CookieAuthenticationEvents + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.ShouldRenew = true; return Task.FromResult(0); } - } + }; }, context => - context.Authentication.SignInAsync("Cookies", + context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); var transaction1 = await SendAsync(server, "http://example.com/testpath"); - clock.Add(TimeSpan.FromMinutes(11)); + _clock.Add(TimeSpan.FromMinutes(11)); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction2.SetCookie); @@ -415,24 +378,22 @@ public async Task ExpiredCookieWithValidatorStillExpired() [Fact] public async Task CookieCanBeRejectedAndSignedOutByValidator() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - SystemClock = clock, - ExpireTimeSpan = TimeSpan.FromMinutes(10), - SlidingExpiration = false, - Events = new CookieAuthenticationEvents + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; + o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.RejectPrincipal(); - ctx.HttpContext.Authentication.SignOutAsync("Cookies"); + ctx.HttpContext.SignOutAsync("Cookies"); return Task.FromResult(0); } - } + }; }, context => - context.Authentication.SignInAsync("Cookies", + context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); var transaction1 = await SendAsync(server, "http://example.com/testpath"); @@ -442,26 +403,60 @@ public async Task CookieCanBeRejectedAndSignedOutByValidator() Assert.Null(FindClaimValue(transaction2, ClaimTypes.Name)); } + [Fact] + public async Task CookieNotRenewedAfterSignOut() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + ctx.ShouldRenew = true; + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + // renews on every request + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction2.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + + var transaction3 = await server.SendAsync("http://example.com/normal", transaction1.CookieNameValue); + Assert.NotNull(transaction3.SetCookie[0]); + + // signout wins over renew + var transaction4 = await server.SendAsync("http://example.com/signout", transaction3.SetCookie[0]); + Assert.Single(transaction4.SetCookie); + Assert.Contains(".AspNetCore.Cookies=; expires=", transaction4.SetCookie[0]); + } + [Fact] public async Task CookieCanBeRenewedByValidator() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - SystemClock = clock, - ExpireTimeSpan = TimeSpan.FromMinutes(10), - SlidingExpiration = false, - Events = new CookieAuthenticationEvents + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; + o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.ShouldRenew = true; return Task.FromResult(0); } - } + }; }, context => - context.Authentication.SignInAsync("Cookies", + context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); var transaction1 = await SendAsync(server, "http://example.com/testpath"); @@ -470,44 +465,116 @@ public async Task CookieCanBeRenewedByValidator() Assert.NotNull(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(5)); + _clock.Add(TimeSpan.FromMinutes(5)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.NotNull(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(6)); + _clock.Add(TimeSpan.FromMinutes(6)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction4.SetCookie); Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(5)); + _clock.Add(TimeSpan.FromMinutes(5)); var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.Null(transaction5.SetCookie); Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name)); } + [Fact] + public async Task CookieCanBeReplacedByValidator() + { + var server = CreateServer(o => + { + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + ctx.ShouldRenew = true; + ctx.ReplacePrincipal(new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice2", "Cookies2")))); + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction2.SetCookie); + Assert.Equal("Alice2", FindClaimValue(transaction2, ClaimTypes.Name)); + } + + [Fact] + public async Task CookieCanBeUpdatedByValidatorDuringRefresh() + { + var replace = false; + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + if (replace) + { + ctx.ShouldRenew = true; + ctx.ReplacePrincipal(new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice2", "Cookies2")))); + ctx.Properties.Items["updated"] = "yes"; + } + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + Assert.Null(FindPropertiesValue(transaction3, "updated")); + + replace = true; + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction4.SetCookie); + Assert.Equal("Alice2", FindClaimValue(transaction4, ClaimTypes.Name)); + Assert.Equal("yes", FindPropertiesValue(transaction4, "updated")); + + replace = false; + + var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); + Assert.Equal("Alice2", FindClaimValue(transaction5, ClaimTypes.Name)); + Assert.Equal("yes", FindPropertiesValue(transaction4, "updated")); + } + [Fact] public async Task CookieCanBeRenewedByValidatorWithSlidingExpiry() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - SystemClock = clock, - ExpireTimeSpan = TimeSpan.FromMinutes(10), - Events = new CookieAuthenticationEvents + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.ShouldRenew = true; return Task.FromResult(0); } - } + }; }, context => - context.Authentication.SignInAsync("Cookies", + context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); var transaction1 = await SendAsync(server, "http://example.com/testpath"); @@ -516,45 +583,98 @@ public async Task CookieCanBeRenewedByValidatorWithSlidingExpiry() Assert.NotNull(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(5)); + _clock.Add(TimeSpan.FromMinutes(5)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.NotNull(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(6)); + _clock.Add(TimeSpan.FromMinutes(6)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue); Assert.NotNull(transaction4.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(11)); + _clock.Add(TimeSpan.FromMinutes(11)); var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); Assert.Null(transaction5.SetCookie); Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name)); } + [Fact] + public async Task CookieCanBeRenewedByValidatorWithModifiedProperties() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = ctx => + { + ctx.ShouldRenew = true; + var id = ctx.Principal.Identities.First(); + var claim = id.FindFirst("counter"); + if (claim == null) + { + id.AddClaim(new Claim("counter", "1")); + } + else + { + id.RemoveClaim(claim); + id.AddClaim(new Claim("counter", claim.Value + "1")); + } + return Task.FromResult(0); + } + }; + }, + context => + context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction2.SetCookie); + Assert.Equal("1", FindClaimValue(transaction2, "counter")); + + _clock.Add(TimeSpan.FromMinutes(5)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); + Assert.NotNull(transaction3.SetCookie); + Assert.Equal("11", FindClaimValue(transaction3, "counter")); + + _clock.Add(TimeSpan.FromMinutes(6)); + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue); + Assert.NotNull(transaction4.SetCookie); + Assert.Equal("111", FindClaimValue(transaction4, "counter")); + + _clock.Add(TimeSpan.FromMinutes(11)); + + var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); + Assert.Null(transaction5.SetCookie); + Assert.Null(FindClaimValue(transaction5, "counter")); + } + [Fact] public async Task CookieValidatorOnlyCalledOnce() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - SystemClock = clock, - ExpireTimeSpan = TimeSpan.FromMinutes(10), - SlidingExpiration = false, - Events = new CookieAuthenticationEvents + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; + o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.ShouldRenew = true; return Task.FromResult(0); } - } + }; }, context => - context.Authentication.SignInAsync("Cookies", + context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); var transaction1 = await SendAsync(server, "http://example.com/testpath"); @@ -563,19 +683,19 @@ public async Task CookieValidatorOnlyCalledOnce() Assert.NotNull(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(5)); + _clock.Add(TimeSpan.FromMinutes(5)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.NotNull(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(6)); + _clock.Add(TimeSpan.FromMinutes(6)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction4.SetCookie); Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(5)); + _clock.Add(TimeSpan.FromMinutes(5)); var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.Null(transaction5.SetCookie); @@ -587,15 +707,13 @@ public async Task CookieValidatorOnlyCalledOnce() [InlineData(false)] public async Task ShouldRenewUpdatesIssuedExpiredUtc(bool sliding) { - var clock = new TestClock(); DateTimeOffset? lastValidateIssuedDate = null; DateTimeOffset? lastExpiresDate = null; - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - SystemClock = clock, - ExpireTimeSpan = TimeSpan.FromMinutes(10), - SlidingExpiration = sliding, - Events = new CookieAuthenticationEvents + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = sliding; + o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { @@ -604,10 +722,10 @@ public async Task ShouldRenewUpdatesIssuedExpiredUtc(bool sliding) ctx.ShouldRenew = true; return Task.FromResult(0); } - } + }; }, context => - context.Authentication.SignInAsync("Cookies", + context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); var transaction1 = await SendAsync(server, "http://example.com/testpath"); @@ -622,13 +740,13 @@ public async Task ShouldRenewUpdatesIssuedExpiredUtc(bool sliding) var firstIssueDate = lastValidateIssuedDate; var firstExpiresDate = lastExpiresDate; - clock.Add(TimeSpan.FromMinutes(1)); + _clock.Add(TimeSpan.FromMinutes(1)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.NotNull(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(2)); + _clock.Add(TimeSpan.FromMinutes(2)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue); Assert.NotNull(transaction4.SetCookie); @@ -641,21 +759,20 @@ public async Task ShouldRenewUpdatesIssuedExpiredUtc(bool sliding) [Fact] public async Task CookieExpirationCanBeOverridenInEvent() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - SystemClock = clock, - ExpireTimeSpan = TimeSpan.FromMinutes(10), - SlidingExpiration = false, - Events = new CookieAuthenticationEvents() + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = false; + o.Events = new CookieAuthenticationEvents() { OnSigningIn = context => { - context.Properties.ExpiresUtc = clock.UtcNow.Add(TimeSpan.FromMinutes(5)); + context.Properties.ExpiresUtc = _clock.UtcNow.Add(TimeSpan.FromMinutes(5)); return Task.FromResult(0); } - } - }, SignInAsAlice); + }; + }, + SignInAsAlice); var transaction1 = await SendAsync(server, "http://example.com/testpath"); @@ -663,13 +780,13 @@ public async Task CookieExpirationCanBeOverridenInEvent() Assert.Null(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(3)); + _clock.Add(TimeSpan.FromMinutes(3)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(3)); + _clock.Add(TimeSpan.FromMinutes(3)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction4.SetCookie); @@ -679,13 +796,12 @@ public async Task CookieExpirationCanBeOverridenInEvent() [Fact] public async Task CookieIsRenewedWithSlidingExpiration() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - SystemClock = clock, - ExpireTimeSpan = TimeSpan.FromMinutes(10), - SlidingExpiration = true - }, SignInAsAlice); + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = true; + }, + SignInAsAlice); var transaction1 = await SendAsync(server, "http://example.com/testpath"); @@ -693,20 +809,20 @@ public async Task CookieIsRenewedWithSlidingExpiration() Assert.Null(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(4)); + _clock.Add(TimeSpan.FromMinutes(4)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(4)); + _clock.Add(TimeSpan.FromMinutes(4)); // transaction4 should arrive with a new SetCookie value var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.NotNull(transaction4.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); - clock.Add(TimeSpan.FromMinutes(4)); + _clock.Add(TimeSpan.FromMinutes(4)); var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); Assert.Null(transaction5.SetCookie); @@ -714,58 +830,70 @@ public async Task CookieIsRenewedWithSlidingExpiration() } [Fact] - public async Task CookieUsesPathBaseByDefault() + public async Task CookieIsRenewedWithSlidingExpirationWithoutTransformations() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions(), - context => + var server = CreateServer(o => { - Assert.Equal(new PathString("/base"), context.Request.PathBase); - return context.Authentication.SignInAsync("Cookies", - new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))); + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = true; + o.Events.OnValidatePrincipal = c => + { + // https://github.com/aspnet/Security/issues/1607 + // On sliding refresh the transformed principal should not be serialized into the cookie, only the original principal. + Assert.Single(c.Principal.Identities); + Assert.True(c.Principal.Identities.First().HasClaim("marker", "true")); + return Task.CompletedTask; + }; }, - new Uri("http://example.com/base")); + SignInAsAlice, + claimsTransform: true); - var transaction1 = await SendAsync(server, "http://example.com/base/testpath"); - Assert.True(transaction1.SetCookie.Contains("path=/base")); - } + var transaction1 = await SendAsync(server, "http://example.com/testpath"); - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task CookieTurnsChallengeIntoForbidWithCookie(bool automatic) - { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions - { - AutomaticAuthenticate = automatic, - SystemClock = clock - }, - SignInAsAlice); + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction2.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); - var transaction1 = await SendAsync(server, "http://example.com/testpath"); + _clock.Add(TimeSpan.FromMinutes(4)); - var url = "http://example.com/challenge"; - var transaction2 = await SendAsync(server, url, transaction1.CookieNameValue); + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction3.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); - Assert.Equal(HttpStatusCode.Redirect, transaction2.Response.StatusCode); - var location = transaction2.Response.Headers.Location; - Assert.Equal("/Account/AccessDenied", location.LocalPath); - Assert.Equal("?ReturnUrl=%2Fchallenge", location.Query); + _clock.Add(TimeSpan.FromMinutes(4)); + + // transaction4 should arrive with a new SetCookie value + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction4.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(4)); + + var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); + Assert.Null(transaction5.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction5, ClaimTypes.Name)); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task CookieChallengeRedirectsToLoginWithoutCookie(bool automatic) + [Fact] + public async Task CookieUsesPathBaseByDefault() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { }, + context => { - AutomaticAuthenticate = automatic, - SystemClock = clock + Assert.Equal(new PathString("/base"), context.Request.PathBase); + return context.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))); }, - SignInAsAlice); + new Uri("http://example.com/base")); + + var transaction1 = await SendAsync(server, "http://example.com/base/testpath"); + Assert.Contains("path=/base", transaction1.SetCookie); + } + + [Fact] + public async Task CookieChallengeRedirectsToLoginWithoutCookie() + { + var server = CreateServer(o => { }, SignInAsAlice); var url = "http://example.com/challenge"; var transaction = await SendAsync(server, url); @@ -775,18 +903,10 @@ public async Task CookieChallengeRedirectsToLoginWithoutCookie(bool automatic) Assert.Equal("/Account/Login", location.LocalPath); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task CookieForbidRedirectsWithoutCookie(bool automatic) + [Fact] + public async Task CookieForbidRedirectsWithoutCookie() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions - { - AutomaticAuthenticate = automatic, - SystemClock = clock - }, - SignInAsAlice); + var server = CreateServer(o => { }, SignInAsAlice); var url = "http://example.com/forbid"; var transaction = await SendAsync(server, url); @@ -796,35 +916,12 @@ public async Task CookieForbidRedirectsWithoutCookie(bool automatic) Assert.Equal("/Account/AccessDenied", location.LocalPath); } - [Fact] - public async Task CookieTurns401ToAccessDeniedWhenSetWithCookie() - { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions - { - SystemClock = clock, - AccessDeniedPath = new PathString("/accessdenied") - }, - SignInAsAlice); - - var transaction1 = await SendAsync(server, "http://example.com/testpath"); - - var transaction2 = await SendAsync(server, "http://example.com/challenge", transaction1.CookieNameValue); - - Assert.Equal(HttpStatusCode.Redirect, transaction2.Response.StatusCode); - - var location = transaction2.Response.Headers.Location; - Assert.Equal("/accessdenied", location.LocalPath); - } - [Fact] public async Task CookieChallengeRedirectsWithLoginPath() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - SystemClock = clock, - LoginPath = new PathString("/page") + o.LoginPath = new PathString("/page"); }); var transaction1 = await SendAsync(server, "http://example.com/testpath"); @@ -837,11 +934,9 @@ public async Task CookieChallengeRedirectsWithLoginPath() [Fact] public async Task CookieChallengeWithUnauthorizedRedirectsToLoginIfNotAuthenticated() { - var clock = new TestClock(); - var server = CreateServer(new CookieAuthenticationOptions + var server = CreateServer(o => { - SystemClock = clock, - LoginPath = new PathString("/page") + o.LoginPath = new PathString("/page"); }); var transaction1 = await SendAsync(server, "http://example.com/testpath"); @@ -851,19 +946,21 @@ public async Task CookieChallengeWithUnauthorizedRedirectsToLoginIfNotAuthentica Assert.Equal(HttpStatusCode.Redirect, transaction2.Response.StatusCode); } - [Fact] - public async Task MapWillNotAffectChallenge() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MapWillAffectChallengeOnlyWithUseAuth(bool useAuth) { var builder = new WebHostBuilder() .Configure(app => { - app.UseCookieAuthentication(new CookieAuthenticationOptions + if (useAuth) { - LoginPath = new PathString("/page") - }); - app.Map("/login", signoutApp => signoutApp.Run(context => context.Authentication.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" }))); + app.UseAuthentication(); + } + app.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" }))); }) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(s => s.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/page"))); var server = new TestServer(builder); var transaction = await server.SendAsync("http://example.com/login"); @@ -871,23 +968,30 @@ public async Task MapWillNotAffectChallenge() Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var location = transaction.Response.Headers.Location; - Assert.Equal("/page", location.LocalPath); + if (useAuth) + { + Assert.Equal("/page", location.LocalPath); + } + else + { + Assert.Equal("/login/page", location.LocalPath); + } Assert.Equal("?ReturnUrl=%2F", location.Query); } - [Fact] + [ConditionalFact(Skip = "Revisit, exception no longer thrown")] public async Task ChallengeDoesNotSet401OnUnauthorized() { var builder = new WebHostBuilder() .Configure(app => { - app.UseCookieAuthentication(); + app.UseAuthentication(); app.Run(async context => { - await Assert.ThrowsAsync(() => context.Authentication.ChallengeAsync()); + await Assert.ThrowsAsync(() => context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme)); }); }) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(services => services.AddAuthentication().AddCookie()); var server = new TestServer(builder); var transaction = await server.SendAsync("http://example.com"); @@ -895,25 +999,49 @@ public async Task ChallengeDoesNotSet401OnUnauthorized() } [Fact] - public async Task UseCookieWithInstanceDoesntUseSharedOptions() + public async Task CanConfigureDefaultCookieInstance() { var builder = new WebHostBuilder() .Configure(app => { - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - CookieName = "One" - }); - app.UseCookieAuthentication(); - app.Run(context => context.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity()))); + app.UseAuthentication(); + app.Run(context => context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity()))); + }) + .ConfigureServices(services => + { + services.AddAuthentication().AddCookie(); + services.Configure(CookieAuthenticationDefaults.AuthenticationScheme, + o => o.Cookie.Name = "One"); + }); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com"); + + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.StartsWith("One=", transaction.SetCookie[0]); + } + + [Fact] + public async Task CanConfigureNamedCookieInstance() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Run(context => context.SignInAsync("Cookie1", new ClaimsPrincipal(new ClaimsIdentity()))); }) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(services => + { + services.AddAuthentication().AddCookie("Cookie1"); + services.Configure("Cookie1", + o => o.Cookie.Name = "One"); + }); var server = new TestServer(builder); var transaction = await server.SendAsync("http://example.com"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - Assert.True(transaction.SetCookie[0].StartsWith(".AspNetCore.Cookies=")); + Assert.StartsWith("One=", transaction.SetCookie[0]); } [Fact] @@ -922,14 +1050,11 @@ public async Task MapWithSignInOnlyRedirectToReturnUrlOnLoginPath() var builder = new WebHostBuilder() .Configure(app => { - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - LoginPath = new PathString("/login") - }); - app.Map("/notlogin", signoutApp => signoutApp.Run(context => context.Authentication.SignInAsync("Cookies", + app.UseAuthentication(); + app.Map("/notlogin", signoutApp => signoutApp.Run(context => context.SignInAsync("Cookies", new ClaimsPrincipal()))); }) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/login"))); var server = new TestServer(builder); var transaction = await server.SendAsync("http://example.com/notlogin?ReturnUrl=%2Fpage"); @@ -943,14 +1068,11 @@ public async Task MapWillNotAffectSignInRedirectToReturnUrl() var builder = new WebHostBuilder() .Configure(app => { - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - LoginPath = new PathString("/login") - }); - app.Map("/login", signoutApp => signoutApp.Run(context => context.Authentication.SignInAsync("Cookies", - new ClaimsPrincipal()))); + app.UseAuthentication(); + app.Map("/login", signoutApp => signoutApp.Run(context => context.SignInAsync("Cookies", new ClaimsPrincipal()))); }) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/login"))); + var server = new TestServer(builder); var transaction = await server.SendAsync("http://example.com/login?ReturnUrl=%2Fpage"); @@ -968,13 +1090,10 @@ public async Task MapWithSignOutOnlyRedirectToReturnUrlOnLogoutPath() var builder = new WebHostBuilder() .Configure(app => { - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - LogoutPath = new PathString("/logout") - }); - app.Map("/notlogout", signoutApp => signoutApp.Run(context => context.Authentication.SignOutAsync("Cookies"))); + app.UseAuthentication(); + app.Map("/notlogout", signoutApp => signoutApp.Run(context => context.SignOutAsync("Cookies"))); }) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LogoutPath = new PathString("/logout"))); var server = new TestServer(builder); var transaction = await server.SendAsync("http://example.com/notlogout?ReturnUrl=%2Fpage"); @@ -988,13 +1107,10 @@ public async Task MapWillNotAffectSignOutRedirectToReturnUrl() var builder = new WebHostBuilder() .Configure(app => { - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - LogoutPath = new PathString("/logout") - }); - app.Map("/logout", signoutApp => signoutApp.Run(context => context.Authentication.SignOutAsync("Cookies"))); + app.UseAuthentication(); + app.Map("/logout", signoutApp => signoutApp.Run(context => context.SignOutAsync("Cookies"))); }) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LogoutPath = new PathString("/logout"))); var server = new TestServer(builder); var transaction = await server.SendAsync("http://example.com/logout?ReturnUrl=%2Fpage"); @@ -1012,13 +1128,10 @@ public async Task MapWillNotAffectAccessDenied() var builder = new WebHostBuilder() .Configure(app => { - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AccessDeniedPath = new PathString("/denied") - }); - app.Map("/forbid", signoutApp => signoutApp.Run(context => context.Authentication.ForbidAsync("Cookies"))); + app.UseAuthentication(); + app.Map("/forbid", signoutApp => signoutApp.Run(context => context.ForbidAsync("Cookies"))); }) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.AccessDeniedPath = new PathString("/denied"))); var server = new TestServer(builder); var transaction = await server.SendAsync("http://example.com/forbid"); @@ -1035,13 +1148,10 @@ public async Task NestedMapWillNotAffectLogin() .Configure(app => app.Map("/base", map => { - map.UseCookieAuthentication(new CookieAuthenticationOptions - { - LoginPath = new PathString("/page") - }); - map.Map("/login", signoutApp => signoutApp.Run(context => context.Authentication.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" }))); + map.UseAuthentication(); + map.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" }))); })) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/page"))); var server = new TestServer(builder); var transaction = await server.SendAsync("http://example.com/base/login"); @@ -1052,6 +1162,95 @@ public async Task NestedMapWillNotAffectLogin() Assert.Equal("?ReturnUrl=%2F", location.Query); } + [Theory] + [InlineData("/redirect_test")] + [InlineData("http://example.com/redirect_to")] + public async Task RedirectUriIsHoneredAfterSignin(string redirectUrl) + { + var server = CreateServer(o => + { + o.LoginPath = "/testpath"; + o.Cookie.Name = "TestCookie"; + }, + async context => + await context.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))), + new AuthenticationProperties { RedirectUri = redirectUrl }) + ); + var transaction = await SendAsync(server, "http://example.com/testpath"); + + Assert.NotEmpty(transaction.SetCookie); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal(redirectUrl, transaction.Response.Headers.Location.ToString()); + } + + [Fact] + public async Task RedirectUriInQueryIsHoneredAfterSignin() + { + var server = CreateServer(o => + { + o.LoginPath = "/testpath"; + o.ReturnUrlParameter = "return"; + o.Cookie.Name = "TestCookie"; + }, + async context => + { + await context.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme)))); + }); + var transaction = await SendAsync(server, "http://example.com/testpath?return=%2Fret_path_2"); + + Assert.NotEmpty(transaction.SetCookie); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/ret_path_2", transaction.Response.Headers.Location.ToString()); + } + + [Fact] + public async Task AbsoluteRedirectUriInQueryStringIsRejected() + { + var server = CreateServer(o => + { + o.LoginPath = "/testpath"; + o.ReturnUrlParameter = "return"; + o.Cookie.Name = "TestCookie"; + }, + async context => + { + await context.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme)))); + }); + var transaction = await SendAsync(server, "http://example.com/testpath?return=http%3A%2F%2Fexample.com%2Fredirect_to"); + + Assert.NotEmpty(transaction.SetCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task EnsurePrecedenceOfRedirectUriAfterSignin() + { + var server = CreateServer(o => + { + o.LoginPath = "/testpath"; + o.ReturnUrlParameter = "return"; + o.Cookie.Name = "TestCookie"; + }, + async context => + { + await context.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))), + new AuthenticationProperties { RedirectUri = "/redirect_test" }); + }); + var transaction = await SendAsync(server, "http://example.com/testpath?return=%2Fret_path_2"); + + Assert.NotEmpty(transaction.SetCookie); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/redirect_test", transaction.Response.Headers.Location.ToString()); + } + [Fact] public async Task NestedMapWillNotAffectAccessDenied() { @@ -1059,13 +1258,10 @@ public async Task NestedMapWillNotAffectAccessDenied() .Configure(app => app.Map("/base", map => { - map.UseCookieAuthentication(new CookieAuthenticationOptions - { - AccessDeniedPath = new PathString("/denied") - }); - map.Map("/forbid", signoutApp => signoutApp.Run(context => context.Authentication.ForbidAsync("Cookies"))); + map.UseAuthentication(); + map.Map("/forbid", signoutApp => signoutApp.Run(context => context.ForbidAsync("Cookies"))); })) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.AccessDeniedPath = new PathString("/denied"))); var server = new TestServer(builder); var transaction = await server.SendAsync("http://example.com/base/forbid"); @@ -1083,17 +1279,17 @@ public async Task CanSpecifyAndShareDataProtector() var builder1 = new WebHostBuilder() .Configure(app => { - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - TicketDataFormat = new TicketDataFormat(dp), - CookieName = "Cookie" - }); + app.UseAuthentication(); app.Use((context, next) => - context.Authentication.SignInAsync("Cookies", + context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), new AuthenticationProperties())); }) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => + { + o.TicketDataFormat = new TicketDataFormat(dp); + o.Cookie.Name = "Cookie"; + })); var server1 = new TestServer(builder1); var transaction = await SendAsync(server1, "http://example.com/stuff"); @@ -1102,25 +1298,68 @@ public async Task CanSpecifyAndShareDataProtector() var builder2 = new WebHostBuilder() .Configure(app => { - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AuthenticationScheme = "Cookies", - CookieName = "Cookie", - TicketDataFormat = new TicketDataFormat(dp) - }); + app.UseAuthentication(); app.Use(async (context, next) => { - var authContext = new AuthenticateContext("Cookies"); - await context.Authentication.AuthenticateAsync(authContext); - Describe(context.Response, authContext); + var result = await context.AuthenticateAsync("Cookies"); + Describe(context.Response, result); }); }) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(services => services.AddAuthentication().AddCookie("Cookies", o => + { + o.Cookie.Name = "Cookie"; + o.TicketDataFormat = new TicketDataFormat(dp); + })); var server2 = new TestServer(builder2); var transaction2 = await SendAsync(server2, "http://example.com/stuff", transaction.CookieNameValue); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); } + // Issue: https://github.com/aspnet/Security/issues/949 + [Fact] + public async Task NullExpiresUtcPropertyIsGuarded() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => + { + o.Events = new CookieAuthenticationEvents + { + OnValidatePrincipal = context => + { + context.Properties.ExpiresUtc = null; + context.ShouldRenew = true; + return Task.FromResult(0); + } + }; + })) + .Configure(app => + { + app.UseAuthentication(); + + app.Run(async context => + { + if (context.Request.Path == "/signin") + { + await context.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))); + } + else + { + await context.Response.WriteAsync("ha+1"); + } + }); + }); + + var server = new TestServer(builder); + + var cookie = (await server.SendAsync("http://www.example.com/signin")).SetCookie.FirstOrDefault(); + Assert.NotNull(cookie); + + var transaction = await server.SendAsync("http://www.example.com/", cookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + private class NoOpDataProtector : IDataProtector { public IDataProtector CreateProtector(string purpose) @@ -1149,6 +1388,16 @@ private static string FindClaimValue(Transaction transaction, string claimType) return claim.Attribute("value").Value; } + private static string FindPropertiesValue(Transaction transaction, string key) + { + var property = transaction.ResponseElement.Elements("extra").SingleOrDefault(elt => elt.Attribute("type").Value == key); + if (property == null) + { + return null; + } + return property.Attribute("value").Value; + } + private static async Task GetAuthData(TestServer server, string url, string cookie) { var request = new HttpRequestMessage(HttpMethod.Get, url); @@ -1160,18 +1409,44 @@ private static async Task GetAuthData(TestServer server, string url, s return me; } - private static TestServer CreateServer(CookieAuthenticationOptions options, Func testpath = null, Uri baseAddress = null, ClaimsTransformationOptions claimsTransform = null) + private class ClaimsTransformer : IClaimsTransformation + { + public Task TransformAsync(ClaimsPrincipal p) + { + var firstId = p.Identities.First(); + if (firstId.HasClaim("marker", "true")) + { + firstId.RemoveClaim(firstId.FindFirst("marker")); + } + // TransformAsync could be called twice on one request if you have a default scheme and also + // call AuthenticateAsync. + if (!p.Identities.Any(i => i.AuthenticationType == "xform")) + { + var id = new ClaimsIdentity("xform"); + id.AddClaim(new Claim("xform", "yup")); + p.AddIdentity(id); + } + return Task.FromResult(p); + } + } + + private TestServer CreateServer(Action configureOptions, Func testpath = null, Uri baseAddress = null, bool claimsTransform = false) + => CreateServerWithServices(s => + { + s.AddSingleton(_clock); + s.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(configureOptions); + if (claimsTransform) + { + s.AddSingleton(); + } + }, testpath, baseAddress); + + private static TestServer CreateServerWithServices(Action configureServices, Func testpath = null, Uri baseAddress = null) { var builder = new WebHostBuilder() .Configure(app => { - app.UseCookieAuthentication(options); - // app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationScheme = "Cookie2" }); - - if (claimsTransform != null) - { - app.UseClaimsTransformation(claimsTransform); - } + app.UseAuthentication(); app.Use(async (context, next) => { var req = context.Request; @@ -1181,70 +1456,72 @@ private static TestServer CreateServer(CookieAuthenticationOptions options, Func { res.StatusCode = 200; } - else if (req.Path == new PathString("/protected")) - { - res.StatusCode = 401; - } else if (req.Path == new PathString("/forbid")) // Simulate forbidden { - await context.Authentication.ForbidAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await context.ForbidAsync(CookieAuthenticationDefaults.AuthenticationScheme); } else if (req.Path == new PathString("/challenge")) { - await context.Authentication.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme); } else if (req.Path == new PathString("/signout")) { - await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } else if (req.Path == new PathString("/unauthorized")) { - await context.Authentication.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties(), ChallengeBehavior.Unauthorized); + await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties()); } else if (req.Path == new PathString("/protected/CustomRedirect")) { - await context.Authentication.ChallengeAsync(new AuthenticationProperties() { RedirectUri = "/CustomRedirect" }); + await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties() { RedirectUri = "/CustomRedirect" }); } else if (req.Path == new PathString("/me")) { - var authContext = new AuthenticateContext(CookieAuthenticationDefaults.AuthenticationScheme); - authContext.Authenticated(context.User, properties: null, description: null); - Describe(res, authContext); + Describe(res, AuthenticateResult.Success(new AuthenticationTicket(context.User, new AuthenticationProperties(), CookieAuthenticationDefaults.AuthenticationScheme))); } else if (req.Path.StartsWithSegments(new PathString("/me"), out remainder)) { - var authContext = new AuthenticateContext(remainder.Value.Substring(1)); - await context.Authentication.AuthenticateAsync(authContext); - Describe(res, authContext); + var ticket = await context.AuthenticateAsync(remainder.Value.Substring(1)); + Describe(res, ticket); } else if (req.Path == new PathString("/testpath") && testpath != null) { await testpath(context); } + else if (req.Path == new PathString("/checkforerrors")) + { + var result = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); // this used to be "Automatic" + if (result.Failure != null) + { + throw new Exception("Failed to authenticate", result.Failure); + } + return; + } else { await next(); } }); }) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(configureServices); var server = new TestServer(builder); server.BaseAddress = baseAddress; return server; } - private static void Describe(HttpResponse res, AuthenticateContext result) + private static void Describe(HttpResponse res, AuthenticateResult result) { res.StatusCode = 200; res.ContentType = "text/xml"; var xml = new XElement("xml"); - if (result != null && result.Principal != null) + if (result?.Ticket?.Principal != null) { - xml.Add(result.Principal.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value)))); + xml.Add(result.Ticket.Principal.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value)))); } - if (result != null && result.Properties != null) + if (result?.Ticket?.Properties != null) { - xml.Add(result.Properties.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value)))); + xml.Add(result.Ticket.Properties.Items.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value)))); } var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); res.Body.Write(xmlBytes, 0, xmlBytes.Length); diff --git a/test/Microsoft.AspNetCore.Authentication.Test/DynamicSchemeTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/DynamicSchemeTests.cs new file mode 100644 index 000000000..d658609b0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/DynamicSchemeTests.cs @@ -0,0 +1,170 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public class DynamicSchemeTests + { + [Fact] + public async Task OptionsAreConfiguredOnce() + { + var server = CreateServer(s => + { + s.Configure("One", o => o.Instance = new Singleton()); + s.Configure("Two", o => o.Instance = new Singleton()); + }); + // Add One scheme + var response = await server.CreateClient().GetAsync("http://example.com/add/One"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var transaction = await server.SendAsync("http://example.com/auth/One"); + Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One")); + Assert.Equal("1", transaction.FindClaimValue("Count")); + + // Verify option is not recreated + transaction = await server.SendAsync("http://example.com/auth/One"); + Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One")); + Assert.Equal("1", transaction.FindClaimValue("Count")); + + // Add Two scheme + response = await server.CreateClient().GetAsync("http://example.com/add/Two"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + transaction = await server.SendAsync("http://example.com/auth/Two"); + Assert.Equal("Two", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "Two")); + Assert.Equal("2", transaction.FindClaimValue("Count")); + + // Verify options are not recreated + transaction = await server.SendAsync("http://example.com/auth/One"); + Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One")); + Assert.Equal("1", transaction.FindClaimValue("Count")); + transaction = await server.SendAsync("http://example.com/auth/Two"); + Assert.Equal("Two", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "Two")); + Assert.Equal("2", transaction.FindClaimValue("Count")); + } + + [Fact] + public async Task CanAddAndRemoveSchemes() + { + var server = CreateServer(); + await Assert.ThrowsAsync(() => server.SendAsync("http://example.com/auth/One")); + + // Add One scheme + var response = await server.CreateClient().GetAsync("http://example.com/add/One"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var transaction = await server.SendAsync("http://example.com/auth/One"); + Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One")); + + // Add Two scheme + response = await server.CreateClient().GetAsync("http://example.com/add/Two"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + transaction = await server.SendAsync("http://example.com/auth/Two"); + Assert.Equal("Two", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "Two")); + + // Remove Two + response = await server.CreateClient().GetAsync("http://example.com/remove/Two"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + await Assert.ThrowsAsync(() => server.SendAsync("http://example.com/auth/Two")); + transaction = await server.SendAsync("http://example.com/auth/One"); + Assert.Equal("One", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "One")); + + // Remove One + response = await server.CreateClient().GetAsync("http://example.com/remove/One"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + await Assert.ThrowsAsync(() => server.SendAsync("http://example.com/auth/Two")); + await Assert.ThrowsAsync(() => server.SendAsync("http://example.com/auth/One")); + } + + public class TestOptions : AuthenticationSchemeOptions + { + public Singleton Instance { get; set; } + } + + public class Singleton + { + public static int _count; + + public Singleton() + { + _count++; + Count = _count; + } + + public int Count { get; } + } + + private class TestHandler : AuthenticationHandler + { + public TestHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { + } + + protected override Task HandleAuthenticateAsync() + { + var principal = new ClaimsPrincipal(); + var id = new ClaimsIdentity(); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name)); + if (Options.Instance != null) + { + id.AddClaim(new Claim("Count", Options.Instance.Count.ToString())); + } + principal.AddIdentity(id); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + } + + private static TestServer CreateServer(Action configureServices = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path.StartsWithSegments(new PathString("/add"), out var remainder)) + { + var name = remainder.Value.Substring(1); + var auth = context.RequestServices.GetRequiredService(); + var scheme = new AuthenticationScheme(name, name, typeof(TestHandler)); + auth.AddScheme(scheme); + } + else if (req.Path.StartsWithSegments(new PathString("/auth"), out remainder)) + { + var name = (remainder.Value.Length > 0) ? remainder.Value.Substring(1) : null; + var result = await context.AuthenticateAsync(name); + res.Describe(result?.Ticket?.Principal); + } + else if (req.Path.StartsWithSegments(new PathString("/remove"), out remainder)) + { + var name = remainder.Value.Substring(1); + var auth = context.RequestServices.GetRequiredService(); + auth.RemoveScheme(name); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + configureServices?.Invoke(services); + services.AddAuthentication(); + }); + return new TestServer(builder); + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/Facebook/FacebookMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/Facebook/FacebookMiddlewareTests.cs deleted file mode 100644 index eaa339d15..000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/Facebook/FacebookMiddlewareTests.cs +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.OAuth; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using Xunit; - -namespace Microsoft.AspNetCore.Authentication.Facebook -{ - public class FacebookMiddlewareTests - { - [Fact] - public async Task ChallengeWillTriggerApplyRedirectEvent() - { - var server = CreateServer( - app => - { - app.UseFacebookAuthentication(new FacebookOptions - { - AppId = "Test App Id", - AppSecret = "Test App Secret", - Events = new OAuthEvents - { - OnRedirectToAuthorizationEndpoint = context => - { - context.Response.Redirect(context.RedirectUri + "&custom=test"); - return Task.FromResult(0); - } - } - }); - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AuthenticationScheme = "External", - AutomaticAuthenticate = true - }); - }, - services => - { - services.AddAuthentication(options => - { - options.SignInScheme = "External"; - }); - }, - context => - { - // REVIEW: Gross. - context.Authentication.ChallengeAsync("Facebook").GetAwaiter().GetResult(); - return true; - }); - var transaction = await server.SendAsync("http://example.com/challenge"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var query = transaction.Response.Headers.Location.Query; - Assert.Contains("custom=test", query); - } - - [Fact] - public async Task NestedMapWillNotAffectRedirect() - { - var server = CreateServer(app => - app.Map("/base", map => { - map.UseFacebookAuthentication(new FacebookOptions - { - AppId = "Test App Id", - AppSecret = "Test App Secret", - SignInScheme = "External" - }); - map.Map("/login", signoutApp => signoutApp.Run(context => context.Authentication.ChallengeAsync("Facebook", new AuthenticationProperties() { RedirectUri = "/" }))); - }), - services => services.AddAuthentication(), - handler: null); - var transaction = await server.SendAsync("http://example.com/base/login"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var location = transaction.Response.Headers.Location.AbsoluteUri; - Assert.Contains("https://www.facebook.com/v2.5/dialog/oauth", location); - Assert.Contains("response_type=code", location); - Assert.Contains("client_id=", location); - Assert.Contains("redirect_uri=" + UrlEncoder.Default.Encode("http://example.com/base/signin-facebook"), location); - Assert.Contains("scope=", location); - Assert.Contains("state=", location); - } - - [Fact] - public async Task MapWillNotAffectRedirect() - { - var server = CreateServer( - app => - { - app.UseFacebookAuthentication(new FacebookOptions - { - AppId = "Test App Id", - AppSecret = "Test App Secret", - SignInScheme = "External" - }); - app.Map("/login", signoutApp => signoutApp.Run(context => context.Authentication.ChallengeAsync("Facebook", new AuthenticationProperties() { RedirectUri = "/" }))); - }, - services => services.AddAuthentication(), - handler: null); - var transaction = await server.SendAsync("http://example.com/login"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var location = transaction.Response.Headers.Location.AbsoluteUri; - Assert.Contains("https://www.facebook.com/v2.5/dialog/oauth", location); - Assert.Contains("response_type=code", location); - Assert.Contains("client_id=", location); - Assert.Contains("redirect_uri="+ UrlEncoder.Default.Encode("http://example.com/signin-facebook"), location); - Assert.Contains("scope=", location); - Assert.Contains("state=", location); - } - - [Fact] - public async Task ChallengeWillTriggerRedirection() - { - var server = CreateServer( - app => - { - app.UseFacebookAuthentication(new FacebookOptions - { - AppId = "Test App Id", - AppSecret = "Test App Secret" - }); - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AuthenticationScheme = "External" - }); - }, - services => - { - services.AddAuthentication(options => options.SignInScheme = "External"); - }, - context => - { - // REVIEW: gross - context.Authentication.ChallengeAsync("Facebook").GetAwaiter().GetResult(); - return true; - }); - var transaction = await server.SendAsync("http://example.com/challenge"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var location = transaction.Response.Headers.Location.AbsoluteUri; - Assert.Contains("https://www.facebook.com/v2.5/dialog/oauth", location); - Assert.Contains("response_type=code", location); - Assert.Contains("client_id=", location); - Assert.Contains("redirect_uri=", location); - Assert.Contains("scope=", location); - Assert.Contains("state=", location); - } - - [Fact] - public async Task CustomUserInfoEndpointHasValidGraphQuery() - { - var customUserInfoEndpoint = "https://graph.facebook.com/me?fields=email,timezone,picture"; - var finalUserInfoEndpoint = string.Empty; - var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("FacebookTest")); - var server = CreateServer( - app => - { - app.UseCookieAuthentication(); - app.UseFacebookAuthentication(new FacebookOptions - { - AppId = "Test App Id", - AppSecret = "Test App Secret", - StateDataFormat = stateFormat, - UserInformationEndpoint = customUserInfoEndpoint, - BackchannelHttpHandler = new TestHttpMessageHandler - { - Sender = req => - { - if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == FacebookDefaults.TokenEndpoint) - { - var res = new HttpResponseMessage(HttpStatusCode.OK); - var graphResponse = JsonConvert.SerializeObject(new - { - access_token = "TestAuthToken" - }); - res.Content = new StringContent(graphResponse, Encoding.UTF8); - return res; - } - if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == - new Uri(customUserInfoEndpoint).GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped)) - { - finalUserInfoEndpoint = req.RequestUri.ToString(); - var res = new HttpResponseMessage(HttpStatusCode.OK); - var graphResponse = JsonConvert.SerializeObject(new - { - id = "TestProfileId", - name = "TestName" - }); - res.Content = new StringContent(graphResponse, Encoding.UTF8); - return res; - } - return null; - } - } - }); - }, - services => - { - services.AddAuthentication(options => options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme); - }, handler: null); - - var properties = new AuthenticationProperties(); - var correlationKey = ".xsrf"; - var correlationValue = "TestCorrelationId"; - properties.Items.Add(correlationKey, correlationValue); - properties.RedirectUri = "/me"; - var state = stateFormat.Protect(properties); - var transaction = await server.SendAsync( - "https://example.com/signin-facebook?code=TestCode&state=" + UrlEncoder.Default.Encode(state), - $".AspNetCore.Correlation.Facebook.{correlationValue}=N"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); - Assert.Equal(1, finalUserInfoEndpoint.Count(c => c == '?')); - Assert.Contains("fields=email,timezone,picture", finalUserInfoEndpoint); - Assert.Contains("&access_token=", finalUserInfoEndpoint); - } - - private static TestServer CreateServer(Action configure, Action configureServices, Func handler) - { - var builder = new WebHostBuilder() - .Configure(app => - { - if (configure != null) - { - configure(app); - } - app.Use(async (context, next) => - { - if (handler == null || !handler(context)) - { - await next(); - } - }); - }) - .ConfigureServices(configureServices); - return new TestServer(builder); - } - } -} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs new file mode 100644 index 000000000..964af2ce7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs @@ -0,0 +1,389 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json; +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Facebook +{ + public class FacebookTests : RemoteAuthenticationTests + { + protected override string DefaultScheme => FacebookDefaults.AuthenticationScheme; + protected override Type HandlerType => typeof(FacebookHandler); + protected override bool SupportsSignIn { get => false; } + protected override bool SupportsSignOut { get => false; } + + protected override void RegisterAuth(AuthenticationBuilder services, Action configure) + { + services.AddFacebook(o => + { + ConfigureDefaults(o); + configure.Invoke(o); + }); + } + + protected override void ConfigureDefaults(FacebookOptions o) + { + o.AppId = "whatever"; + o.AppSecret = "whatever"; + o.SignInScheme = "auth1"; + } + + [Fact] + public async Task ThrowsIfAppIdMissing() + { + var server = CreateServer( + app => { }, + services => services.AddAuthentication().AddFacebook(o => o.SignInScheme = "Whatever"), + async context => + { + await Assert.ThrowsAsync("AppId", () => context.ChallengeAsync("Facebook")); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ThrowsIfAppSecretMissing() + { + var server = CreateServer( + app => { }, + services => services.AddAuthentication().AddFacebook(o => o.AppId = "Whatever"), + async context => + { + await Assert.ThrowsAsync("AppSecret", () => context.ChallengeAsync("Facebook")); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var server = CreateServer( + app => + { + app.UseAuthentication(); + }, + services => + { + services.AddAuthentication("External") + .AddCookie("External", o => { }) + .AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + o.Events = new OAuthEvents + { + OnRedirectToAuthorizationEndpoint = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + return Task.FromResult(0); + } + }; + }); + }, + async context => + { + await context.ChallengeAsync("Facebook"); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var query = transaction.Response.Headers.Location.Query; + Assert.Contains("custom=test", query); + } + + [Fact] + public async Task ChallengeWillIncludeScopeAsConfigured() + { + var server = CreateServer( + app => app.UseAuthentication(), + services => + { + services.AddAuthentication().AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + o.Scope.Clear(); + o.Scope.Add("foo"); + o.Scope.Add("bar"); + }); + }, + async context => + { + await context.ChallengeAsync(FacebookDefaults.AuthenticationScheme); + return true; + }); + + var transaction = await server.SendAsync("http://example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=foo,bar", res.Headers.Location.Query); + } + + [Fact] + public async Task ChallengeWillIncludeScopeAsOverwritten() + { + var server = CreateServer( + app => app.UseAuthentication(), + services => + { + services.AddAuthentication().AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + o.Scope.Clear(); + o.Scope.Add("foo"); + o.Scope.Add("bar"); + }); + }, + async context => + { + var properties = new OAuthChallengeProperties(); + properties.SetScope("baz", "qux"); + await context.ChallengeAsync(FacebookDefaults.AuthenticationScheme, properties); + return true; + }); + + var transaction = await server.SendAsync("http://example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=baz,qux", res.Headers.Location.Query); + } + + [Fact] + public async Task ChallengeWillIncludeScopeAsOverwrittenWithBaseAuthenticationProperties() + { + var server = CreateServer( + app => app.UseAuthentication(), + services => + { + services.AddAuthentication().AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + o.Scope.Clear(); + o.Scope.Add("foo"); + o.Scope.Add("bar"); + }); + }, + async context => + { + var properties = new AuthenticationProperties(); + properties.SetParameter(OAuthChallengeProperties.ScopeKey, new string[] { "baz", "qux" }); + await context.ChallengeAsync(FacebookDefaults.AuthenticationScheme, properties); + return true; + }); + + var transaction = await server.SendAsync("http://example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=baz,qux", res.Headers.Location.Query); + } + + [Fact] + public async Task NestedMapWillNotAffectRedirect() + { + var server = CreateServer(app => app.Map("/base", map => + { + map.UseAuthentication(); + map.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Facebook", new AuthenticationProperties() { RedirectUri = "/" }))); + }), + services => + { + services.AddAuthentication() + .AddCookie("External", o => { }) + .AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + }); + }, + handler: null); + + var transaction = await server.SendAsync("http://example.com/base/login"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://www.facebook.com/v3.1/dialog/oauth", location); + Assert.Contains("response_type=code", location); + Assert.Contains("client_id=", location); + Assert.Contains("redirect_uri=" + UrlEncoder.Default.Encode("http://example.com/base/signin-facebook"), location); + Assert.Contains("scope=", location); + Assert.Contains("state=", location); + } + + [Fact] + public async Task MapWillNotAffectRedirect() + { + var server = CreateServer( + app => + { + app.UseAuthentication(); + app.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Facebook", new AuthenticationProperties() { RedirectUri = "/" }))); + }, + services => + { + services.AddAuthentication() + .AddCookie("External", o => { }) + .AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + o.SignInScheme = "External"; + }); + }, + handler: null); + var transaction = await server.SendAsync("http://example.com/login"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://www.facebook.com/v3.1/dialog/oauth", location); + Assert.Contains("response_type=code", location); + Assert.Contains("client_id=", location); + Assert.Contains("redirect_uri=" + UrlEncoder.Default.Encode("http://example.com/signin-facebook"), location); + Assert.Contains("scope=", location); + Assert.Contains("state=", location); + } + + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer( + app => app.UseAuthentication(), + services => + { + services.AddAuthentication(options => + { + options.DefaultSignInScheme = "External"; + }) + .AddCookie() + .AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + }); + }, + async context => + { + await context.ChallengeAsync("Facebook"); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://www.facebook.com/v3.1/dialog/oauth", location); + Assert.Contains("response_type=code", location); + Assert.Contains("client_id=", location); + Assert.Contains("redirect_uri=", location); + Assert.Contains("scope=", location); + Assert.Contains("state=", location); + } + + [Fact] + public async Task CustomUserInfoEndpointHasValidGraphQuery() + { + var customUserInfoEndpoint = "https://graph.facebook.com/me?fields=email,timezone,picture"; + var finalUserInfoEndpoint = string.Empty; + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("FacebookTest")); + var server = CreateServer( + app => app.UseAuthentication(), + services => + { + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie() + .AddFacebook(o => + { + o.AppId = "Test App Id"; + o.AppSecret = "Test App Secret"; + o.StateDataFormat = stateFormat; + o.UserInformationEndpoint = customUserInfoEndpoint; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == FacebookDefaults.TokenEndpoint) + { + var res = new HttpResponseMessage(HttpStatusCode.OK); + var graphResponse = JsonConvert.SerializeObject(new + { + access_token = "TestAuthToken" + }); + res.Content = new StringContent(graphResponse, Encoding.UTF8); + return res; + } + if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == + new Uri(customUserInfoEndpoint).GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped)) + { + finalUserInfoEndpoint = req.RequestUri.ToString(); + var res = new HttpResponseMessage(HttpStatusCode.OK); + var graphResponse = JsonConvert.SerializeObject(new + { + id = "TestProfileId", + name = "TestName" + }); + res.Content = new StringContent(graphResponse, Encoding.UTF8); + return res; + } + return null; + } + }; + }); + }, + handler: null); + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-facebook?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Facebook.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(1, finalUserInfoEndpoint.Count(c => c == '?')); + Assert.Contains("fields=email,timezone,picture", finalUserInfoEndpoint); + Assert.Contains("&access_token=", finalUserInfoEndpoint); + } + + private static TestServer CreateServer(Action configure, Action configureServices, Func> handler) + { + var builder = new WebHostBuilder() + .Configure(app => + { + configure?.Invoke(app); + app.Use(async (context, next) => + { + if (handler == null || !await handler(context)) + { + await next(); + } + }); + }) + .ConfigureServices(configureServices); + return new TestServer(builder); + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/Google/GoogleMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/Google/GoogleMiddlewareTests.cs deleted file mode 100644 index 31fb9e317..000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/Google/GoogleMiddlewareTests.cs +++ /dev/null @@ -1,847 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Security.Claims; -using System.Text; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication.OAuth; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Http.Features.Authentication; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using Xunit; - -namespace Microsoft.AspNetCore.Authentication.Google -{ - public class GoogleMiddlewareTests - { - [Fact] - public async Task ChallengeWillTriggerRedirection() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret" - }); - var transaction = await server.SendAsync("https://example.com/challenge"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var location = transaction.Response.Headers.Location.ToString(); - Assert.Contains("https://accounts.google.com/o/oauth2/auth?response_type=code", location); - Assert.Contains("&client_id=", location); - Assert.Contains("&redirect_uri=", location); - Assert.Contains("&scope=", location); - Assert.Contains("&state=", location); - - Assert.DoesNotContain("access_type=", location); - Assert.DoesNotContain("approval_prompt=", location); - Assert.DoesNotContain("login_hint=", location); - } - - [Fact] - public async Task SignInThrows() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret" - }); - var transaction = await server.SendAsync("https://example.com/signIn"); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - } - - [Fact] - public async Task SignOutThrows() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret" - }); - var transaction = await server.SendAsync("https://example.com/signOut"); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - } - - [Fact] - public async Task ForbidThrows() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret" - }); - var transaction = await server.SendAsync("https://example.com/signOut"); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - } - - [Fact] - public async Task Challenge401WillTriggerRedirection() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret", - AutomaticChallenge = true - }); - var transaction = await server.SendAsync("https://example.com/401"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var location = transaction.Response.Headers.Location.ToString(); - Assert.Contains("https://accounts.google.com/o/oauth2/auth?response_type=code", location); - Assert.Contains("&client_id=", location); - Assert.Contains("&redirect_uri=", location); - Assert.Contains("&scope=", location); - Assert.Contains("&state=", location); - } - - [Fact] - public async Task ChallengeWillSetCorrelationCookie() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret" - }); - var transaction = await server.SendAsync("https://example.com/challenge"); - Assert.Contains(transaction.SetCookie, cookie => cookie.StartsWith(".AspNetCore.Correlation.Google.")); - } - - [Fact] - public async Task Challenge401WillSetCorrelationCookie() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret", - AutomaticChallenge = true - }); - var transaction = await server.SendAsync("https://example.com/401"); - Assert.Contains(transaction.SetCookie, cookie => cookie.StartsWith(".AspNetCore.Correlation.Google.")); - } - - [Fact] - public async Task ChallengeWillSetDefaultScope() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret" - }); - var transaction = await server.SendAsync("https://example.com/challenge"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var query = transaction.Response.Headers.Location.Query; - Assert.Contains("&scope=" + UrlEncoder.Default.Encode("openid profile email"), query); - } - - [Fact] - public async Task Challenge401WillSetDefaultScope() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret", - AutomaticChallenge = true - }); - var transaction = await server.SendAsync("https://example.com/401"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var query = transaction.Response.Headers.Location.Query; - Assert.Contains("&scope=" + UrlEncoder.Default.Encode("openid profile email"), query); - } - - [Fact] - public async Task ChallengeWillUseAuthenticationPropertiesAsParameters() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret", - AutomaticChallenge = true - }, - context => - { - var req = context.Request; - var res = context.Response; - if (req.Path == new PathString("/challenge2")) - { - return context.Authentication.ChallengeAsync("Google", new AuthenticationProperties( - new Dictionary() - { - { "scope", "https://www.googleapis.com/auth/plus.login" }, - { "access_type", "offline" }, - { "approval_prompt", "force" }, - { "login_hint", "test@example.com" } - })); - } - - return Task.FromResult(null); - }); - var transaction = await server.SendAsync("https://example.com/challenge2"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var query = transaction.Response.Headers.Location.Query; - Assert.Contains("scope=" + UrlEncoder.Default.Encode("https://www.googleapis.com/auth/plus.login"), query); - Assert.Contains("access_type=offline", query); - Assert.Contains("approval_prompt=force", query); - Assert.Contains("login_hint=" + UrlEncoder.Default.Encode("test@example.com"), query); - } - - [Fact] - public async Task ChallengeWillTriggerApplyRedirectEvent() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret", - Events = new OAuthEvents - { - OnRedirectToAuthorizationEndpoint = context => - { - context.Response.Redirect(context.RedirectUri + "&custom=test"); - return Task.FromResult(0); - } - } - }); - var transaction = await server.SendAsync("https://example.com/challenge"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var query = transaction.Response.Headers.Location.Query; - Assert.Contains("custom=test", query); - } - - [Fact] - public async Task AuthenticateWillFail() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret" - }, - async context => - { - var req = context.Request; - var res = context.Response; - if (req.Path == new PathString("/auth")) - { - var auth = new AuthenticateContext("Google"); - await context.Authentication.AuthenticateAsync(auth); - Assert.NotNull(auth.Error); - } - }); - var transaction = await server.SendAsync("https://example.com/auth"); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - } - - [Fact] - public async Task ReplyPathWithoutStateQueryStringWillBeRejected() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret" - }); - var error = await Assert.ThrowsAnyAsync(() => server.SendAsync("https://example.com/signin-google?code=TestCode")); - Assert.Equal("The oauth state was missing or invalid.", error.GetBaseException().Message); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReplyPathWithErrorFails(bool redirect) - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret", - Events = redirect ? new OAuthEvents() - { - OnRemoteFailure = ctx => - { - ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); - ctx.HandleResponse(); - return Task.FromResult(0); - } - } : new OAuthEvents() - }); - var sendTask = server.SendAsync("https://example.com/signin-google?error=OMG&error_description=SoBad&error_uri=foobar"); - if (redirect) - { - var transaction = await sendTask; - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/error?FailureMessage=OMG" + UrlEncoder.Default.Encode(";Description=SoBad;Uri=foobar"), transaction.Response.Headers.GetValues("Location").First()); - } - else - { - var error = await Assert.ThrowsAnyAsync(() => sendTask); - Assert.Equal("OMG;Description=SoBad;Uri=foobar", error.GetBaseException().Message); - } - } - - [Theory] - [InlineData(null)] - [InlineData("CustomIssuer")] - public async Task ReplyPathWillAuthenticateValidAuthorizeCodeAndState(string claimsIssuer) - { - var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret", - SaveTokens = true, - StateDataFormat = stateFormat, - ClaimsIssuer = claimsIssuer, - BackchannelHttpHandler = new TestHttpMessageHandler - { - Sender = req => - { - if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v3/token") - { - return ReturnJsonResponse(new - { - access_token = "Test Access Token", - expires_in = 3600, - token_type = "Bearer" - }); - } - else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") - { - return ReturnJsonResponse(new - { - id = "Test User ID", - displayName = "Test Name", - name = new - { - familyName = "Test Family Name", - givenName = "Test Given Name" - }, - url = "Profile link", - emails = new[] - { - new - { - value = "Test email", - type = "account" - } - } - }); - } - - throw new NotImplementedException(req.RequestUri.AbsoluteUri); - } - } - }); - - var properties = new AuthenticationProperties(); - var correlationKey = ".xsrf"; - var correlationValue = "TestCorrelationId"; - properties.Items.Add(correlationKey, correlationValue); - properties.RedirectUri = "/me"; - var state = stateFormat.Protect(properties); - var transaction = await server.SendAsync( - "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), - $".AspNetCore.Correlation.Google.{correlationValue}=N"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); - Assert.Equal(2, transaction.SetCookie.Count); - Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); - Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); - - var authCookie = transaction.AuthenticationCookieValue; - transaction = await server.SendAsync("https://example.com/me", authCookie); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - var expectedIssuer = claimsIssuer ?? GoogleDefaults.AuthenticationScheme; - Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name, expectedIssuer)); - Assert.Equal("Test User ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier, expectedIssuer)); - Assert.Equal("Test Given Name", transaction.FindClaimValue(ClaimTypes.GivenName, expectedIssuer)); - Assert.Equal("Test Family Name", transaction.FindClaimValue(ClaimTypes.Surname, expectedIssuer)); - Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email, expectedIssuer)); - - // Ensure claims transformation - Assert.Equal("yup", transaction.FindClaimValue("xform")); - - transaction = await server.SendAsync("https://example.com/tokens", authCookie); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - Assert.Equal("Test Access Token", transaction.FindTokenValue("access_token")); - Assert.Equal("Bearer", transaction.FindTokenValue("token_type")); - Assert.NotNull(transaction.FindTokenValue("expires_at")); - } - - // REVIEW: Fix this once we revisit error handling to not blow up - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReplyPathWillThrowIfCodeIsInvalid(bool redirect) - { - var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret", - StateDataFormat = stateFormat, - BackchannelHttpHandler = new TestHttpMessageHandler - { - Sender = req => - { - return ReturnJsonResponse(new { Error = "Error" }, - HttpStatusCode.BadRequest); - } - }, - Events = redirect ? new OAuthEvents() - { - OnRemoteFailure = ctx => - { - ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); - ctx.HandleResponse(); - return Task.FromResult(0); - } - } : new OAuthEvents() - }); - var properties = new AuthenticationProperties(); - var correlationKey = ".xsrf"; - var correlationValue = "TestCorrelationId"; - properties.Items.Add(correlationKey, correlationValue); - properties.RedirectUri = "/me"; - - var state = stateFormat.Protect(properties); - var sendTask = server.SendAsync( - "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), - $".AspNetCore.Correlation.Google.{correlationValue}=N"); - if (redirect) - { - var transaction = await sendTask; - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};"), - transaction.Response.Headers.GetValues("Location").First()); - } - else - { - var error = await Assert.ThrowsAnyAsync(() => sendTask); - Assert.Equal("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};", error.GetBaseException().Message); - } - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReplyPathWillRejectIfAccessTokenIsMissing(bool redirect) - { - var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret", - StateDataFormat = stateFormat, - BackchannelHttpHandler = new TestHttpMessageHandler - { - Sender = req => - { - return ReturnJsonResponse(new object()); - } - }, - Events = redirect ? new OAuthEvents() - { - OnRemoteFailure = ctx => - { - ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); - ctx.HandleResponse(); - return Task.FromResult(0); - } - } : new OAuthEvents() - }); - var properties = new AuthenticationProperties(); - var correlationKey = ".xsrf"; - var correlationValue = "TestCorrelationId"; - properties.Items.Add(correlationKey, correlationValue); - properties.RedirectUri = "/me"; - var state = stateFormat.Protect(properties); - var sendTask = server.SendAsync( - "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), - $".AspNetCore.Correlation.Google.{correlationValue}=N"); - if (redirect) - { - var transaction = await sendTask; - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("Failed to retrieve access token."), - transaction.Response.Headers.GetValues("Location").First()); - } - else - { - var error = await Assert.ThrowsAnyAsync(() => sendTask); - Assert.Equal("Failed to retrieve access token.", error.GetBaseException().Message); - } - } - - [Fact] - public async Task AuthenticatedEventCanGetRefreshToken() - { - var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret", - StateDataFormat = stateFormat, - BackchannelHttpHandler = new TestHttpMessageHandler - { - Sender = req => - { - if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v3/token") - { - return ReturnJsonResponse(new - { - access_token = "Test Access Token", - expires_in = 3600, - token_type = "Bearer", - refresh_token = "Test Refresh Token" - }); - } - else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") - { - return ReturnJsonResponse(new - { - id = "Test User ID", - displayName = "Test Name", - name = new - { - familyName = "Test Family Name", - givenName = "Test Given Name" - }, - url = "Profile link", - emails = new[] - { - new - { - value = "Test email", - type = "account" - } - } - }); - } - - throw new NotImplementedException(req.RequestUri.AbsoluteUri); - } - }, - Events = new OAuthEvents - { - OnCreatingTicket = context => - { - var refreshToken = context.RefreshToken; - context.Ticket.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "Google") }, "Google")); - return Task.FromResult(0); - } - } - }); - var properties = new AuthenticationProperties(); - var correlationKey = ".xsrf"; - var correlationValue = "TestCorrelationId"; - properties.Items.Add(correlationKey, correlationValue); - properties.RedirectUri = "/me"; - var state = stateFormat.Protect(properties); - var transaction = await server.SendAsync( - "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), - $".AspNetCore.Correlation.Google.{correlationValue}=N"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); - Assert.Equal(2, transaction.SetCookie.Count); - Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); - Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); - - var authCookie = transaction.AuthenticationCookieValue; - transaction = await server.SendAsync("https://example.com/me", authCookie); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - Assert.Equal("Test Refresh Token", transaction.FindClaimValue("RefreshToken")); - } - - [Fact] - public async Task NullRedirectUriWillRedirectToSlash() - { - var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret", - StateDataFormat = stateFormat, - BackchannelHttpHandler = new TestHttpMessageHandler - { - Sender = req => - { - if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v3/token") - { - return ReturnJsonResponse(new - { - access_token = "Test Access Token", - expires_in = 3600, - token_type = "Bearer", - refresh_token = "Test Refresh Token" - }); - } - else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") - { - return ReturnJsonResponse(new - { - id = "Test User ID", - displayName = "Test Name", - name = new - { - familyName = "Test Family Name", - givenName = "Test Given Name" - }, - url = "Profile link", - emails = new[] - { - new - { - value = "Test email", - type = "account" - } - } - }); - } - - throw new NotImplementedException(req.RequestUri.AbsoluteUri); - } - }, - Events = new OAuthEvents - { - OnTicketReceived = context => - { - context.Ticket.Properties.RedirectUri = null; - return Task.FromResult(0); - } - } - }); - var properties = new AuthenticationProperties(); - var correlationKey = ".xsrf"; - var correlationValue = "TestCorrelationId"; - properties.Items.Add(correlationKey, correlationValue); - var state = stateFormat.Protect(properties); - var transaction = await server.SendAsync( - "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), - $".AspNetCore.Correlation.Google.{correlationValue}=N"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/", transaction.Response.Headers.GetValues("Location").First()); - Assert.Equal(2, transaction.SetCookie.Count); - Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); - Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); - } - - [Fact] - public async Task ValidateAuthenticatedContext() - { - var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret", - StateDataFormat = stateFormat, - AccessType = "offline", - Events = new OAuthEvents() - { - OnCreatingTicket = context => - { - Assert.NotNull(context.User); - Assert.Equal(context.AccessToken, "Test Access Token"); - Assert.Equal(context.RefreshToken, "Test Refresh Token"); - Assert.Equal(context.ExpiresIn, TimeSpan.FromSeconds(3600)); - Assert.Equal(GoogleHelper.GetEmail(context.User), "Test email"); - Assert.Equal(GoogleHelper.GetId(context.User), "Test User ID"); - Assert.Equal(GoogleHelper.GetName(context.User), "Test Name"); - Assert.Equal(GoogleHelper.GetFamilyName(context.User), "Test Family Name"); - Assert.Equal(GoogleHelper.GetGivenName(context.User), "Test Given Name"); - return Task.FromResult(0); - } - }, - BackchannelHttpHandler = new TestHttpMessageHandler - { - Sender = req => - { - if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v3/token") - { - return ReturnJsonResponse(new - { - access_token = "Test Access Token", - expires_in = 3600, - token_type = "Bearer", - refresh_token = "Test Refresh Token" - }); - } - else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") - { - return ReturnJsonResponse(new - { - id = "Test User ID", - displayName = "Test Name", - name = new - { - familyName = "Test Family Name", - givenName = "Test Given Name" - }, - url = "Profile link", - emails = new[] - { - new - { - value = "Test email", - type = "account" - } - } - }); - } - - throw new NotImplementedException(req.RequestUri.AbsoluteUri); - } - } - }); - - var properties = new AuthenticationProperties(); - var correlationKey = ".xsrf"; - var correlationValue = "TestCorrelationId"; - properties.Items.Add(correlationKey, correlationValue); - properties.RedirectUri = "/foo"; - var state = stateFormat.Protect(properties); - - //Post a message to the Google middleware - var transaction = await server.SendAsync( - "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), - $".AspNetCore.Correlation.Google.{correlationValue}=N"); - - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/foo", transaction.Response.Headers.GetValues("Location").First()); - } - - [Fact] - public async Task NoStateCausesException() - { - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret" - }); - - //Post a message to the Google middleware - var error = await Assert.ThrowsAnyAsync(() => server.SendAsync("https://example.com/signin-google?code=TestCode")); - Assert.Equal("The oauth state was missing or invalid.", error.GetBaseException().Message); - } - - [Fact] - public async Task CanRedirectOnError() - { - var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); - var server = CreateServer(new GoogleOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret", - Events = new OAuthEvents() - { - OnRemoteFailure = ctx => - { - ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); - ctx.HandleResponse(); - return Task.FromResult(0); - } - } - }); - - //Post a message to the Google middleware - var transaction = await server.SendAsync( - "https://example.com/signin-google?code=TestCode"); - - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("The oauth state was missing or invalid."), - transaction.Response.Headers.GetValues("Location").First()); - } - - private static HttpResponseMessage ReturnJsonResponse(object content, HttpStatusCode code = HttpStatusCode.OK) - { - var res = new HttpResponseMessage(code); - var text = JsonConvert.SerializeObject(content); - res.Content = new StringContent(text, Encoding.UTF8, "application/json"); - return res; - } - - private static TestServer CreateServer(GoogleOptions options, Func testpath = null) - { - var builder = new WebHostBuilder() - .Configure(app => - { - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AuthenticationScheme = TestExtensions.CookieAuthenticationScheme, - AutomaticAuthenticate = true - }); - app.UseGoogleAuthentication(options); - app.UseClaimsTransformation(context => - { - var id = new ClaimsIdentity("xform"); - id.AddClaim(new Claim("xform", "yup")); - context.Principal.AddIdentity(id); - return Task.FromResult(context.Principal); - }); - app.Use(async (context, next) => - { - var req = context.Request; - var res = context.Response; - if (req.Path == new PathString("/challenge")) - { - await context.Authentication.ChallengeAsync("Google"); - } - else if (req.Path == new PathString("/tokens")) - { - var authContext = new AuthenticateContext(TestExtensions.CookieAuthenticationScheme); - await context.Authentication.AuthenticateAsync(authContext); - var tokens = new AuthenticationProperties(authContext.Properties).GetTokens(); - res.Describe(tokens); - } - else if (req.Path == new PathString("/me")) - { - res.Describe(context.User); - } - else if (req.Path == new PathString("/unauthorized")) - { - // Simulate Authorization failure - var result = await context.Authentication.AuthenticateAsync("Google"); - await context.Authentication.ChallengeAsync("Google"); - } - else if (req.Path == new PathString("/unauthorizedAuto")) - { - var result = await context.Authentication.AuthenticateAsync("Google"); - await context.Authentication.ChallengeAsync(); - } - else if (req.Path == new PathString("/401")) - { - res.StatusCode = 401; - } - else if (req.Path == new PathString("/signIn")) - { - await Assert.ThrowsAsync(() => context.Authentication.SignInAsync("Google", new ClaimsPrincipal())); - } - else if (req.Path == new PathString("/signOut")) - { - await Assert.ThrowsAsync(() => context.Authentication.SignOutAsync("Google")); - } - else if (req.Path == new PathString("/forbid")) - { - await Assert.ThrowsAsync(() => context.Authentication.ForbidAsync("Google")); - } - else if (testpath != null) - { - await testpath(context); - } - else - { - await next(); - } - }); - }) - .ConfigureServices(services => - { - services.AddAuthentication(authOptions => authOptions.SignInScheme = TestExtensions.CookieAuthenticationScheme); - }); - return new TestServer(builder); - } - } -} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs new file mode 100644 index 000000000..ce158eaf2 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs @@ -0,0 +1,1285 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Google +{ + public class GoogleTests : RemoteAuthenticationTests + { + protected override string DefaultScheme => GoogleDefaults.AuthenticationScheme; + protected override Type HandlerType => typeof(GoogleHandler); + protected override bool SupportsSignIn { get => false; } + protected override bool SupportsSignOut { get => false; } + + protected override void RegisterAuth(AuthenticationBuilder services, Action configure) + { + services.AddGoogle(o => + { + ConfigureDefaults(o); + configure.Invoke(o); + }); + } + + protected override void ConfigureDefaults(GoogleOptions o) + { + o.ClientId = "whatever"; + o.ClientSecret = "whatever"; + o.SignInScheme = "auth1"; + } + + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.ToString(); + Assert.Contains("https://accounts.google.com/o/oauth2/v2/auth?response_type=code", location); + Assert.Contains("&client_id=", location); + Assert.Contains("&redirect_uri=", location); + Assert.Contains("&scope=", location); + Assert.Contains("&state=", location); + + Assert.DoesNotContain("access_type=", location); + Assert.DoesNotContain("prompt=", location); + Assert.DoesNotContain("approval_prompt=", location); + Assert.DoesNotContain("login_hint=", location); + Assert.DoesNotContain("include_granted_scopes=", location); + } + + [Fact] + public async Task SignInThrows() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signIn"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task SignOutThrows() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ForbidThrows() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task Challenge401WillNotTriggerRedirection() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/401"); + Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode); + } + + [Fact] + public async Task ChallengeWillSetCorrelationCookie() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + Assert.Contains(transaction.SetCookie, cookie => cookie.StartsWith(".AspNetCore.Correlation.Google.")); + } + + [Fact] + public async Task ChallengeWillSetDefaultScope() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var query = transaction.Response.Headers.Location.Query; + Assert.Contains("&scope=" + UrlEncoder.Default.Encode("openid profile email"), query); + } + + [Fact] + public async Task ChallengeWillUseAuthenticationPropertiesParametersAsQueryArguments() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + }, + context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge2")) + { + return context.ChallengeAsync("Google", new GoogleChallengeProperties + { + Scope = new string[] { "openid", "https://www.googleapis.com/auth/plus.login" }, + AccessType = "offline", + ApprovalPrompt = "force", + Prompt = "consent", + LoginHint = "test@example.com", + IncludeGrantedScopes = false, + }); + } + + return Task.FromResult(null); + }); + var transaction = await server.SendAsync("https://example.com/challenge2"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + // verify query arguments + var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query); + Assert.Equal("openid https://www.googleapis.com/auth/plus.login", query["scope"]); + Assert.Equal("offline", query["access_type"]); + Assert.Equal("force", query["approval_prompt"]); + Assert.Equal("consent", query["prompt"]); + Assert.Equal("false", query["include_granted_scopes"]); + Assert.Equal("test@example.com", query["login_hint"]); + + // verify that the passed items were not serialized + var stateProperties = stateFormat.Unprotect(query["state"]); + Assert.DoesNotContain("scope", stateProperties.Items.Keys); + Assert.DoesNotContain("access_type", stateProperties.Items.Keys); + Assert.DoesNotContain("include_granted_scopes", stateProperties.Items.Keys); + Assert.DoesNotContain("approval_prompt", stateProperties.Items.Keys); + Assert.DoesNotContain("prompt", stateProperties.Items.Keys); + Assert.DoesNotContain("login_hint", stateProperties.Items.Keys); + } + + [Fact] + public async Task ChallengeWillUseAuthenticationPropertiesItemsAsParameters() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + }, + context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge2")) + { + return context.ChallengeAsync("Google", new AuthenticationProperties(new Dictionary() + { + { "scope", "https://www.googleapis.com/auth/plus.login" }, + { "access_type", "offline" }, + { "approval_prompt", "force" }, + { "prompt", "consent" }, + { "login_hint", "test@example.com" }, + { "include_granted_scopes", "false" } + })); + } + + return Task.FromResult(null); + }); + var transaction = await server.SendAsync("https://example.com/challenge2"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + // verify query arguments + var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query); + Assert.Equal("https://www.googleapis.com/auth/plus.login", query["scope"]); + Assert.Equal("offline", query["access_type"]); + Assert.Equal("force", query["approval_prompt"]); + Assert.Equal("consent", query["prompt"]); + Assert.Equal("false", query["include_granted_scopes"]); + Assert.Equal("test@example.com", query["login_hint"]); + + // verify that the passed items were not serialized + var stateProperties = stateFormat.Unprotect(query["state"]); + Assert.DoesNotContain("scope", stateProperties.Items.Keys); + Assert.DoesNotContain("access_type", stateProperties.Items.Keys); + Assert.DoesNotContain("include_granted_scopes", stateProperties.Items.Keys); + Assert.DoesNotContain("approval_prompt", stateProperties.Items.Keys); + Assert.DoesNotContain("prompt", stateProperties.Items.Keys); + Assert.DoesNotContain("login_hint", stateProperties.Items.Keys); + } + + [Fact] + public async Task ChallengeWillUseAuthenticationPropertiesItemsAsQueryArgumentsButParametersWillOverwrite() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + }, + context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge2")) + { + return context.ChallengeAsync("Google", new GoogleChallengeProperties(new Dictionary + { + ["scope"] = "https://www.googleapis.com/auth/plus.login", + ["access_type"] = "offline", + ["include_granted_scopes"] = "false", + ["approval_prompt"] = "force", + ["prompt"] = "login", + ["login_hint"] = "this-will-be-overwritten@example.com", + }) + { + Prompt = "consent", + LoginHint = "test@example.com", + }); + } + + return Task.FromResult(null); + }); + var transaction = await server.SendAsync("https://example.com/challenge2"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + // verify query arguments + var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query); + Assert.Equal("https://www.googleapis.com/auth/plus.login", query["scope"]); + Assert.Equal("offline", query["access_type"]); + Assert.Equal("force", query["approval_prompt"]); + Assert.Equal("consent", query["prompt"]); + Assert.Equal("false", query["include_granted_scopes"]); + Assert.Equal("test@example.com", query["login_hint"]); + + // verify that the passed items were not serialized + var stateProperties = stateFormat.Unprotect(query["state"]); + Assert.DoesNotContain("scope", stateProperties.Items.Keys); + Assert.DoesNotContain("access_type", stateProperties.Items.Keys); + Assert.DoesNotContain("include_granted_scopes", stateProperties.Items.Keys); + Assert.DoesNotContain("approval_prompt", stateProperties.Items.Keys); + Assert.DoesNotContain("prompt", stateProperties.Items.Keys); + Assert.DoesNotContain("login_hint", stateProperties.Items.Keys); + } + + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.Events = new OAuthEvents + { + OnRedirectToAuthorizationEndpoint = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + return Task.FromResult(0); + } + }; + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var query = transaction.Response.Headers.Location.Query; + Assert.Contains("custom=test", query); + } + + [Fact] + public async Task AuthenticateWithoutCookieWillFail() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }, + async context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/auth")) + { + var result = await context.AuthenticateAsync("Google"); + Assert.NotNull(result.Failure); + } + }); + var transaction = await server.SendAsync("https://example.com/auth"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ReplyPathWithoutStateQueryStringWillBeRejected() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var error = await Assert.ThrowsAnyAsync(() => server.SendAsync("https://example.com/signin-google?code=TestCode")); + Assert.Equal("The oauth state was missing or invalid.", error.GetBaseException().Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReplyPathWithAccessDeniedErrorFails(bool redirect) + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = new TestStateDataFormat(); + o.Events = redirect ? new OAuthEvents() + { + OnAccessDenied = ctx => + { + ctx.Response.Redirect("/error?FailureMessage=AccessDenied"); + ctx.HandleResponse(); + return Task.FromResult(0); + } + } : new OAuthEvents(); + }); + var sendTask = server.SendAsync("https://example.com/signin-google?error=access_denied&error_description=SoBad&error_uri=foobar&state=protected_state", + ".AspNetCore.Correlation.Google.correlationId=N"); + if (redirect) + { + var transaction = await sendTask; + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?FailureMessage=AccessDenied", transaction.Response.Headers.GetValues("Location").First()); + } + else + { + var error = await Assert.ThrowsAnyAsync(() => sendTask); + Assert.Equal("Access was denied by the resource owner or by the remote server.", error.GetBaseException().Message); + } + } + + [Fact] + public async Task ReplyPathWithAccessDeniedError_AllowsCustomizingPath() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = new TestStateDataFormat(); + o.AccessDeniedPath = "/access-denied"; + o.Events = new OAuthEvents() + { + OnAccessDenied = ctx => + { + Assert.Equal("/access-denied", ctx.AccessDeniedPath.Value); + Assert.Equal("http://testhost/redirect", ctx.ReturnUrl); + Assert.Equal("ReturnUrl", ctx.ReturnUrlParameter); + ctx.AccessDeniedPath = "/custom-denied-page"; + ctx.ReturnUrl = "http://www.google.com/"; + ctx.ReturnUrlParameter = "rurl"; + return Task.FromResult(0); + } + }; + }); + var transaction = await server.SendAsync("https://example.com/signin-google?error=access_denied&error_description=SoBad&error_uri=foobar&state=protected_state", + ".AspNetCore.Correlation.Google.correlationId=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/custom-denied-page?rurl=http%3A%2F%2Fwww.google.com%2F", transaction.Response.Headers.GetValues("Location").First()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReplyPathWithErrorFails(bool redirect) + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = new TestStateDataFormat(); + o.Events = redirect ? new OAuthEvents() + { + OnRemoteFailure = ctx => + { + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); + ctx.HandleResponse(); + return Task.FromResult(0); + } + } : new OAuthEvents(); + }); + var sendTask = server.SendAsync("https://example.com/signin-google?error=OMG&error_description=SoBad&error_uri=foobar&state=protected_state", + ".AspNetCore.Correlation.Google.correlationId=N"); + if (redirect) + { + var transaction = await sendTask; + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?FailureMessage=OMG" + UrlEncoder.Default.Encode(";Description=SoBad;Uri=foobar"), transaction.Response.Headers.GetValues("Location").First()); + } + else + { + var error = await Assert.ThrowsAnyAsync(() => sendTask); + Assert.Equal("OMG;Description=SoBad;Uri=foobar", error.GetBaseException().Message); + } + } + + [Theory] + [InlineData(null)] + [InlineData("CustomIssuer")] + public async Task ReplyPathWillAuthenticateValidAuthorizeCodeAndState(string claimsIssuer) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.SaveTokens = true; + o.StateDataFormat = stateFormat; + if (claimsIssuer != null) + { + o.ClaimsIssuer = claimsIssuer; + } + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expires_in = 3600, + token_type = "Bearer" + }); + } + else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + name = new + { + familyName = "Test Family Name", + givenName = "Test Given Name" + }, + url = "Profile link", + emails = new[] + { + new + { + value = "Test email", + type = "account" + } + } + }); + } + + throw new NotImplementedException(req.RequestUri.AbsoluteUri); + } + }; + }); + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/me", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + var expectedIssuer = claimsIssuer ?? GoogleDefaults.AuthenticationScheme; + Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name, expectedIssuer)); + Assert.Equal("Test User ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier, expectedIssuer)); + Assert.Equal("Test Given Name", transaction.FindClaimValue(ClaimTypes.GivenName, expectedIssuer)); + Assert.Equal("Test Family Name", transaction.FindClaimValue(ClaimTypes.Surname, expectedIssuer)); + Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email, expectedIssuer)); + + // Ensure claims transformation + Assert.Equal("yup", transaction.FindClaimValue("xform")); + + transaction = await server.SendAsync("https://example.com/tokens", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Access Token", transaction.FindTokenValue("access_token")); + Assert.Equal("Bearer", transaction.FindTokenValue("token_type")); + Assert.NotNull(transaction.FindTokenValue("expires_at")); + } + + // REVIEW: Fix this once we revisit error handling to not blow up + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReplyPathWillThrowIfCodeIsInvalid(bool redirect) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + return ReturnJsonResponse(new { Error = "Error" }, + HttpStatusCode.BadRequest); + } + }; + o.Events = redirect ? new OAuthEvents() + { + OnRemoteFailure = ctx => + { + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); + ctx.HandleResponse(); + return Task.FromResult(0); + } + } : new OAuthEvents(); + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + + var state = stateFormat.Protect(properties); + var sendTask = server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + if (redirect) + { + var transaction = await sendTask; + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};"), + transaction.Response.Headers.GetValues("Location").First()); + } + else + { + var error = await Assert.ThrowsAnyAsync(() => sendTask); + Assert.Equal("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};", error.GetBaseException().Message); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReplyPathWillRejectIfAccessTokenIsMissing(bool redirect) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + return ReturnJsonResponse(new object()); + } + }; + o.Events = redirect ? new OAuthEvents() + { + OnRemoteFailure = ctx => + { + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); + ctx.HandleResponse(); + return Task.FromResult(0); + } + } : new OAuthEvents(); + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var sendTask = server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + if (redirect) + { + var transaction = await sendTask; + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("Failed to retrieve access token."), + transaction.Response.Headers.GetValues("Location").First()); + } + else + { + var error = await Assert.ThrowsAnyAsync(() => sendTask); + Assert.Equal("Failed to retrieve access token.", error.GetBaseException().Message); + } + } + + [Fact] + public async Task AuthenticatedEventCanGetRefreshToken() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expires_in = 3600, + token_type = "Bearer", + refresh_token = "Test Refresh Token" + }); + } + else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + name = new + { + familyName = "Test Family Name", + givenName = "Test Given Name" + }, + url = "Profile link", + emails = new[] + { + new + { + value = "Test email", + type = "account" + } + } + }); + } + + throw new NotImplementedException(req.RequestUri.AbsoluteUri); + } + }; + o.Events = new OAuthEvents + { + OnCreatingTicket = context => + { + var refreshToken = context.RefreshToken; + context.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "Google") }, "Google")); + return Task.FromResult(0); + } + }; + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/me", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Refresh Token", transaction.FindClaimValue("RefreshToken")); + } + + [Fact] + public async Task NullRedirectUriWillRedirectToSlash() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expires_in = 3600, + token_type = "Bearer", + refresh_token = "Test Refresh Token" + }); + } + else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + name = new + { + familyName = "Test Family Name", + givenName = "Test Given Name" + }, + url = "Profile link", + emails = new[] + { + new + { + value = "Test email", + type = "account" + } + } + }); + } + + throw new NotImplementedException(req.RequestUri.AbsoluteUri); + } + }; + o.Events = new OAuthEvents + { + OnTicketReceived = context => + { + context.Properties.RedirectUri = null; + return Task.FromResult(0); + } + }; + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + } + + [Fact] + public async Task ValidateAuthenticatedContext() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.AccessType = "offline"; + o.Events = new OAuthEvents() + { + OnCreatingTicket = context => + { + Assert.NotNull(context.User); + Assert.Equal("Test Access Token", context.AccessToken); + Assert.Equal("Test Refresh Token", context.RefreshToken); + Assert.Equal(TimeSpan.FromSeconds(3600), context.ExpiresIn); + Assert.Equal("Test email", context.Identity.FindFirst(ClaimTypes.Email)?.Value); + Assert.Equal("Test User ID", context.Identity.FindFirst(ClaimTypes.NameIdentifier)?.Value); + Assert.Equal("Test Name", context.Identity.FindFirst(ClaimTypes.Name)?.Value); + Assert.Equal("Test Family Name", context.Identity.FindFirst(ClaimTypes.Surname)?.Value); + Assert.Equal("Test Given Name", context.Identity.FindFirst(ClaimTypes.GivenName)?.Value); + return Task.FromResult(0); + } + }; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expires_in = 3600, + token_type = "Bearer", + refresh_token = "Test Refresh Token" + }); + } + else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + name = new + { + familyName = "Test Family Name", + givenName = "Test Given Name" + }, + url = "Profile link", + emails = new[] + { + new + { + value = "Test email", + type = "account" + } + } + }); + } + + throw new NotImplementedException(req.RequestUri.AbsoluteUri); + } + }; + }); + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/foo"; + var state = stateFormat.Protect(properties); + + //Post a message to the Google middleware + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/foo", transaction.Response.Headers.GetValues("Location").First()); + } + + [Fact] + public async Task NoStateCausesException() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + + //Post a message to the Google middleware + var error = await Assert.ThrowsAnyAsync(() => server.SendAsync("https://example.com/signin-google?code=TestCode")); + Assert.Equal("The oauth state was missing or invalid.", error.GetBaseException().Message); + } + + [Fact] + public async Task CanRedirectOnError() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.Events = new OAuthEvents() + { + OnRemoteFailure = ctx => + { + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); + ctx.HandleResponse(); + return Task.FromResult(0); + } + }; + }); + + //Post a message to the Google middleware + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("The oauth state was missing or invalid."), + transaction.Response.Headers.GetValues("Location").First()); + } + + [Fact] + public async Task AuthenticateAutomaticWhenAlreadySignedInSucceeds() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.SaveTokens = true; + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/authenticate", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name)); + Assert.Equal("Test User ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier)); + Assert.Equal("Test Given Name", transaction.FindClaimValue(ClaimTypes.GivenName)); + Assert.Equal("Test Family Name", transaction.FindClaimValue(ClaimTypes.Surname)); + Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email)); + + // Ensure claims transformation + Assert.Equal("yup", transaction.FindClaimValue("xform")); + } + + [Fact] + public async Task AuthenticateGoogleWhenAlreadySignedInSucceeds() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.SaveTokens = true; + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/authenticateGoogle", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name)); + Assert.Equal("Test User ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier)); + Assert.Equal("Test Given Name", transaction.FindClaimValue(ClaimTypes.GivenName)); + Assert.Equal("Test Family Name", transaction.FindClaimValue(ClaimTypes.Surname)); + Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email)); + + // Ensure claims transformation + Assert.Equal("yup", transaction.FindClaimValue("xform")); + } + + [Fact] + public async Task AuthenticateFacebookWhenAlreadySignedWithGoogleReturnsNull() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.SaveTokens = true; + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/authenticateFacebook", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Null(transaction.FindClaimValue(ClaimTypes.Name)); + } + + [Fact] + public async Task ChallengeFacebookWhenAlreadySignedWithGoogleSucceeds() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("GoogleTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.StateDataFormat = stateFormat; + o.SaveTokens = true; + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Google.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/challengeFacebook", authCookie); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.StartsWith("https://www.facebook.com/", transaction.Response.Headers.Location.OriginalString); + } + + private HttpMessageHandler CreateBackchannel() + { + return new TestHttpMessageHandler() + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expires_in = 3600, + token_type = "Bearer" + }); + } + else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + name = new + { + familyName = "Test Family Name", + givenName = "Test Given Name" + }, + url = "Profile link", + emails = new[] + { + new + { + value = "Test email", + type = "account" + } + } + }); + } + + throw new NotImplementedException(req.RequestUri.AbsoluteUri); + } + }; + } + + private static HttpResponseMessage ReturnJsonResponse(object content, HttpStatusCode code = HttpStatusCode.OK) + { + var res = new HttpResponseMessage(code); + var text = JsonConvert.SerializeObject(content); + res.Content = new StringContent(text, Encoding.UTF8, "application/json"); + return res; + } + + private class ClaimsTransformer : IClaimsTransformation + { + public Task TransformAsync(ClaimsPrincipal p) + { + if (!p.Identities.Any(i => i.AuthenticationType == "xform")) + { + var id = new ClaimsIdentity("xform"); + id.AddClaim(new Claim("xform", "yup")); + p.AddIdentity(id); + } + return Task.FromResult(p); + } + } + + private static TestServer CreateServer(Action configureOptions, Func testpath = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge")) + { + await context.ChallengeAsync(); + } + else if (req.Path == new PathString("/challengeFacebook")) + { + await context.ChallengeAsync("Facebook"); + } + else if (req.Path == new PathString("/tokens")) + { + var result = await context.AuthenticateAsync(TestExtensions.CookieAuthenticationScheme); + var tokens = result.Properties.GetTokens(); + res.Describe(tokens); + } + else if (req.Path == new PathString("/me")) + { + res.Describe(context.User); + } + else if (req.Path == new PathString("/authenticate")) + { + var result = await context.AuthenticateAsync(TestExtensions.CookieAuthenticationScheme); + res.Describe(result.Principal); + } + else if (req.Path == new PathString("/authenticateGoogle")) + { + var result = await context.AuthenticateAsync("Google"); + res.Describe(result?.Principal); + } + else if (req.Path == new PathString("/authenticateFacebook")) + { + var result = await context.AuthenticateAsync("Facebook"); + res.Describe(result?.Principal); + } + else if (req.Path == new PathString("/unauthorized")) + { + // Simulate Authorization failure + var result = await context.AuthenticateAsync("Google"); + await context.ChallengeAsync("Google"); + } + else if (req.Path == new PathString("/unauthorizedAuto")) + { + var result = await context.AuthenticateAsync("Google"); + await context.ChallengeAsync("Google"); + } + else if (req.Path == new PathString("/401")) + { + res.StatusCode = 401; + } + else if (req.Path == new PathString("/signIn")) + { + await Assert.ThrowsAsync(() => context.SignInAsync("Google", new ClaimsPrincipal())); + } + else if (req.Path == new PathString("/signOut")) + { + await Assert.ThrowsAsync(() => context.SignOutAsync("Google")); + } + else if (req.Path == new PathString("/forbid")) + { + await Assert.ThrowsAsync(() => context.ForbidAsync("Google")); + } + else if (testpath != null) + { + await testpath(context); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + services.AddTransient(); + services.AddAuthentication(TestExtensions.CookieAuthenticationScheme) + .AddCookie(TestExtensions.CookieAuthenticationScheme, o => o.ForwardChallenge = GoogleDefaults.AuthenticationScheme) + .AddGoogle(configureOptions) + .AddFacebook(o => + { + o.ClientId = "Test ClientId"; + o.ClientSecret = "Test AppSecrent"; + }); + }); + return new TestServer(builder); + } + + private class TestStateDataFormat : ISecureDataFormat + { + private AuthenticationProperties Data { get; set; } + + public string Protect(AuthenticationProperties data) + { + return "protected_state"; + } + + public string Protect(AuthenticationProperties data, string purpose) + { + throw new NotImplementedException(); + } + + public AuthenticationProperties Unprotect(string protectedText) + { + Assert.Equal("protected_state", protectedText); + var properties = new AuthenticationProperties(new Dictionary() + { + { ".xsrf", "correlationId" }, + { "testkey", "testvalue" } + }); + properties.RedirectUri = "http://testhost/redirect"; + return properties; + } + + public AuthenticationProperties Unprotect(string protectedText, string purpose) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs deleted file mode 100644 index 81c03dc83..000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs +++ /dev/null @@ -1,521 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Security.Claims; -using System.Threading.Tasks; -using System.Xml.Linq; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Http.Features.Authentication; -using Microsoft.AspNetCore.TestHost; -using Microsoft.AspNetCore.Testing.xunit; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.IdentityModel.Tokens; -using Xunit; - -namespace Microsoft.AspNetCore.Authentication.JwtBearer -{ - public class JwtBearerMiddlewareTests - { - [ConditionalFact(Skip = "Need to remove dependency on AAD since the generated tokens will expire")] - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/179 - public async Task BearerTokenValidation() - { - var options = new JwtBearerOptions - { - AutomaticAuthenticate = true, - Authority = "https://login.windows.net/tushartest.onmicrosoft.com", - Audience = "https://TusharTest.onmicrosoft.com/TodoListService-ManualJwt" - }; - options.TokenValidationParameters.ValidateLifetime = false; - var server = CreateServer(options); - - var newBearerToken = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImtyaU1QZG1Cdng2OHNrVDgtbVBBQjNCc2VlQSJ9.eyJhdWQiOiJodHRwczovL1R1c2hhclRlc3Qub25taWNyb3NvZnQuY29tL1RvZG9MaXN0U2VydmljZS1NYW51YWxKd3QiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9hZmJlY2UwMy1hZWFhLTRmM2YtODVlNy1jZTA4ZGQyMGNlNTAvIiwiaWF0IjoxNDE4MzMwNjE0LCJuYmYiOjE0MTgzMzA2MTQsImV4cCI6MTQxODMzNDUxNCwidmVyIjoiMS4wIiwidGlkIjoiYWZiZWNlMDMtYWVhYS00ZjNmLTg1ZTctY2UwOGRkMjBjZTUwIiwiYW1yIjpbInB3ZCJdLCJvaWQiOiI1Mzk3OTdjMi00MDE5LTQ2NTktOWRiNS03MmM0Yzc3NzhhMzMiLCJ1cG4iOiJWaWN0b3JAVHVzaGFyVGVzdC5vbm1pY3Jvc29mdC5jb20iLCJ1bmlxdWVfbmFtZSI6IlZpY3RvckBUdXNoYXJUZXN0Lm9ubWljcm9zb2Z0LmNvbSIsInN1YiI6IkQyMm9aMW9VTzEzTUFiQXZrdnFyd2REVE80WXZJdjlzMV9GNWlVOVUwYnciLCJmYW1pbHlfbmFtZSI6Ikd1cHRhIiwiZ2l2ZW5fbmFtZSI6IlZpY3RvciIsImFwcGlkIjoiNjEzYjVhZjgtZjJjMy00MWI2LWExZGMtNDE2Yzk3ODAzMGI3IiwiYXBwaWRhY3IiOiIwIiwic2NwIjoidXNlcl9pbXBlcnNvbmF0aW9uIiwiYWNyIjoiMSJ9.N_Kw1EhoVGrHbE6hOcm7ERdZ7paBQiNdObvp2c6T6n5CE8p0fZqmUd-ya_EqwElcD6SiKSiP7gj0gpNUnOJcBl_H2X8GseaeeMxBrZdsnDL8qecc6_ygHruwlPltnLTdka67s1Ow4fDSHaqhVTEk6lzGmNEcbNAyb0CxQxU6o7Fh0yHRiWoLsT8yqYk8nKzsHXfZBNby4aRo3_hXaa4i0SZLYfDGGYPdttG4vT_u54QGGd4Wzbonv2gjDlllOVGOwoJS6kfl1h8mk0qxdiIaT_ChbDWgkWvTB7bTvBE-EgHgV0XmAo0WtJeSxgjsG3KhhEPsONmqrSjhIUV4IVnF2w"; - var response = await SendAsync(server, "http://example.com/oauth", newBearerToken); - Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); - } - - [Fact] - public async Task SignInThrows() - { - var server = CreateServer(new JwtBearerOptions - { - AutomaticAuthenticate = true - }); - var transaction = await server.SendAsync("https://example.com/signIn"); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - } - - [Fact] - public async Task SignOutThrows() - { - var server = CreateServer(new JwtBearerOptions - { - AutomaticAuthenticate = true - }); - var transaction = await server.SendAsync("https://example.com/signOut"); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - } - - - [Fact] - public async Task CustomHeaderReceived() - { - var server = CreateServer(new JwtBearerOptions - { - AutomaticAuthenticate = true, - Events = new JwtBearerEvents() - { - OnMessageReceived = context => - { - var claims = new[] - { - new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"), - new Claim(ClaimTypes.Email, "bob@contoso.com"), - new Claim(ClaimsIdentity.DefaultNameClaimType, "bob") - }; - - context.Ticket = new AuthenticationTicket( - new ClaimsPrincipal(new ClaimsIdentity(claims, context.Options.AuthenticationScheme)), - new AuthenticationProperties(), context.Options.AuthenticationScheme); - - context.HandleResponse(); - - return Task.FromResult(null); - } - } - }); - - var response = await SendAsync(server, "http://example.com/oauth", "someHeader someblob"); - Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); - Assert.Equal("Bob le Magnifique", response.ResponseText); - } - - [Fact] - public async Task NoHeaderReceived() - { - var server = CreateServer(new JwtBearerOptions()); - var response = await SendAsync(server, "http://example.com/oauth"); - Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); - } - - [Fact] - public async Task HeaderWithoutBearerReceived() - { - var server = CreateServer(new JwtBearerOptions()); - var response = await SendAsync(server, "http://example.com/oauth","Token"); - Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); - } - - [Fact] - public async Task UnrecognizedTokenReceived() - { - var server = CreateServer(new JwtBearerOptions - { - AutomaticAuthenticate = true - }); - - var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); - Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); - Assert.Equal("", response.ResponseText); - } - - [Fact] - public async Task InvalidTokenReceived() - { - var options = new JwtBearerOptions - { - AutomaticAuthenticate = true - }; - options.SecurityTokenValidators.Clear(); - options.SecurityTokenValidators.Add(new InvalidTokenValidator()); - var server = CreateServer(options); - - var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); - Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); - Assert.Equal("", response.ResponseText); - } - - [Fact] - public async Task CustomTokenValidated() - { - var options = new JwtBearerOptions - { - AutomaticAuthenticate = true, - Events = new JwtBearerEvents() - { - OnTokenValidated = context => - { - // Retrieve the NameIdentifier claim from the identity - // returned by the custom security token validator. - var identity = (ClaimsIdentity)context.Ticket.Principal.Identity; - var identifier = identity.FindFirst(ClaimTypes.NameIdentifier); - - Assert.Equal("Bob le Tout Puissant", identifier.Value); - - // Remove the existing NameIdentifier claim and replace it - // with a new one containing a different value. - identity.RemoveClaim(identifier); - // Make sure to use a different name identifier - // than the one defined by BlobTokenValidator. - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique")); - - return Task.FromResult(null); - } - } - }; - options.SecurityTokenValidators.Clear(); - options.SecurityTokenValidators.Add(new BlobTokenValidator(options.AuthenticationScheme)); - var server = CreateServer(options); - - var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); - Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); - Assert.Equal("Bob le Magnifique", response.ResponseText); - } - - [Fact] - public async Task RetrievingTokenFromAlternateLocation() - { - var options = new JwtBearerOptions() - { - AutomaticAuthenticate = true, - Events = new JwtBearerEvents() - { - OnMessageReceived = context => - { - context.Token = "CustomToken"; - return Task.FromResult(null); - } - } - }; - options.SecurityTokenValidators.Clear(); - options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT", token => - { - Assert.Equal("CustomToken", token); - })); - var server = CreateServer(options); - - var response = await SendAsync(server, "http://example.com/oauth", "Bearer Token"); - Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); - Assert.Equal("Bob le Tout Puissant", response.ResponseText); - } - - [Fact] - public async Task BearerTurns401To403IfAuthenticated() - { - var options = new JwtBearerOptions(); - options.SecurityTokenValidators.Clear(); - options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); - var server = CreateServer(options); - - var response = await SendAsync(server, "http://example.com/unauthorized", "Bearer Token"); - Assert.Equal(HttpStatusCode.Forbidden, response.Response.StatusCode); - } - - [Fact] - public async Task BearerDoesNothingTo401IfNotAuthenticated() - { - var server = CreateServer(new JwtBearerOptions()); - - var response = await SendAsync(server, "http://example.com/unauthorized"); - Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); - } - - [Fact] - public async Task EventOnMessageReceivedSkipped_NoMoreEventsExecuted() - { - var server = CreateServer(new JwtBearerOptions - { - AutomaticAuthenticate = true, - Events = new JwtBearerEvents() - { - OnMessageReceived = context => - { - context.SkipToNextMiddleware(); - return Task.FromResult(0); - }, - OnTokenValidated = context => - { - throw new NotImplementedException(); - }, - OnAuthenticationFailed = context => - { - throw new NotImplementedException(context.Exception.ToString()); - }, - OnChallenge = context => - { - throw new NotImplementedException(); - }, - } - }); - - var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); - Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); - Assert.Equal(string.Empty, response.ResponseText); - } - - [Fact] - public async Task EventOnTokenValidatedSkipped_NoMoreEventsExecuted() - { - var options = new JwtBearerOptions - { - AutomaticAuthenticate = true, - Events = new JwtBearerEvents() - { - OnTokenValidated = context => - { - context.SkipToNextMiddleware(); - return Task.FromResult(0); - }, - OnAuthenticationFailed = context => - { - throw new NotImplementedException(context.Exception.ToString()); - }, - OnChallenge = context => - { - throw new NotImplementedException(); - }, - } - }; - options.SecurityTokenValidators.Clear(); - options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); - var server = CreateServer(options); - - var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); - Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); - Assert.Equal(string.Empty, response.ResponseText); - } - - [Fact] - public async Task EventOnAuthenticationFailedSkipped_NoMoreEventsExecuted() - { - var options = new JwtBearerOptions - { - AutomaticAuthenticate = true, - Events = new JwtBearerEvents() - { - OnTokenValidated = context => - { - throw new Exception("Test Exception"); - }, - OnAuthenticationFailed = context => - { - context.SkipToNextMiddleware(); - return Task.FromResult(0); - }, - OnChallenge = context => - { - throw new NotImplementedException(); - }, - } - }; - options.SecurityTokenValidators.Clear(); - options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); - var server = CreateServer(options); - - var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); - Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); - Assert.Equal(string.Empty, response.ResponseText); - } - - [Fact] - public async Task EventOnChallengeSkipped_ResponseNotModified() - { - var server = CreateServer(new JwtBearerOptions - { - AutomaticAuthenticate = true, - AutomaticChallenge = true, - Events = new JwtBearerEvents() - { - OnChallenge = context => - { - context.SkipToNextMiddleware(); - return Task.FromResult(0); - }, - } - }); - - var response = await SendAsync(server, "http://example.com/unauthorized", "Bearer Token"); - Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); - Assert.Empty(response.Response.Headers.WwwAuthenticate); - Assert.Equal(string.Empty, response.ResponseText); - } - - class InvalidTokenValidator : ISecurityTokenValidator - { - public InvalidTokenValidator() - { - } - - public bool CanValidateToken => true; - - public int MaximumTokenSizeInBytes - { - get { throw new NotImplementedException(); } - set { throw new NotImplementedException(); } - } - - public bool CanReadToken(string securityToken) => true; - - public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) - { - throw new SecurityTokenException("InvalidToken"); - } - } - - class BlobTokenValidator : ISecurityTokenValidator - { - private Action _tokenValidator; - - public BlobTokenValidator(string authenticationScheme) - { - AuthenticationScheme = authenticationScheme; - - } - public BlobTokenValidator(string authenticationScheme, Action tokenValidator) - { - AuthenticationScheme = authenticationScheme; - _tokenValidator = tokenValidator; - } - - public string AuthenticationScheme { get; } - - public bool CanValidateToken => true; - - public int MaximumTokenSizeInBytes - { - get - { - throw new NotImplementedException(); - } - set - { - throw new NotImplementedException(); - } - } - - public bool CanReadToken(string securityToken) => true; - - public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) - { - validatedToken = null; - if (_tokenValidator != null) - { - _tokenValidator(securityToken); - } - - var claims = new[] - { - // Make sure to use a different name identifier - // than the one defined by CustomTokenValidated. - new Claim(ClaimTypes.NameIdentifier, "Bob le Tout Puissant"), - new Claim(ClaimTypes.Email, "bob@contoso.com"), - new Claim(ClaimsIdentity.DefaultNameClaimType, "bob"), - }; - - return new ClaimsPrincipal(new ClaimsIdentity(claims, AuthenticationScheme)); - } - } - - private static TestServer CreateServer(JwtBearerOptions options, Func handler = null) - { - var builder = new WebHostBuilder() - .Configure(app => - { - if (options != null) - { - app.UseJwtBearerAuthentication(options); - } - - app.Use(async (context, next) => - { - if (context.Request.Path == new PathString("/checkforerrors")) - { - var authContext = new AuthenticateContext(Http.Authentication.AuthenticationManager.AutomaticScheme); - await context.Authentication.AuthenticateAsync(authContext); - if (authContext.Error != null) - { - throw new Exception("Failed to authenticate", authContext.Error); - } - return; - } - else if (context.Request.Path == new PathString("/oauth")) - { - if (context.User == null || - context.User.Identity == null || - !context.User.Identity.IsAuthenticated) - { - context.Response.StatusCode = 401; - - return; - } - - var identifier = context.User.FindFirst(ClaimTypes.NameIdentifier); - if (identifier == null) - { - context.Response.StatusCode = 500; - - return; - } - - await context.Response.WriteAsync(identifier.Value); - } - else if (context.Request.Path == new PathString("/unauthorized")) - { - // Simulate Authorization failure - var result = await context.Authentication.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); - await context.Authentication.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme); - } - else if (context.Request.Path == new PathString("/signIn")) - { - await Assert.ThrowsAsync(() => context.Authentication.SignInAsync(JwtBearerDefaults.AuthenticationScheme, new ClaimsPrincipal())); - } - else if (context.Request.Path == new PathString("/signOut")) - { - await Assert.ThrowsAsync(() => context.Authentication.SignOutAsync(JwtBearerDefaults.AuthenticationScheme)); - } - else - { - await next(); - } - }); - }) - .ConfigureServices(services => services.AddAuthentication()); - return new TestServer(builder); - } - - // TODO: see if we can share the TestExtensions SendAsync method (only diff is auth header) - private static async Task SendAsync(TestServer server, string uri, string authorizationHeader = null) - { - var request = new HttpRequestMessage(HttpMethod.Get, uri); - if (!string.IsNullOrEmpty(authorizationHeader)) - { - request.Headers.Add("Authorization", authorizationHeader); - } - - var transaction = new Transaction - { - Request = request, - Response = await server.CreateClient().SendAsync(request), - }; - - transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); - - if (transaction.Response.Content != null && - transaction.Response.Content.Headers.ContentType != null && - transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") - { - transaction.ResponseElement = XElement.Parse(transaction.ResponseText); - } - - return transaction; - } - } -} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs new file mode 100644 index 000000000..ad77803cd --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs @@ -0,0 +1,845 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.JwtBearer +{ + public class JwtBearerTests : SharedAuthenticationTests + { + protected override string DefaultScheme => JwtBearerDefaults.AuthenticationScheme; + protected override Type HandlerType => typeof(JwtBearerHandler); + protected override bool SupportsSignIn { get => false; } + protected override bool SupportsSignOut { get => false; } + + protected override void RegisterAuth(AuthenticationBuilder services, Action configure) + { + services.AddJwtBearer(o => + { + ConfigureDefaults(o); + configure.Invoke(o); + }); + } + + private void ConfigureDefaults(JwtBearerOptions o) + { + } + + [Fact] + public async Task BearerTokenValidation() + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128))); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob") + }; + + var token = new JwtSecurityToken( + issuer: "issuer.contoso.com", + audience: "audience.contoso.com", + claims: claims, + expires: DateTime.Now.AddMinutes(30), + signingCredentials: creds); + + var tokenText = new JwtSecurityTokenHandler().WriteToken(token); + + var server = CreateServer(o => + { + o.TokenValidationParameters = new TokenValidationParameters() + { + ValidIssuer = "issuer.contoso.com", + ValidAudience = "audience.contoso.com", + IssuerSigningKey = key, + }; + }); + + var newBearerToken = "Bearer " + tokenText; + var response = await SendAsync(server, "http://example.com/oauth", newBearerToken); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + } + + [Fact] + public async Task SaveBearerToken() + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('a', 128))); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob") + }; + + var token = new JwtSecurityToken( + issuer: "issuer.contoso.com", + audience: "audience.contoso.com", + claims: claims, + expires: DateTime.Now.AddMinutes(30), + signingCredentials: creds); + + var tokenText = new JwtSecurityTokenHandler().WriteToken(token); + + var server = CreateServer(o => + { + o.SaveToken = true; + o.TokenValidationParameters = new TokenValidationParameters() + { + ValidIssuer = "issuer.contoso.com", + ValidAudience = "audience.contoso.com", + IssuerSigningKey = key, + }; + }); + + var newBearerToken = "Bearer " + tokenText; + var response = await SendAsync(server, "http://example.com/token", newBearerToken); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(tokenText, await response.Response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task SignInThrows() + { + var server = CreateServer(); + var transaction = await server.SendAsync("https://example.com/signIn"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task SignOutThrows() + { + var server = CreateServer(); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ThrowAtAuthenticationFailedEvent() + { + var server = CreateServer(o => + { + o.Events = new JwtBearerEvents + { + OnAuthenticationFailed = context => + { + context.Response.StatusCode = 401; + throw new Exception(); + }, + OnMessageReceived = context => + { + context.Token = "something"; + return Task.FromResult(0); + } + }; + o.SecurityTokenValidators.Clear(); + o.SecurityTokenValidators.Insert(0, new InvalidTokenValidator()); + }, + async (context, next) => + { + try + { + await next(); + Assert.False(true, "Expected exception is not thrown"); + } + catch (Exception) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("i got this"); + } + }); + + var transaction = await server.SendAsync("https://example.com/signIn"); + + Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode); + } + + [Fact] + public async Task CustomHeaderReceived() + { + var server = CreateServer(o => + { + o.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob") + }; + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); + context.Success(); + + return Task.FromResult(null); + } + }; + }); + + var response = await SendAsync(server, "http://example.com/oauth", "someHeader someblob"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal("Bob le Magnifique", response.ResponseText); + } + + [Fact] + public async Task NoHeaderReceived() + { + var server = CreateServer(); + var response = await SendAsync(server, "http://example.com/oauth"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + } + + [Fact] + public async Task HeaderWithoutBearerReceived() + { + var server = CreateServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Token"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + } + + [Fact] + public async Task UnrecognizedTokenReceived() + { + var server = CreateServer(); + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("", response.ResponseText); + } + + [Fact] + public async Task InvalidTokenReceived() + { + var server = CreateServer(options => + { + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new InvalidTokenValidator()); + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer error=\"invalid_token\"", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Theory] + [InlineData(typeof(SecurityTokenInvalidAudienceException), "The audience is invalid")] + [InlineData(typeof(SecurityTokenInvalidIssuerException), "The issuer is invalid")] + [InlineData(typeof(SecurityTokenNoExpirationException), "The token has no expiration")] + [InlineData(typeof(SecurityTokenInvalidLifetimeException), "The token lifetime is invalid")] + [InlineData(typeof(SecurityTokenNotYetValidException), "The token is not valid yet")] + [InlineData(typeof(SecurityTokenExpiredException), "The token is expired")] + [InlineData(typeof(SecurityTokenInvalidSignatureException), "The signature is invalid")] + [InlineData(typeof(SecurityTokenSignatureKeyNotFoundException), "The signature key was not found")] + public async Task ExceptionReportedInHeaderForAuthenticationFailures(Type errorType, string message) + { + var server = CreateServer(options => + { + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new InvalidTokenValidator(errorType)); + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal($"Bearer error=\"invalid_token\", error_description=\"{message}\"", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Theory] + [InlineData(typeof(ArgumentException))] + public async Task ExceptionNotReportedInHeaderForOtherFailures(Type errorType) + { + var server = CreateServer(options => + { + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new InvalidTokenValidator(errorType)); + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer error=\"invalid_token\"", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Fact] + public async Task ExceptionsReportedInHeaderForMultipleAuthenticationFailures() + { + var server = CreateServer(options => + { + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new InvalidTokenValidator(typeof(SecurityTokenInvalidAudienceException))); + options.SecurityTokenValidators.Add(new InvalidTokenValidator(typeof(SecurityTokenSignatureKeyNotFoundException))); + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer error=\"invalid_token\", error_description=\"The audience is invalid; The signature key was not found\"", + response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Theory] + [InlineData("custom_error", "custom_description", "custom_uri")] + [InlineData("custom_error", "custom_description", null)] + [InlineData("custom_error", null, null)] + [InlineData(null, "custom_description", "custom_uri")] + [InlineData(null, "custom_description", null)] + [InlineData(null, null, "custom_uri")] + public async Task ExceptionsReportedInHeaderExposesUserDefinedError(string error, string description, string uri) + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents + { + OnChallenge = context => + { + context.Error = error; + context.ErrorDescription = description; + context.ErrorUri = uri; + + return Task.FromResult(0); + } + }; + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("", response.ResponseText); + + var builder = new StringBuilder(JwtBearerDefaults.AuthenticationScheme); + + if (!string.IsNullOrEmpty(error)) + { + builder.Append(" error=\""); + builder.Append(error); + builder.Append("\""); + } + if (!string.IsNullOrEmpty(description)) + { + if (!string.IsNullOrEmpty(error)) + { + builder.Append(","); + } + + builder.Append(" error_description=\""); + builder.Append(description); + builder.Append('\"'); + } + if (!string.IsNullOrEmpty(uri)) + { + if (!string.IsNullOrEmpty(error) || + !string.IsNullOrEmpty(description)) + { + builder.Append(","); + } + + builder.Append(" error_uri=\""); + builder.Append(uri); + builder.Append('\"'); + } + + Assert.Equal(builder.ToString(), response.Response.Headers.WwwAuthenticate.First().ToString()); + } + + [Fact] + public async Task ExceptionNotReportedInHeaderWhenIncludeErrorDetailsIsFalse() + { + var server = CreateServer(o => + { + o.IncludeErrorDetails = false; + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Fact] + public async Task ExceptionNotReportedInHeaderWhenTokenWasMissing() + { + var server = CreateServer(); + + var response = await SendAsync(server, "http://example.com/oauth"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + + [Fact] + public async Task CustomTokenValidated() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + // Retrieve the NameIdentifier claim from the identity + // returned by the custom security token validator. + var identity = (ClaimsIdentity)context.Principal.Identity; + var identifier = identity.FindFirst(ClaimTypes.NameIdentifier); + + Assert.Equal("Bob le Tout Puissant", identifier.Value); + + // Remove the existing NameIdentifier claim and replace it + // with a new one containing a different value. + identity.RemoveClaim(identifier); + // Make sure to use a different name identifier + // than the one defined by BlobTokenValidator. + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique")); + + return Task.FromResult(null); + } + }; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator(JwtBearerDefaults.AuthenticationScheme)); + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal("Bob le Magnifique", response.ResponseText); + } + + [Fact] + public async Task RetrievingTokenFromAlternateLocation() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + context.Token = "CustomToken"; + return Task.FromResult(null); + } + }; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT", token => + { + Assert.Equal("CustomToken", token); + })); + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal("Bob le Tout Puissant", response.ResponseText); + } + + [Fact] + public async Task EventOnMessageReceivedSkip_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + context.NoResult(); + return Task.FromResult(0); + }, + OnTokenValidated = context => + { + throw new NotImplementedException(); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + }); + + var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnMessageReceivedReject_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + context.Fail("Authentication was aborted from user code."); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }, + OnTokenValidated = context => + { + throw new NotImplementedException(); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + }); + + var exception = await Assert.ThrowsAsync(delegate + { + return SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + } + + [Fact] + public async Task EventOnTokenValidatedSkip_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + context.NoResult(); + return Task.FromResult(0); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); + }); + + var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnTokenValidatedReject_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + context.Fail("Authentication was aborted from user code."); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); + }); + + var exception = await Assert.ThrowsAsync(delegate + { + return SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + } + + [Fact] + public async Task EventOnAuthenticationFailedSkip_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + throw new Exception("Test Exception"); + }, + OnAuthenticationFailed = context => + { + context.NoResult(); + return Task.FromResult(0); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); + }); + + var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnAuthenticationFailedReject_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.Events = new JwtBearerEvents() + { + OnTokenValidated = context => + { + throw new Exception("Test Exception"); + }, + OnAuthenticationFailed = context => + { + context.Fail("Authentication was aborted from user code."); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); + }); + + var exception = await Assert.ThrowsAsync(delegate + { + return SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + } + + [Fact] + public async Task EventOnChallengeSkip_ResponseNotModified() + { + var server = CreateServer(o => + { + o.Events = new JwtBearerEvents() + { + OnChallenge = context => + { + context.HandleResponse(); + return Task.FromResult(0); + }, + }; + }); + + var response = await SendAsync(server, "http://example.com/unauthorized", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Empty(response.Response.Headers.WwwAuthenticate); + Assert.Equal(string.Empty, response.ResponseText); + } + + class InvalidTokenValidator : ISecurityTokenValidator + { + public InvalidTokenValidator() + { + ExceptionType = typeof(SecurityTokenException); + } + + public InvalidTokenValidator(Type exceptionType) + { + ExceptionType = exceptionType; + } + + public Type ExceptionType { get; set; } + + public bool CanValidateToken => true; + + public int MaximumTokenSizeInBytes + { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + public bool CanReadToken(string securityToken) => true; + + public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) + { + var constructor = ExceptionType.GetTypeInfo().GetConstructor(new[] { typeof(string) }); + var exception = (Exception)constructor.Invoke(new[] { ExceptionType.Name }); + throw exception; + } + } + + class BlobTokenValidator : ISecurityTokenValidator + { + private Action _tokenValidator; + + public BlobTokenValidator(string authenticationScheme) + { + AuthenticationScheme = authenticationScheme; + + } + public BlobTokenValidator(string authenticationScheme, Action tokenValidator) + { + AuthenticationScheme = authenticationScheme; + _tokenValidator = tokenValidator; + } + + public string AuthenticationScheme { get; } + + public bool CanValidateToken => true; + + public int MaximumTokenSizeInBytes + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public bool CanReadToken(string securityToken) => true; + + public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) + { + validatedToken = null; + _tokenValidator?.Invoke(securityToken); + + var claims = new[] + { + // Make sure to use a different name identifier + // than the one defined by CustomTokenValidated. + new Claim(ClaimTypes.NameIdentifier, "Bob le Tout Puissant"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob"), + }; + + return new ClaimsPrincipal(new ClaimsIdentity(claims, AuthenticationScheme)); + } + } + + private static TestServer CreateServer(Action options = null, Func, Task> handlerBeforeAuth = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + if (handlerBeforeAuth != null) + { + app.Use(handlerBeforeAuth); + } + + app.UseAuthentication(); + app.Use(async (context, next) => + { + if (context.Request.Path == new PathString("/checkforerrors")) + { + var result = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); // this used to be "Automatic" + if (result.Failure != null) + { + throw new Exception("Failed to authenticate", result.Failure); + } + return; + } + else if (context.Request.Path == new PathString("/oauth")) + { + if (context.User == null || + context.User.Identity == null || + !context.User.Identity.IsAuthenticated) + { + context.Response.StatusCode = 401; + // REVIEW: no more automatic challenge + await context.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme); + return; + } + + var identifier = context.User.FindFirst(ClaimTypes.NameIdentifier); + if (identifier == null) + { + context.Response.StatusCode = 500; + return; + } + + await context.Response.WriteAsync(identifier.Value); + } + else if (context.Request.Path == new PathString("/token")) + { + var token = await context.GetTokenAsync("access_token"); + await context.Response.WriteAsync(token); + } + else if (context.Request.Path == new PathString("/unauthorized")) + { + // Simulate Authorization failure + var result = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); + await context.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme); + } + else if (context.Request.Path == new PathString("/signIn")) + { + await Assert.ThrowsAsync(() => context.SignInAsync(JwtBearerDefaults.AuthenticationScheme, new ClaimsPrincipal())); + } + else if (context.Request.Path == new PathString("/signOut")) + { + await Assert.ThrowsAsync(() => context.SignOutAsync(JwtBearerDefaults.AuthenticationScheme)); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options)); + + return new TestServer(builder); + } + + // TODO: see if we can share the TestExtensions SendAsync method (only diff is auth header) + private static async Task SendAsync(TestServer server, string uri, string authorizationHeader = null) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(authorizationHeader)) + { + request.Headers.Add("Authorization", authorizationHeader); + } + + var transaction = new Transaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + + return transaction; + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj b/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj new file mode 100644 index 000000000..c7a380868 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj @@ -0,0 +1,47 @@ + + + + netcoreapp3.0 + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.xproj b/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.xproj deleted file mode 100644 index aec10ee93..000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.xproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 8da26cd1-1302-4cfd-9270-9fa1b7c6138b - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - - - - \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs deleted file mode 100644 index 0ddfb5a3c..000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Security.Claims; -using System.Text; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication.OAuth; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using Xunit; - -namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount -{ - public class MicrosoftAccountMiddlewareTests - { - [Fact] - public async Task ChallengeWillTriggerApplyRedirectEvent() - { - var server = CreateServer(new MicrosoftAccountOptions - { - ClientId = "Test Client Id", - ClientSecret = "Test Client Secret", - Events = new OAuthEvents - { - OnRedirectToAuthorizationEndpoint = context => - { - context.Response.Redirect(context.RedirectUri + "&custom=test"); - return Task.FromResult(0); - } - } - }); - var transaction = await server.SendAsync("http://example.com/challenge"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var query = transaction.Response.Headers.Location.Query; - Assert.Contains("custom=test", query); - } - - [Fact] - public async Task SignInThrows() - { - var server = CreateServer(new MicrosoftAccountOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret" - }); - var transaction = await server.SendAsync("https://example.com/signIn"); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - } - - [Fact] - public async Task SignOutThrows() - { - var server = CreateServer(new MicrosoftAccountOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret" - }); - var transaction = await server.SendAsync("https://example.com/signOut"); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - } - - [Fact] - public async Task ForbidThrows() - { - var server = CreateServer(new MicrosoftAccountOptions - { - ClientId = "Test Id", - ClientSecret = "Test Secret" - }); - var transaction = await server.SendAsync("https://example.com/signOut"); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - } - - [Fact] - public async Task ChallengeWillTriggerRedirection() - { - var server = CreateServer(new MicrosoftAccountOptions - { - ClientId = "Test Client Id", - ClientSecret = "Test Client Secret" - }); - var transaction = await server.SendAsync("http://example.com/challenge"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var location = transaction.Response.Headers.Location.AbsoluteUri; - Assert.Contains("https://login.microsoftonline.com/common/oauth2/v2.0/authorize", location); - Assert.Contains("response_type=code", location); - Assert.Contains("client_id=", location); - Assert.Contains("redirect_uri=", location); - Assert.Contains("scope=", location); - Assert.Contains("state=", location); - } - - [Fact] - public async Task AuthenticatedEventCanGetRefreshToken() - { - var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("MsftTest")); - var server = CreateServer(new MicrosoftAccountOptions - { - ClientId = "Test Client Id", - ClientSecret = "Test Client Secret", - StateDataFormat = stateFormat, - BackchannelHttpHandler = new TestHttpMessageHandler - { - Sender = req => - { - if (req.RequestUri.AbsoluteUri == "https://login.microsoftonline.com/common/oauth2/v2.0/token") - { - return ReturnJsonResponse(new - { - access_token = "Test Access Token", - expire_in = 3600, - token_type = "Bearer", - refresh_token = "Test Refresh Token" - }); - } - else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://graph.microsoft.com/v1.0/me") - { - return ReturnJsonResponse(new - { - id = "Test User ID", - displayName = "Test Name", - givenName = "Test Given Name", - surname = "Test Family Name", - mail = "Test email" - }); - } - - return null; - } - }, - Events = new OAuthEvents - { - OnCreatingTicket = context => - { - var refreshToken = context.RefreshToken; - context.Ticket.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "Microsoft") }, "Microsoft")); - return Task.FromResult(null); - } - } - }); - var properties = new AuthenticationProperties(); - var correlationKey = ".xsrf"; - var correlationValue = "TestCorrelationId"; - properties.Items.Add(correlationKey, correlationValue); - properties.RedirectUri = "/me"; - var state = stateFormat.Protect(properties); - var transaction = await server.SendAsync( - "https://example.com/signin-microsoft?code=TestCode&state=" + UrlEncoder.Default.Encode(state), - $".AspNetCore.Correlation.Microsoft.{correlationValue}=N"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); - Assert.Equal(2, transaction.SetCookie.Count); - Assert.Contains($".AspNetCore.Correlation.Microsoft.{correlationValue}", transaction.SetCookie[0]); - Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); - - var authCookie = transaction.AuthenticationCookieValue; - transaction = await server.SendAsync("https://example.com/me", authCookie); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - Assert.Equal("Test Refresh Token", transaction.FindClaimValue("RefreshToken")); - } - - private static TestServer CreateServer(MicrosoftAccountOptions options) - { - var builder = new WebHostBuilder() - .Configure(app => - { - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AuthenticationScheme = TestExtensions.CookieAuthenticationScheme, - AutomaticAuthenticate = true - }); - app.UseMicrosoftAccountAuthentication(options); - - app.Use(async (context, next) => - { - var req = context.Request; - var res = context.Response; - if (req.Path == new PathString("/challenge")) - { - await context.Authentication.ChallengeAsync("Microsoft"); - } - else if (req.Path == new PathString("/me")) - { - res.Describe(context.User); - } - else if (req.Path == new PathString("/signIn")) - { - await Assert.ThrowsAsync(() => context.Authentication.SignInAsync("Microsoft", new ClaimsPrincipal())); - } - else if (req.Path == new PathString("/signOut")) - { - await Assert.ThrowsAsync(() => context.Authentication.SignOutAsync("Microsoft")); - } - else if (req.Path == new PathString("/forbid")) - { - await Assert.ThrowsAsync(() => context.Authentication.ForbidAsync("Microsoft")); - } - else - { - await next(); - } - }); - }) - .ConfigureServices(services => - { - services.AddAuthentication(); - services.Configure(authOptions => - { - authOptions.SignInScheme = TestExtensions.CookieAuthenticationScheme; - }); - }); - return new TestServer(builder); - } - - private static HttpResponseMessage ReturnJsonResponse(object content) - { - var res = new HttpResponseMessage(HttpStatusCode.OK); - var text = JsonConvert.SerializeObject(content); - res.Content = new StringContent(text, Encoding.UTF8, "application/json"); - return res; - } - } -} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs new file mode 100644 index 000000000..26a5484c8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs @@ -0,0 +1,310 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authentication.MicrosoftAccount; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json; +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount +{ + public class MicrosoftAccountTests : RemoteAuthenticationTests + { + protected override string DefaultScheme => MicrosoftAccountDefaults.AuthenticationScheme; + protected override Type HandlerType => typeof(MicrosoftAccountHandler); + protected override bool SupportsSignIn { get => false; } + protected override bool SupportsSignOut { get => false; } + + protected override void RegisterAuth(AuthenticationBuilder services, Action configure) + { + services.AddMicrosoftAccount(o => + { + ConfigureDefaults(o); + configure.Invoke(o); + }); + } + + protected override void ConfigureDefaults(MicrosoftAccountOptions o) + { + o.ClientId = "whatever"; + o.ClientSecret = "whatever"; + o.SignInScheme = "auth1"; + } + + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var server = CreateServer(o => + { + o.ClientId = "Test Client Id"; + o.ClientSecret = "Test Client Secret"; + o.Events = new OAuthEvents + { + OnRedirectToAuthorizationEndpoint = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + return Task.FromResult(0); + } + }; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var query = transaction.Response.Headers.Location.Query; + Assert.Contains("custom=test", query); + } + + [Fact] + public async Task SignInThrows() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signIn"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task SignOutThrows() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ForbidThrows() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://login.microsoftonline.com/common/oauth2/v2.0/authorize", location); + Assert.Contains("response_type=code", location); + Assert.Contains("client_id=", location); + Assert.Contains("redirect_uri=", location); + Assert.Contains("scope=", location); + Assert.Contains("state=", location); + } + + [Fact] + public async Task ChallengeWillIncludeScopeAsConfigured() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.Scope.Clear(); + o.Scope.Add("foo"); + o.Scope.Add("bar"); + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=foo%20bar", res.Headers.Location.Query); + } + + [Fact] + public async Task ChallengeWillIncludeScopeAsOverwritten() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.Scope.Clear(); + o.Scope.Add("foo"); + o.Scope.Add("bar"); + }); + var transaction = await server.SendAsync("http://example.com/challengeWithOtherScope"); + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=baz%20qux", res.Headers.Location.Query); + } + + [Fact] + public async Task ChallengeWillIncludeScopeAsOverwrittenWithBaseAuthenticationProperties() + { + var server = CreateServer(o => + { + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.Scope.Clear(); + o.Scope.Add("foo"); + o.Scope.Add("bar"); + }); + var transaction = await server.SendAsync("http://example.com/challengeWithOtherScopeWithBaseAuthenticationProperties"); + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=baz%20qux", res.Headers.Location.Query); + } + + [Fact] + public async Task AuthenticatedEventCanGetRefreshToken() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("MsftTest")); + var server = CreateServer(o => + { + o.ClientId = "Test Client Id"; + o.ClientSecret = "Test Client Secret"; + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://login.microsoftonline.com/common/oauth2/v2.0/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expire_in = 3600, + token_type = "Bearer", + refresh_token = "Test Refresh Token" + }); + } + else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://graph.microsoft.com/v1.0/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + givenName = "Test Given Name", + surname = "Test Family Name", + mail = "Test email" + }); + } + + return null; + } + }; + o.Events = new OAuthEvents + { + OnCreatingTicket = context => + { + var refreshToken = context.RefreshToken; + context.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "Microsoft") }, "Microsoft")); + return Task.FromResult(null); + } + }; + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-microsoft?code=TestCode&state=" + UrlEncoder.Default.Encode(state), + $".AspNetCore.Correlation.Microsoft.{correlationValue}=N"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal(2, transaction.SetCookie.Count); + Assert.Contains($".AspNetCore.Correlation.Microsoft.{correlationValue}", transaction.SetCookie[0]); + Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/me", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Refresh Token", transaction.FindClaimValue("RefreshToken")); + } + + private static TestServer CreateServer(Action configureOptions) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge")) + { + await context.ChallengeAsync("Microsoft"); + } + else if (req.Path == new PathString("/challengeWithOtherScope")) + { + var properties = new OAuthChallengeProperties(); + properties.SetScope("baz", "qux"); + await context.ChallengeAsync("Microsoft", properties); + } + else if (req.Path == new PathString("/challengeWithOtherScopeWithBaseAuthenticationProperties")) + { + var properties = new AuthenticationProperties(); + properties.SetParameter(OAuthChallengeProperties.ScopeKey, new string[] { "baz", "qux" }); + await context.ChallengeAsync("Microsoft", properties); + } + else if (req.Path == new PathString("/me")) + { + res.Describe(context.User); + } + else if (req.Path == new PathString("/signIn")) + { + await Assert.ThrowsAsync(() => context.SignInAsync("Microsoft", new ClaimsPrincipal())); + } + else if (req.Path == new PathString("/signOut")) + { + await Assert.ThrowsAsync(() => context.SignOutAsync("Microsoft")); + } + else if (req.Path == new PathString("/forbid")) + { + await Assert.ThrowsAsync(() => context.ForbidAsync("Microsoft")); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + services.AddAuthentication(TestExtensions.CookieAuthenticationScheme) + .AddCookie(TestExtensions.CookieAuthenticationScheme, o => { }) + .AddMicrosoftAccount(configureOptions); + }); + return new TestServer(builder); + } + + private static HttpResponseMessage ReturnJsonResponse(object content) + { + var res = new HttpResponseMessage(HttpStatusCode.OK); + var text = JsonConvert.SerializeObject(content); + res.Content = new StringContent(text, Encoding.UTF8, "application/json"); + return res; + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OAuthChallengePropertiesTest.cs b/test/Microsoft.AspNetCore.Authentication.Test/OAuthChallengePropertiesTest.cs new file mode 100644 index 000000000..c359bb0e8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OAuthChallengePropertiesTest.cs @@ -0,0 +1,149 @@ +using System; +using Microsoft.AspNetCore.Authentication.Google; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Test +{ + public class OAuthChallengePropertiesTest + { + [Fact] + public void ScopeProperty() + { + var properties = new OAuthChallengeProperties + { + Scope = new string[] { "foo", "bar" } + }; + Assert.Equal(new string[] { "foo", "bar" }, properties.Scope); + Assert.Equal(new string[] { "foo", "bar" }, properties.Parameters["scope"]); + } + + [Fact] + public void ScopeProperty_NullValue() + { + var properties = new OAuthChallengeProperties(); + properties.Parameters["scope"] = new string[] { "foo", "bar" }; + Assert.Equal(new string[] { "foo", "bar" }, properties.Scope); + + properties.Scope = null; + Assert.Null(properties.Scope); + } + + [Fact] + public void SetScope() + { + var properties = new OAuthChallengeProperties(); + properties.SetScope("foo", "bar"); + Assert.Equal(new string[] { "foo", "bar" }, properties.Scope); + Assert.Equal(new string[] { "foo", "bar" }, properties.Parameters["scope"]); + } + + [Fact] + public void OidcMaxAge() + { + var properties = new OpenIdConnectChallengeProperties() + { + MaxAge = TimeSpan.FromSeconds(200) + }; + Assert.Equal(TimeSpan.FromSeconds(200), properties.MaxAge); + } + + [Fact] + public void OidcMaxAge_NullValue() + { + var properties = new OpenIdConnectChallengeProperties(); + properties.Parameters["max_age"] = TimeSpan.FromSeconds(500); + Assert.Equal(TimeSpan.FromSeconds(500), properties.MaxAge); + + properties.MaxAge = null; + Assert.Null(properties.MaxAge); + } + + [Fact] + public void OidcPrompt() + { + var properties = new OpenIdConnectChallengeProperties() + { + Prompt = "login" + }; + Assert.Equal("login", properties.Prompt); + Assert.Equal("login", properties.Parameters["prompt"]); + } + + [Fact] + public void OidcPrompt_NullValue() + { + var properties = new OpenIdConnectChallengeProperties(); + properties.Parameters["prompt"] = "consent"; + Assert.Equal("consent", properties.Prompt); + + properties.Prompt = null; + Assert.Null(properties.Prompt); + } + + [Fact] + public void GoogleProperties() + { + var properties = new GoogleChallengeProperties() + { + AccessType = "offline", + ApprovalPrompt = "force", + LoginHint = "test@example.com", + Prompt = "login", + }; + Assert.Equal("offline", properties.AccessType); + Assert.Equal("offline", properties.Parameters["access_type"]); + Assert.Equal("force", properties.ApprovalPrompt); + Assert.Equal("force", properties.Parameters["approval_prompt"]); + Assert.Equal("test@example.com", properties.LoginHint); + Assert.Equal("test@example.com", properties.Parameters["login_hint"]); + Assert.Equal("login", properties.Prompt); + Assert.Equal("login", properties.Parameters["prompt"]); + } + + [Fact] + public void GoogleProperties_NullValues() + { + var properties = new GoogleChallengeProperties(); + properties.Parameters["access_type"] = "offline"; + properties.Parameters["approval_prompt"] = "force"; + properties.Parameters["login_hint"] = "test@example.com"; + properties.Parameters["prompt"] = "login"; + Assert.Equal("offline", properties.AccessType); + Assert.Equal("force", properties.ApprovalPrompt); + Assert.Equal("test@example.com", properties.LoginHint); + Assert.Equal("login", properties.Prompt); + + properties.AccessType = null; + Assert.Null(properties.AccessType); + + properties.ApprovalPrompt = null; + Assert.Null(properties.ApprovalPrompt); + + properties.LoginHint = null; + Assert.Null(properties.LoginHint); + + properties.Prompt = null; + Assert.Null(properties.Prompt); + } + + [Fact] + public void GoogleIncludeGrantedScopes() + { + var properties = new GoogleChallengeProperties() + { + IncludeGrantedScopes = true + }; + Assert.True(properties.IncludeGrantedScopes); + Assert.Equal(true, properties.Parameters["include_granted_scopes"]); + + properties.IncludeGrantedScopes = false; + Assert.False(properties.IncludeGrantedScopes); + Assert.Equal(false, properties.Parameters["include_granted_scopes"]); + + properties.IncludeGrantedScopes = null; + Assert.Null(properties.IncludeGrantedScopes); + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OAuthTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/OAuthTests.cs new file mode 100644 index 000000000..838798cea --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OAuthTests.cs @@ -0,0 +1,434 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.OAuth +{ + public class OAuthTests : RemoteAuthenticationTests + { + protected override string DefaultScheme => OAuthDefaults.DisplayName; + protected override Type HandlerType => typeof(OAuthHandler); + protected override bool SupportsSignIn { get => false; } + protected override bool SupportsSignOut { get => false; } + + protected override void RegisterAuth(AuthenticationBuilder services, Action configure) + { + services.AddOAuth(DefaultScheme, o => + { + ConfigureDefaults(o); + configure.Invoke(o); + }); + } + + [Fact] + public async Task ThrowsIfClientIdMissing() + { + var server = CreateServer( + services => services.AddAuthentication().AddOAuth("weeblie", o => + { + o.SignInScheme = "whatever"; + o.CallbackPath = "/"; + o.ClientSecret = "whatever"; + o.TokenEndpoint = "/"; + o.AuthorizationEndpoint = "/"; + })); + await Assert.ThrowsAsync("ClientId", () => server.SendAsync("http://example.com/")); + } + + [Fact] + public async Task ThrowsIfClientSecretMissing() + { + var server = CreateServer( + services => services.AddAuthentication().AddOAuth("weeblie", o => + { + o.SignInScheme = "whatever"; + o.ClientId = "Whatever;"; + o.CallbackPath = "/"; + o.TokenEndpoint = "/"; + o.AuthorizationEndpoint = "/"; + })); + await Assert.ThrowsAsync("ClientSecret", () => server.SendAsync("http://example.com/")); + } + + [Fact] + public async Task ThrowsIfCallbackPathMissing() + { + var server = CreateServer( + services => services.AddAuthentication().AddOAuth("weeblie", o => + { + o.ClientId = "Whatever;"; + o.ClientSecret = "Whatever;"; + o.TokenEndpoint = "/"; + o.AuthorizationEndpoint = "/"; + o.SignInScheme = "eh"; + })); + await Assert.ThrowsAsync("CallbackPath", () => server.SendAsync("http://example.com/")); + } + + [Fact] + public async Task ThrowsIfTokenEndpointMissing() + { + var server = CreateServer( + services => services.AddAuthentication().AddOAuth("weeblie", o => + { + o.ClientId = "Whatever;"; + o.ClientSecret = "Whatever;"; + o.CallbackPath = "/"; + o.AuthorizationEndpoint = "/"; + o.SignInScheme = "eh"; + })); + await Assert.ThrowsAsync("TokenEndpoint", () => server.SendAsync("http://example.com/")); + } + + [Fact] + public async Task ThrowsIfAuthorizationEndpointMissing() + { + var server = CreateServer( + services => services.AddAuthentication().AddOAuth("weeblie", o => + { + o.ClientId = "Whatever;"; + o.ClientSecret = "Whatever;"; + o.CallbackPath = "/"; + o.TokenEndpoint = "/"; + o.SignInScheme = "eh"; + })); + await Assert.ThrowsAsync("AuthorizationEndpoint", () => server.SendAsync("http://example.com/")); + } + + [Fact] + public async Task RedirectToIdentityProvider_SetsCorrelationIdCookiePath_ToCallBackPath() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + ConfigureDefaults(opt); + }), + async ctx => + { + await ctx.ChallengeAsync("Weblie"); + return true; + }); + + var transaction = await server.SendAsync("https://www.example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie"); + var correlation = Assert.Single(setCookie.Value, v => v.StartsWith(".AspNetCore.Correlation.")); + Assert.Contains("path=/oauth-callback", correlation); + } + + [Fact] + public async Task RedirectToAuthorizeEndpoint_CorrelationIdCookieOptions_CanBeOverriden() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + ConfigureDefaults(opt); + opt.CorrelationCookie.Path = "/"; + }), + async ctx => + { + await ctx.ChallengeAsync("Weblie"); + return true; + }); + + var transaction = await server.SendAsync("https://www.example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie"); + var correlation = Assert.Single(setCookie.Value, v => v.StartsWith(".AspNetCore.Correlation.")); + Assert.Contains("path=/", correlation); + } + + [Fact] + public async Task RedirectToAuthorizeEndpoint_HasScopeAsConfigured() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + ConfigureDefaults(opt); + opt.Scope.Clear(); + opt.Scope.Add("foo"); + opt.Scope.Add("bar"); + }), + async ctx => + { + await ctx.ChallengeAsync("Weblie"); + return true; + }); + + var transaction = await server.SendAsync("https://www.example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=foo%20bar", res.Headers.Location.Query); + } + + [Fact] + public async Task RedirectToAuthorizeEndpoint_HasScopeAsOverwritten() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + ConfigureDefaults(opt); + opt.Scope.Clear(); + opt.Scope.Add("foo"); + opt.Scope.Add("bar"); + }), + async ctx => + { + var properties = new OAuthChallengeProperties(); + properties.SetScope("baz", "qux"); + await ctx.ChallengeAsync("Weblie", properties); + return true; + }); + + var transaction = await server.SendAsync("https://www.example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=baz%20qux", res.Headers.Location.Query); + } + + [Fact] + public async Task RedirectToAuthorizeEndpoint_HasScopeAsOverwrittenWithBaseAuthenticationProperties() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + ConfigureDefaults(opt); + opt.Scope.Clear(); + opt.Scope.Add("foo"); + opt.Scope.Add("bar"); + }), + async ctx => + { + var properties = new AuthenticationProperties(); + properties.SetParameter(OAuthChallengeProperties.ScopeKey, new string[] { "baz", "qux" }); + await ctx.ChallengeAsync("Weblie", properties); + return true; + }); + + var transaction = await server.SendAsync("https://www.example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("scope=baz%20qux", res.Headers.Location.Query); + } + + protected override void ConfigureDefaults(OAuthOptions o) + { + o.ClientId = "Test Id"; + o.ClientSecret = "secret"; + o.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + o.AuthorizationEndpoint = "https://example.com/provider/login"; + o.TokenEndpoint = "https://example.com/provider/token"; + o.CallbackPath = "/oauth-callback"; + } + + [Fact] + public async Task HandleRequestAsync_RedirectsToAccessDeniedPathWhenExplicitlySet() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + opt.ClientId = "Test Id"; + opt.ClientSecret = "secret"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.AuthorizationEndpoint = "https://example.com/provider/login"; + opt.TokenEndpoint = "https://example.com/provider/token"; + opt.CallbackPath = "/oauth-callback"; + opt.AccessDeniedPath = "/access-denied"; + opt.StateDataFormat = new TestStateDataFormat(); + opt.Events.OnRemoteFailure = context => throw new InvalidOperationException("This event should not be called."); + })); + + var transaction = await server.SendAsync("https://www.example.com/oauth-callback?error=access_denied&state=protected_state", + ".AspNetCore.Correlation.Weblie.correlationId=N"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/access-denied?ReturnUrl=http%3A%2F%2Ftesthost%2Fredirect", transaction.Response.Headers.Location.ToString()); + } + + [Fact] + public async Task HandleRequestAsync_InvokesAccessDeniedEvent() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + opt.ClientId = "Test Id"; + opt.ClientSecret = "secret"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.AuthorizationEndpoint = "https://example.com/provider/login"; + opt.TokenEndpoint = "https://example.com/provider/token"; + opt.CallbackPath = "/oauth-callback"; + opt.StateDataFormat = new TestStateDataFormat(); + opt.Events = new OAuthEvents() + { + OnAccessDenied = context => + { + Assert.Equal("testvalue", context.Properties.Items["testkey"]); + context.Response.StatusCode = StatusCodes.Status406NotAcceptable; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + })); + + var transaction = await server.SendAsync("https://www.example.com/oauth-callback?error=access_denied&state=protected_state", + ".AspNetCore.Correlation.Weblie.correlationId=N"); + + Assert.Equal(HttpStatusCode.NotAcceptable, transaction.Response.StatusCode); + Assert.Null(transaction.Response.Headers.Location); + } + + [Fact] + public async Task HandleRequestAsync_InvokesRemoteFailureEventWhenAccessDeniedPathIsNotExplicitlySet() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + opt.ClientId = "Test Id"; + opt.ClientSecret = "secret"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.AuthorizationEndpoint = "https://example.com/provider/login"; + opt.TokenEndpoint = "https://example.com/provider/token"; + opt.CallbackPath = "/oauth-callback"; + opt.StateDataFormat = new TestStateDataFormat(); + opt.Events = new OAuthEvents() + { + OnRemoteFailure = context => + { + Assert.Equal("Access was denied by the resource owner or by the remote server.", context.Failure.Message); + Assert.Equal("testvalue", context.Properties.Items["testkey"]); + context.Response.StatusCode = StatusCodes.Status406NotAcceptable; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + })); + + var transaction = await server.SendAsync("https://www.example.com/oauth-callback?error=access_denied&state=protected_state", + ".AspNetCore.Correlation.Weblie.correlationId=N"); + + Assert.Equal(HttpStatusCode.NotAcceptable, transaction.Response.StatusCode); + Assert.Null(transaction.Response.Headers.Location); + } + + [Fact] + public async Task RemoteAuthenticationFailed_OAuthError_IncludesProperties() + { + var server = CreateServer( + s => s.AddAuthentication().AddOAuth( + "Weblie", + opt => + { + opt.ClientId = "Test Id"; + opt.ClientSecret = "secret"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.AuthorizationEndpoint = "https://example.com/provider/login"; + opt.TokenEndpoint = "https://example.com/provider/token"; + opt.CallbackPath = "/oauth-callback"; + opt.StateDataFormat = new TestStateDataFormat(); + opt.Events = new OAuthEvents() + { + OnRemoteFailure = context => + { + Assert.Contains("custom_error", context.Failure.Message); + Assert.Equal("testvalue", context.Properties.Items["testkey"]); + context.Response.StatusCode = StatusCodes.Status406NotAcceptable; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + })); + + var transaction = await server.SendAsync("https://www.example.com/oauth-callback?error=custom_error&state=protected_state", + ".AspNetCore.Correlation.Weblie.correlationId=N"); + + Assert.Equal(HttpStatusCode.NotAcceptable, transaction.Response.StatusCode); + Assert.Null(transaction.Response.Headers.Location); + } + + private static TestServer CreateServer(Action configureServices, Func> handler = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + if (handler == null || ! await handler(context)) + { + await next(); + } + }); + }) + .ConfigureServices(configureServices); + return new TestServer(builder); + } + + private class TestStateDataFormat : ISecureDataFormat + { + private AuthenticationProperties Data { get; set; } + + public string Protect(AuthenticationProperties data) + { + return "protected_state"; + } + + public string Protect(AuthenticationProperties data, string purpose) + { + throw new NotImplementedException(); + } + + public AuthenticationProperties Unprotect(string protectedText) + { + Assert.Equal("protected_state", protectedText); + var properties = new AuthenticationProperties(new Dictionary() + { + { ".xsrf", "correlationId" }, + { "testkey", "testvalue" } + }); + properties.RedirectUri = "http://testhost/redirect"; + return properties; + } + + public AuthenticationProperties Unprotect(string protectedText, string purpose) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/AuthenticationPropertiesFormaterKeyValue.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/AuthenticationPropertiesFormaterKeyValue.cs deleted file mode 100644 index 1be4b80bc..000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/AuthenticationPropertiesFormaterKeyValue.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Text; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Http.Authentication; - -namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect -{ - /// - /// This formatter creates an easy to read string of the format: "'key1' 'value1' ..." - /// - public class AuthenticationPropertiesFormaterKeyValue : ISecureDataFormat - { - string _protectedString = Guid.NewGuid().ToString(); - - public string Protect(AuthenticationProperties data) - { - if (data == null || data.Items.Count == 0) - { - return "null"; - } - - var sb = new StringBuilder(); - foreach(var item in data.Items) - { - sb.Append(Uri.EscapeDataString(item.Key) + " " + Uri.EscapeDataString(item.Value) + " "); - } - - return sb.ToString(); - } - public string Protect(AuthenticationProperties data, string purpose) - { - return Protect(data); - } - - public AuthenticationProperties Unprotect(string protectedText) - { - if (string.IsNullOrEmpty(protectedText)) - { - return null; - } - - if (protectedText == "null") - { - return new AuthenticationProperties(); - } - - string[] items = protectedText.Split(' '); - if (items.Length % 2 != 0) - { - return null; - } - - var propeties = new AuthenticationProperties(); - for (int i = 0; i < items.Length - 1; i+=2) - { - propeties.Items.Add(items[i], items[i + 1]); - } - - return propeties; - } - - public AuthenticationProperties Unprotect(string protectedText, string purpose) - { - return Unprotect(protectedText); - } - } -} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/ExpectedQueryValues.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/ExpectedQueryValues.cs deleted file mode 100644 index 66fc2f4ba..000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/ExpectedQueryValues.cs +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; -using System.Text.Encodings.Web; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; -using Xunit; - -namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect -{ - /// - /// This helper class is used to check that query string parameters are as expected. - /// - public class ExpectedQueryValues - { - public ExpectedQueryValues(string authority, OpenIdConnectConfiguration configuration = null) - { - Authority = authority; - Configuration = configuration ?? TestUtilities.DefaultOpenIdConnectConfiguration; - } - - public static ExpectedQueryValues Defaults(string authority) - { - var result = new ExpectedQueryValues(authority); - result.Scope = OpenIdConnectScopes.OpenIdProfile; - result.ResponseType = OpenIdConnectResponseTypes.CodeIdToken; - return result; - } - - public void CheckValues(string query, IEnumerable parameters) - { - var errors = new List(); - if (!query.StartsWith(ExpectedAuthority)) - { - errors.Add("ExpectedAuthority: " + ExpectedAuthority); - } - - foreach(var str in parameters) - { - if (str == OpenIdConnectParameterNames.ClientId) - { - if (!query.Contains(ExpectedClientId)) - errors.Add("ExpectedClientId: " + ExpectedClientId); - - continue; - } - - if (str == OpenIdConnectParameterNames.RedirectUri) - { - if(!query.Contains(ExpectedRedirectUri)) - errors.Add("ExpectedRedirectUri: " + ExpectedRedirectUri); - - continue; - } - - if (str == OpenIdConnectParameterNames.Resource) - { - if(!query.Contains(ExpectedResource)) - errors.Add("ExpectedResource: " + ExpectedResource); - - continue; - } - - if (str == OpenIdConnectParameterNames.ResponseMode) - { - if(!query.Contains(ExpectedResponseMode)) - errors.Add("ExpectedResponseMode: " + ExpectedResponseMode); - - continue; - } - - if (str == OpenIdConnectParameterNames.Scope) - { - if (!query.Contains(ExpectedScope)) - errors.Add("ExpectedScope: " + ExpectedScope); - - continue; - } - - if (str == OpenIdConnectParameterNames.State) - { - if (!query.Contains(ExpectedState)) - errors.Add("ExpectedState: " + ExpectedState); - - continue; - } - } - - if (errors.Count > 0) - { - var sb = new StringBuilder(); - sb.AppendLine("query string not as expected: " + Environment.NewLine + query + Environment.NewLine); - foreach (var str in errors) - { - sb.AppendLine(str); - } - - Debug.WriteLine(sb.ToString()); - Assert.True(false, sb.ToString()); - } - } - - public UrlEncoder Encoder { get; set; } = UrlEncoder.Default; - - public string Authority { get; set; } - - public string ClientId { get; set; } = Guid.NewGuid().ToString(); - - public string RedirectUri { get; set; } = Guid.NewGuid().ToString(); - - public OpenIdConnectRequestType RequestType { get; set; } = OpenIdConnectRequestType.AuthenticationRequest; - - public string Resource { get; set; } = Guid.NewGuid().ToString(); - - public string ResponseMode { get; set; } = OpenIdConnectResponseModes.FormPost; - - public string ResponseType { get; set; } = Guid.NewGuid().ToString(); - - public string Scope { get; set; } = Guid.NewGuid().ToString(); - - public string State { get; set; } = Guid.NewGuid().ToString(); - - public string ExpectedAuthority - { - get - { - if (RequestType == OpenIdConnectRequestType.TokenRequest) - { - return Configuration?.EndSessionEndpoint ?? Authority + @"/oauth2/token"; - } - else if (RequestType == OpenIdConnectRequestType.LogoutRequest) - { - return Configuration?.TokenEndpoint ?? Authority + @"/oauth2/logout"; - } - - return Configuration?.AuthorizationEndpoint ?? Authority + (@"/oauth2/authorize"); - } - } - - public OpenIdConnectConfiguration Configuration { get; set; } - - public string ExpectedClientId - { - get { return OpenIdConnectParameterNames.ClientId + "=" + Encoder.Encode(ClientId); } - } - - public string ExpectedRedirectUri - { - get { return OpenIdConnectParameterNames.RedirectUri + "=" + Encoder.Encode(RedirectUri); } - } - - public string ExpectedResource - { - get { return OpenIdConnectParameterNames.Resource + "=" + Encoder.Encode(Resource); } - } - - public string ExpectedResponseMode - { - get { return OpenIdConnectParameterNames.ResponseMode + "=" + Encoder.Encode(ResponseMode); } - } - - public string ExpectedScope - { - get { return OpenIdConnectParameterNames.Scope + "=" + Encoder.Encode(Scope); } - } - - public string ExpectedState - { - get { return Encoder.Encode(State); } - } - } -} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/MockOpenIdConnectMessage.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/MockOpenIdConnectMessage.cs new file mode 100644 index 000000000..5614fe8fe --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/MockOpenIdConnectMessage.cs @@ -0,0 +1,21 @@ +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + internal class MockOpenIdConnectMessage : OpenIdConnectMessage + { + public string TestAuthorizeEndpoint { get; set; } + + public string TestLogoutRequest { get; set; } + + public override string CreateAuthenticationRequestUrl() + { + return TestAuthorizeEndpoint ?? base.CreateAuthenticationRequestUrl(); + } + + public override string CreateLogoutRequestUrl() + { + return TestLogoutRequest ?? base.CreateLogoutRequestUrl(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs new file mode 100644 index 000000000..cbafc4622 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs @@ -0,0 +1,616 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + public class OpenIdConnectChallengeTests + { + private static readonly string ChallengeEndpoint = TestServerBuilder.TestHost + TestServerBuilder.Challenge; + + [Fact] + public async Task ChallengeRedirectIsIssuedCorrectly() + { + var settings = new TestSettings( + opt => + { + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet; + opt.ClientId = "Test Id"; + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.ClientId, + OpenIdConnectParameterNames.ResponseType, + OpenIdConnectParameterNames.ResponseMode, + OpenIdConnectParameterNames.Scope, + OpenIdConnectParameterNames.RedirectUri, + OpenIdConnectParameterNames.SkuTelemetry, + OpenIdConnectParameterNames.VersionTelemetry); + } + + [Fact] + public async Task AuthorizationRequestDoesNotIncludeTelemetryParametersWhenDisabled() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.DisableTelemetry = true; + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.DoesNotContain(OpenIdConnectParameterNames.SkuTelemetry, res.Headers.Location.Query); + Assert.DoesNotContain(OpenIdConnectParameterNames.VersionTelemetry, res.Headers.Location.Query); + } + + /* + Example of a form post + +
+ + + + + + + + +
+ + + */ + [Fact] + public async Task ChallengeFormPostIssuedCorrectly() + { + var settings = new TestSettings( + opt => + { + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.AuthenticationMethod = OpenIdConnectRedirectBehavior.FormPost; + opt.ClientId = "Test Id"; + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.OK, res.StatusCode); + Assert.Equal("text/html", transaction.Response.Content.Headers.ContentType.MediaType); + + var body = await res.Content.ReadAsStringAsync(); + settings.ValidateChallengeFormPost( + body, + OpenIdConnectParameterNames.ClientId, + OpenIdConnectParameterNames.ResponseType, + OpenIdConnectParameterNames.ResponseMode, + OpenIdConnectParameterNames.Scope, + OpenIdConnectParameterNames.RedirectUri); + } + + [Theory] + [InlineData("sample_user_state")] + [InlineData(null)] + public async Task ChallengeCanSetUserStateThroughProperties(string userState) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("OIDCTest")); + var settings = new TestSettings(o => + { + o.ClientId = "Test Id"; + o.Authority = TestServerBuilder.DefaultAuthority; + o.StateDataFormat = stateFormat; + }); + + var properties = new AuthenticationProperties(); + properties.Items.Add(OpenIdConnectDefaults.UserstatePropertiesKey, userState); + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + var values = settings.ValidateChallengeRedirect(res.Headers.Location); + var actualState = values[OpenIdConnectParameterNames.State]; + var actualProperties = stateFormat.Unprotect(actualState); + + Assert.Equal(userState ?? string.Empty, actualProperties.Items[OpenIdConnectDefaults.UserstatePropertiesKey]); + } + + [Theory] + [InlineData("sample_user_state")] + [InlineData(null)] + public async Task OnRedirectToIdentityProviderEventCanSetState(string userState) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("OIDCTest")); + var settings = new TestSettings(opt => + { + opt.StateDataFormat = stateFormat; + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.ProtocolMessage.State = userState; + return Task.FromResult(0); + } + }; + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + var values = settings.ValidateChallengeRedirect(res.Headers.Location); + var actualState = values[OpenIdConnectParameterNames.State]; + var actualProperties = stateFormat.Unprotect(actualState); + + if (userState != null) + { + Assert.Equal(userState, actualProperties.Items[OpenIdConnectDefaults.UserstatePropertiesKey]); + } + else + { + Assert.False(actualProperties.Items.ContainsKey(OpenIdConnectDefaults.UserstatePropertiesKey)); + } + } + + [Fact] + public async Task OnRedirectToIdentityProviderEventIsHit() + { + var eventIsHit = false; + var settings = new TestSettings( + opts => + { + opts.ClientId = "Test Id"; + opts.Authority = TestServerBuilder.DefaultAuthority; + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + eventIsHit = true; + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + Assert.True(eventIsHit); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.ClientId, + OpenIdConnectParameterNames.ResponseType, + OpenIdConnectParameterNames.ResponseMode, + OpenIdConnectParameterNames.Scope, + OpenIdConnectParameterNames.RedirectUri); + } + + + [Fact] + public async Task OnRedirectToIdentityProviderEventCanReplaceValues() + { + var newClientId = Guid.NewGuid().ToString(); + + var settings = new TestSettings( + opts => + { + opts.ClientId = "Test Id"; + opts.Authority = TestServerBuilder.DefaultAuthority; + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.ProtocolMessage.ClientId = newClientId; + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.ResponseType, + OpenIdConnectParameterNames.ResponseMode, + OpenIdConnectParameterNames.Scope, + OpenIdConnectParameterNames.RedirectUri); + + var actual = res.Headers.Location.Query.Trim('?').Split('&').Single(seg => seg.StartsWith($"{OpenIdConnectParameterNames.ClientId}=")); + Assert.Equal($"{OpenIdConnectParameterNames.ClientId}={newClientId}", actual); + } + + [Fact] + public async Task OnRedirectToIdentityProviderEventCanReplaceMessage() + { + var newMessage = new MockOpenIdConnectMessage + { + IssuerAddress = "http://example.com/", + TestAuthorizeEndpoint = $"http://example.com/{Guid.NewGuid()}/oauth2/signin" + }; + + var settings = new TestSettings( + opts => + { + opts.ClientId = "Test Id"; + opts.Authority = TestServerBuilder.DefaultAuthority; + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.ProtocolMessage = newMessage; + + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + // The CreateAuthenticationRequestUrl method is overridden MockOpenIdConnectMessage where + // query string is not generated and the authorization endpoint is replaced. + Assert.Equal(newMessage.TestAuthorizeEndpoint, res.Headers.Location.AbsoluteUri); + } + + [Fact] + public async Task OnRedirectToIdentityProviderEventHandlesResponse() + { + var settings = new TestSettings( + opts => + { + opts.ClientId = "Test Id"; + opts.Authority = TestServerBuilder.DefaultAuthority; + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.Response.StatusCode = 410; + context.Response.Headers.Add("tea", "Oolong"); + context.HandleResponse(); + + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Gone, res.StatusCode); + Assert.Equal("Oolong", res.Headers.GetValues("tea").Single()); + Assert.Null(res.Headers.Location); + } + + // This test can be further refined. When one auth handler skips, the authentication responsibility + // will be flowed to the next one. A dummy auth handler can be added to ensure the correct logic. + [Fact] + public async Task OnRedirectToIdentityProviderEventHandleResponse() + { + var settings = new TestSettings( + opts => + { + opts.ClientId = "Test Id"; + opts.Authority = TestServerBuilder.DefaultAuthority; + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.HandleResponse(); + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.OK, res.StatusCode); + Assert.Null(res.Headers.Location); + } + + [Theory] + [InlineData(OpenIdConnectRedirectBehavior.RedirectGet)] + [InlineData(OpenIdConnectRedirectBehavior.FormPost)] + public async Task ChallengeSetsNonceAndStateCookies(OpenIdConnectRedirectBehavior method) + { + var settings = new TestSettings(o => + { + o.AuthenticationMethod = method; + o.ClientId = "Test Id"; + o.Authority = TestServerBuilder.DefaultAuthority; + }); + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var challengeCookies = SetCookieHeaderValue.ParseList(transaction.SetCookie); + var nonceCookie = challengeCookies.Where(cookie => cookie.Name.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix, StringComparison.Ordinal)).Single(); + Assert.True(nonceCookie.Expires.HasValue); + Assert.True(nonceCookie.Expires > DateTime.UtcNow); + Assert.True(nonceCookie.HttpOnly); + Assert.Equal("/signin-oidc", nonceCookie.Path); + Assert.Equal("N", nonceCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.None, nonceCookie.SameSite); + + var correlationCookie = challengeCookies.Where(cookie => cookie.Name.StartsWith(".AspNetCore.Correlation.", StringComparison.Ordinal)).Single(); + Assert.True(correlationCookie.Expires.HasValue); + Assert.True(nonceCookie.Expires > DateTime.UtcNow); + Assert.True(correlationCookie.HttpOnly); + Assert.Equal("/signin-oidc", correlationCookie.Path); + Assert.False(StringSegment.IsNullOrEmpty(correlationCookie.Value)); + + Assert.Equal(2, challengeCookies.Count); + } + + [Fact] + public async Task Challenge_WithEmptyConfig_Fails() + { + var settings = new TestSettings( + opt => + { + opt.ClientId = "Test Id"; + opt.Configuration = new OpenIdConnectConfiguration(); + }); + + var server = settings.CreateTestServer(); + var exception = await Assert.ThrowsAsync(() => server.SendAsync(ChallengeEndpoint)); + Assert.Equal("Cannot redirect to the authorization endpoint, the configuration may be missing or invalid.", exception.Message); + } + + [Fact] + public async Task Challenge_WithDefaultMaxAge_HasExpectedMaxAgeParam() + { + var settings = new TestSettings( + opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.MaxAge); + } + + [Fact] + public async Task Challenge_WithSpecificMaxAge_HasExpectedMaxAgeParam() + { + var settings = new TestSettings( + opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.MaxAge = TimeSpan.FromMinutes(20); + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.MaxAge); + } + + [Fact] + public async Task Challenge_HasExpectedPromptParam() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.Prompt = "consent"; + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(ChallengeEndpoint); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location, OpenIdConnectParameterNames.Prompt); + Assert.Contains("prompt=consent", res.Headers.Location.Query); + } + + [Fact] + public async Task Challenge_HasOverwrittenPromptParam() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.Prompt = "consent"; + }); + var properties = new OpenIdConnectChallengeProperties() + { + Prompt = "login", + }; + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location); + Assert.Contains("prompt=login", res.Headers.Location.Query); + } + + [Fact] + public async Task Challenge_HasOverwrittenPromptParamFromBaseAuthenticationProperties() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.Prompt = "consent"; + }); + var properties = new AuthenticationProperties(); + properties.SetParameter(OpenIdConnectChallengeProperties.PromptKey, "login"); + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location); + Assert.Contains("prompt=login", res.Headers.Location.Query); + } + + [Fact] + public async Task Challenge_HasOverwrittenScopeParam() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.Scope.Clear(); + opt.Scope.Add("foo"); + opt.Scope.Add("bar"); + }); + var properties = new OpenIdConnectChallengeProperties(); + properties.SetScope("baz", "qux"); + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location); + Assert.Contains("scope=baz%20qux", res.Headers.Location.Query); + } + + [Fact] + public async Task Challenge_HasOverwrittenScopeParamFromBaseAuthenticationProperties() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.Scope.Clear(); + opt.Scope.Add("foo"); + opt.Scope.Add("bar"); + }); + var properties = new AuthenticationProperties(); + properties.SetParameter(OpenIdConnectChallengeProperties.ScopeKey, new string[] { "baz", "qux" }); + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location); + Assert.Contains("scope=baz%20qux", res.Headers.Location.Query); + } + + [Fact] + public async Task Challenge_HasOverwrittenMaxAgeParam() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.MaxAge = TimeSpan.FromSeconds(500); + }); + var properties = new OpenIdConnectChallengeProperties() + { + MaxAge = TimeSpan.FromSeconds(1234), + }; + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location); + Assert.Contains("max_age=1234", res.Headers.Location.Query); + } + + [Fact] + public async Task Challenge_HasOverwrittenMaxAgeParaFromBaseAuthenticationPropertiesm() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.MaxAge = TimeSpan.FromSeconds(500); + }); + var properties = new AuthenticationProperties(); + properties.SetParameter(OpenIdConnectChallengeProperties.MaxAgeKey, TimeSpan.FromSeconds(1234)); + + var server = settings.CreateTestServer(properties); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location); + Assert.Contains("max_age=1234", res.Headers.Location.Query); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs new file mode 100644 index 000000000..ed368c1ef --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs @@ -0,0 +1,574 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Authentication.Tests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + public class OpenIdConnectConfigurationTests + { + private void ConfigureDefaults(OpenIdConnectOptions o) + { + o.Authority = TestServerBuilder.DefaultAuthority; + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + o.SignInScheme = "auth1"; + } + + [Fact] + public async Task CanForwardDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + }); + + var forwardDefault = new TestHandler(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await context.SignOutAsync(); + Assert.Equal(1, forwardDefault.SignOutCount); + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignInThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + [Fact] + public async Task ForwardSignOutWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.SignOutAsync(); + Assert.Equal(1, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardForbidWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ForbidAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(1, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardAuthenticateWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardAuthenticate = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(1, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardChallengeWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("specific", "specific"); + o.AddScheme("auth1", "auth1"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardChallenge = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ChallengeAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(1, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardSelectorWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, selector.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, selector.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, selector.ChallengeCount); + + await context.SignOutAsync(); + Assert.Equal(1, selector.SignOutCount); + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task NullForwardSelectorUsesDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => null; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await context.SignOutAsync(); + Assert.Equal(1, forwardDefault.SignOutCount); + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task SpecificForwardWinsOverSelectorAndDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddOpenIdConnect(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + o.ForwardAuthenticate = "specific"; + o.ForwardChallenge = "specific"; + o.ForwardSignIn = "specific"; + o.ForwardSignOut = "specific"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, specific.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, specific.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, specific.ChallengeCount); + + await context.SignOutAsync(); + Assert.Equal(1, specific.SignOutCount); + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + } + + [Fact] + public async Task MetadataAddressIsGeneratedFromAuthorityWhenMissing() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddAuthentication() + .AddCookie() + .AddOpenIdConnect(o => + { + o.Authority = TestServerBuilder.DefaultAuthority; + o.ClientId = Guid.NewGuid().ToString(); + o.SignInScheme = Guid.NewGuid().ToString(); + }); + }) + .Configure(app => + { + app.UseAuthentication(); + app.Run(async context => + { + var resolver = context.RequestServices.GetRequiredService(); + var handler = await resolver.GetHandlerAsync(context, OpenIdConnectDefaults.AuthenticationScheme) as OpenIdConnectHandler; + Assert.Equal($"{TestServerBuilder.DefaultAuthority}/.well-known/openid-configuration", handler.Options.MetadataAddress); + }); + }); + var server = new TestServer(builder); + var transaction = await server.SendAsync(@"https://example.com"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public Task ThrowsWhenSignInSchemeIsSetToSelf() + { + return TestConfigurationException( + o => + { + o.SignInScheme = OpenIdConnectDefaults.AuthenticationScheme; + o.Authority = TestServerBuilder.DefaultAuthority; + o.ClientId = "Test Id"; + o.ClientSecret = "Test Secret"; + }, + ex => Assert.Contains("cannot be set to itself", ex.Message)); + } + + [Fact] + public Task ThrowsWhenClientIdIsMissing() + { + return TestConfigurationException( + o => + { + o.SignInScheme = "TestScheme"; + o.Authority = TestServerBuilder.DefaultAuthority; + }, + ex => Assert.Equal("ClientId", ex.ParamName)); + } + + [Fact] + public Task ThrowsWhenAuthorityIsMissing() + { + return TestConfigurationException( + o => + { + o.SignInScheme = "TestScheme"; + o.ClientId = "Test Id"; + o.CallbackPath = "/"; + }, + ex => Assert.Equal("Provide Authority, MetadataAddress, Configuration, or ConfigurationManager to OpenIdConnectOptions", ex.Message) + ); + } + + [Fact] + public Task ThrowsWhenAuthorityIsNotHttps() + { + return TestConfigurationException( + o => + { + o.SignInScheme = "TestScheme"; + o.ClientId = "Test Id"; + o.MetadataAddress = "http://example.com"; + o.CallbackPath = "/"; + }, + ex => Assert.Equal("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false.", ex.Message) + ); + } + + [Fact] + public Task ThrowsWhenMetadataAddressIsNotHttps() + { + return TestConfigurationException( + o => + { + o.SignInScheme = "TestScheme"; + o.ClientId = "Test Id"; + o.MetadataAddress = "http://example.com"; + o.CallbackPath = "/"; + }, + ex => Assert.Equal("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false.", ex.Message) + ); + } + + [Fact] + public Task ThrowsWhenMaxAgeIsNegative() + { + return TestConfigurationException( + o => + { + o.SignInScheme = "TestScheme"; + o.ClientId = "Test Id"; + o.Authority = TestServerBuilder.DefaultAuthority; + o.MaxAge = TimeSpan.FromSeconds(-1); + }, + ex => Assert.StartsWith("The value must not be a negative TimeSpan.", ex.Message) + ); + } + + private TestServer BuildTestServer(Action options) + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddAuthentication() + .AddCookie() + .AddOpenIdConnect(options); + }) + .Configure(app => app.UseAuthentication()); + + return new TestServer(builder); + } + + private async Task TestConfigurationException( + Action options, + Action verifyException) + where T : Exception + { + var exception = await Assert.ThrowsAsync(() => BuildTestServer(options).SendAsync(@"https://example.com")); + verifyException(exception); + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs new file mode 100644 index 000000000..090bf3dec --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs @@ -0,0 +1,1403 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + public class OpenIdConnectEventTests + { + private readonly RequestDelegate AppWritePath = context => context.Response.WriteAsync(context.Request.Path); + private readonly RequestDelegate AppNotImpl = context => { throw new NotImplementedException("App"); }; + + [Fact] + public async Task OnMessageReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + }; + events.OnMessageReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", ""); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnMessageReceived_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectRemoteFailure = true, + }; + events.OnMessageReceived = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", ""); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnMessageReceived_Handled_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + }; + events.OnMessageReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", ""); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidated_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + }; + events.OnTokenValidated = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidated_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectRemoteFailure = true, + }; + events.OnTokenValidated = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidated_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + }; + events.OnTokenValidated = context => + { + context.HandleResponse(); + context.Principal = null; + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidated_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectTicketReceived = true, + }; + events.OnTokenValidated = context => + { + context.HandleResponse(); + context.Principal = null; + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + events.OnTokenValidated = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthorizationCodeReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + }; + events.OnAuthorizationCodeReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthorizationCodeReceived_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectRemoteFailure = true, + }; + events.OnAuthorizationCodeReceived = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthorizationCodeReceived_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + }; + events.OnAuthorizationCodeReceived = context => + { + context.HandleResponse(); + context.Principal = null; + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthorizationCodeReceived_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTicketReceived = true, + }; + events.OnAuthorizationCodeReceived = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenResponseReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + }; + events.OnTokenResponseReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenResponseReceived_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectRemoteFailure = true, + }; + events.OnTokenResponseReceived = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenResponseReceived_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + }; + events.OnTokenResponseReceived = context => + { + context.Principal = null; + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenResponseReceived_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectTicketReceived = true, + }; + events.OnTokenResponseReceived = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidatedBackchannel_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + }; + events.OnTokenValidated = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidatedBackchannel_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectRemoteFailure = true, + }; + events.OnTokenValidated = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidatedBackchannel_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + }; + events.OnTokenValidated = context => + { + context.Principal = null; + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTokenValidatedBackchannel_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectTicketReceived = true, + }; + events.OnTokenValidated = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnUserInformationReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + }; + events.OnUserInformationReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnUserInformationReceived_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectRemoteFailure = true, + }; + events.OnUserInformationReceived = context => + { + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnUserInformationReceived_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + }; + events.OnUserInformationReceived = context => + { + context.Principal = null; + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnUserInformationReceived_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectTicketReceived = true, + }; + events.OnUserInformationReceived = context => + { + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthenticationFailed_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthenticationFailed_Fail_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + ExpectRemoteFailure = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + context.Fail("Authentication was aborted from user code."); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var exception = await Assert.ThrowsAsync(delegate + { + return PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + }); + + Assert.Equal("Authentication was aborted from user code.", exception.InnerException.Message); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthenticationFailed_HandledWithoutTicket_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + Assert.Null(context.Principal); + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAuthenticationFailed_HandledWithTicket_SkipToTicketReceived() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + ExpectTicketReceived = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + Assert.Null(context.Principal); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob") + }; + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); + context.Success(); + return Task.FromResult(0); + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAccessDenied_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectAccessDenied = true + }; + events.OnAccessDenied = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "error=access_denied&state=protected_state"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnAccessDenied_Handled_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectAccessDenied = true + }; + events.OnAccessDenied = context => + { + Assert.Equal("testvalue", context.Properties.Items["testkey"]); + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "error=access_denied&state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRemoteFailure_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + ExpectRemoteFailure = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnAuthenticationFailed = context => + { + Assert.Equal("TestException", context.Exception.Message); + return Task.FromResult(0); + }; + events.OnRemoteFailure = context => + { + Assert.Equal("TestException", context.Failure.Message); + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRemoteFailure_Handled_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectAuthenticationFailed = true, + ExpectRemoteFailure = true, + }; + events.OnUserInformationReceived = context => + { + throw new NotImplementedException("TestException"); + }; + events.OnRemoteFailure = context => + { + Assert.Equal("TestException", context.Failure.Message); + Assert.Equal("testvalue", context.Properties.Items["testkey"]); + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTicketReceived_Skip_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectTicketReceived = true, + }; + events.OnTicketReceived = context => + { + context.SkipHandler(); + return Task.FromResult(0); + }; + var server = CreateServer(events, AppWritePath); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("/signin-oidc", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnTicketReceived_Handled_NoMoreEventsRun() + { + var events = new ExpectedOidcEvents() + { + ExpectMessageReceived = true, + ExpectTokenValidated = true, + ExpectAuthorizationCodeReceived = true, + ExpectTokenResponseReceived = true, + ExpectUserInfoReceived = true, + ExpectTicketReceived = true, + }; + events.OnTicketReceived = context => + { + context.HandleResponse(); + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.FromResult(0); + }; + var server = CreateServer(events, AppNotImpl); + + var response = await PostAsync(server, "signin-oidc", "id_token=my_id_token&state=protected_state&code=my_code"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRedirectToIdentityProviderForSignOut_Invoked() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectForSignOut = true, + }; + var server = CreateServer(events, + context => + { + return context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); + }); + + var client = server.CreateClient(); + var response = await client.GetAsync("/"); + + Assert.Equal(HttpStatusCode.Found, response.StatusCode); + Assert.Equal("http://testhost/end", response.Headers.Location.GetLeftPart(UriPartial.Path)); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRedirectToIdentityProviderForSignOut_Handled_RedirectNotInvoked() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectForSignOut = true, + }; + events.OnRedirectToIdentityProviderForSignOut = context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + context.HandleResponse(); + return Task.CompletedTask; + }; + var server = CreateServer(events, + context => + { + return context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); + }); + + var client = server.CreateClient(); + var response = await client.GetAsync("/"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Null(response.Headers.Location); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRemoteSignOut_Invoked() + { + var events = new ExpectedOidcEvents() + { + ExpectRemoteSignOut = true, + }; + var server = CreateServer(events, AppNotImpl); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-oidc"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + events.ValidateExpectations(); + Assert.True(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values)); + Assert.True(SetCookieHeaderValue.TryParseStrictList(values.ToList(), out var parsedValues)); + Assert.Equal(1, parsedValues.Count); + Assert.True(StringSegment.IsNullOrEmpty(parsedValues.Single().Value)); + } + + [Fact] + public async Task OnRemoteSignOut_Handled_NoSignout() + { + var events = new ExpectedOidcEvents() + { + ExpectRemoteSignOut = true, + }; + events.OnRemoteSignOut = context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + context.HandleResponse(); + return Task.CompletedTask; + }; + var server = CreateServer(events, AppNotImpl); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-oidc"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + events.ValidateExpectations(); + Assert.False(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values)); + } + + [Fact] + public async Task OnRemoteSignOut_Skip_NoSignout() + { + var events = new ExpectedOidcEvents() + { + ExpectRemoteSignOut = true, + }; + events.OnRemoteSignOut = context => + { + context.SkipHandler(); + return Task.CompletedTask; + }; + var server = CreateServer(events, context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.CompletedTask; + }); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-oidc"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + events.ValidateExpectations(); + Assert.False(response.Headers.TryGetValues(HeaderNames.SetCookie, out var values)); + } + + [Fact] + public async Task OnRedirectToSignedOutRedirectUri_Invoked() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectToSignedOut = true, + }; + var server = CreateServer(events, AppNotImpl); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-callback-oidc?state=protected_state"); + + Assert.Equal(HttpStatusCode.Found, response.StatusCode); + Assert.Equal("http://testhost/redirect", response.Headers.Location.AbsoluteUri); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRedirectToSignedOutRedirectUri_Handled_NoRedirect() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectToSignedOut = true, + }; + events.OnSignedOutCallbackRedirect = context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + context.HandleResponse(); + return Task.CompletedTask; + }; + var server = CreateServer(events, AppNotImpl); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-callback-oidc?state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Null(response.Headers.Location); + events.ValidateExpectations(); + } + + [Fact] + public async Task OnRedirectToSignedOutRedirectUri_Skipped_NoRedirect() + { + var events = new ExpectedOidcEvents() + { + ExpectRedirectToSignedOut = true, + }; + events.OnSignedOutCallbackRedirect = context => + { + context.SkipHandler(); + return Task.CompletedTask; + }; + var server = CreateServer(events, + context => + { + context.Response.StatusCode = StatusCodes.Status202Accepted; + return Task.CompletedTask; + }); + + var client = server.CreateClient(); + var response = await client.GetAsync("/signout-callback-oidc?state=protected_state"); + + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.Null(response.Headers.Location); + events.ValidateExpectations(); + } + + private class ExpectedOidcEvents : OpenIdConnectEvents + { + public bool ExpectMessageReceived { get; set; } + public bool InvokedMessageReceived { get; set; } + + public bool ExpectTokenValidated { get; set; } + public bool InvokedTokenValidated { get; set; } + + public bool ExpectAccessDenied { get; set; } + public bool InvokedAccessDenied { get; set; } + + public bool ExpectRemoteFailure { get; set; } + public bool InvokedRemoteFailure { get; set; } + + public bool ExpectTicketReceived { get; set; } + public bool InvokedTicketReceived { get; set; } + + public bool ExpectAuthorizationCodeReceived { get; set; } + public bool InvokedAuthorizationCodeReceived { get; set; } + + public bool ExpectTokenResponseReceived { get; set; } + public bool InvokedTokenResponseReceived { get; set; } + + public bool ExpectUserInfoReceived { get; set; } + public bool InvokedUserInfoReceived { get; set; } + + public bool ExpectAuthenticationFailed { get; set; } + public bool InvokeAuthenticationFailed { get; set; } + + public bool ExpectRedirectForSignOut { get; set; } + public bool InvokedRedirectForSignOut { get; set; } + + public bool ExpectRemoteSignOut { get; set; } + public bool InvokedRemoteSignOut { get; set; } + + public bool ExpectRedirectToSignedOut { get; set; } + public bool InvokedRedirectToSignedOut { get; set; } + + public override Task MessageReceived(MessageReceivedContext context) + { + InvokedMessageReceived = true; + return base.MessageReceived(context); + } + + public override Task TokenValidated(TokenValidatedContext context) + { + InvokedTokenValidated = true; + return base.TokenValidated(context); + } + + public override Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context) + { + InvokedAuthorizationCodeReceived = true; + return base.AuthorizationCodeReceived(context); + } + + public override Task TokenResponseReceived(TokenResponseReceivedContext context) + { + InvokedTokenResponseReceived = true; + return base.TokenResponseReceived(context); + } + + public override Task UserInformationReceived(UserInformationReceivedContext context) + { + InvokedUserInfoReceived = true; + return base.UserInformationReceived(context); + } + + public override Task AuthenticationFailed(AuthenticationFailedContext context) + { + InvokeAuthenticationFailed = true; + return base.AuthenticationFailed(context); + } + + public override Task TicketReceived(TicketReceivedContext context) + { + InvokedTicketReceived = true; + return base.TicketReceived(context); + } + + public override Task AccessDenied(AccessDeniedContext context) + { + InvokedAccessDenied = true; + return base.AccessDenied(context); + } + + public override Task RemoteFailure(RemoteFailureContext context) + { + InvokedRemoteFailure = true; + return base.RemoteFailure(context); + } + + public override Task RedirectToIdentityProviderForSignOut(RedirectContext context) + { + InvokedRedirectForSignOut = true; + return base.RedirectToIdentityProviderForSignOut(context); + } + + public override Task RemoteSignOut(RemoteSignOutContext context) + { + InvokedRemoteSignOut = true; + return base.RemoteSignOut(context); + } + + public override Task SignedOutCallbackRedirect(RemoteSignOutContext context) + { + InvokedRedirectToSignedOut = true; + return base.SignedOutCallbackRedirect(context); + } + + public void ValidateExpectations() + { + Assert.Equal(ExpectMessageReceived, InvokedMessageReceived); + Assert.Equal(ExpectTokenValidated, InvokedTokenValidated); + Assert.Equal(ExpectAuthorizationCodeReceived, InvokedAuthorizationCodeReceived); + Assert.Equal(ExpectTokenResponseReceived, InvokedTokenResponseReceived); + Assert.Equal(ExpectUserInfoReceived, InvokedUserInfoReceived); + Assert.Equal(ExpectAuthenticationFailed, InvokeAuthenticationFailed); + Assert.Equal(ExpectTicketReceived, InvokedTicketReceived); + Assert.Equal(ExpectAccessDenied, InvokedAccessDenied); + Assert.Equal(ExpectRemoteFailure, InvokedRemoteFailure); + Assert.Equal(ExpectRedirectForSignOut, InvokedRedirectForSignOut); + Assert.Equal(ExpectRemoteSignOut, InvokedRemoteSignOut); + Assert.Equal(ExpectRedirectToSignedOut, InvokedRedirectToSignedOut); + } + } + + private TestServer CreateServer(OpenIdConnectEvents events, RequestDelegate appCode) + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddAuthentication(auth => + { + auth.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddOpenIdConnect(o => + { + o.Events = events; + o.ClientId = "ClientId"; + o.GetClaimsFromUserInfoEndpoint = true; + o.Configuration = new OpenIdConnectConfiguration() + { + TokenEndpoint = "http://testhost/tokens", + UserInfoEndpoint = "http://testhost/user", + EndSessionEndpoint = "http://testhost/end" + }; + o.StateDataFormat = new TestStateDataFormat(); + o.SecurityTokenValidator = new TestTokenValidator(); + o.ProtocolValidator = new TestProtocolValidator(); + o.BackchannelHttpHandler = new TestBackchannel(); + }); + }) + .Configure(app => + { + app.UseAuthentication(); + app.Run(appCode); + }); + + return new TestServer(builder); + } + + private Task PostAsync(TestServer server, string path, string form) + { + var client = server.CreateClient(); + var cookie = ".AspNetCore.Correlation." + OpenIdConnectDefaults.AuthenticationScheme + ".correlationId=N"; + client.DefaultRequestHeaders.Add("Cookie", cookie); + return client.PostAsync("signin-oidc", + new StringContent(form, Encoding.ASCII, "application/x-www-form-urlencoded")); + } + + private class TestStateDataFormat : ISecureDataFormat + { + private AuthenticationProperties Data { get; set; } + + public string Protect(AuthenticationProperties data) + { + return "protected_state"; + } + + public string Protect(AuthenticationProperties data, string purpose) + { + throw new NotImplementedException(); + } + + public AuthenticationProperties Unprotect(string protectedText) + { + Assert.Equal("protected_state", protectedText); + var properties = new AuthenticationProperties(new Dictionary() + { + { ".xsrf", "correlationId" }, + { OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, "redirect_uri" }, + { "testkey", "testvalue" } + }); + properties.RedirectUri = "http://testhost/redirect"; + return properties; + } + + public AuthenticationProperties Unprotect(string protectedText, string purpose) + { + throw new NotImplementedException(); + } + } + + private class TestTokenValidator : ISecurityTokenValidator + { + public bool CanValidateToken => true; + + public int MaximumTokenSizeInBytes + { + get { return 1024; } + set { throw new NotImplementedException(); } + } + + public bool CanReadToken(string securityToken) + { + Assert.Equal("my_id_token", securityToken); + return true; + } + + public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) + { + Assert.Equal("my_id_token", securityToken); + validatedToken = new JwtSecurityToken(); + return new ClaimsPrincipal(new ClaimsIdentity("customAuthType")); + } + } + + private class TestProtocolValidator : OpenIdConnectProtocolValidator + { + public override void ValidateAuthenticationResponse(OpenIdConnectProtocolValidationContext validationContext) + { + } + + public override void ValidateTokenResponse(OpenIdConnectProtocolValidationContext validationContext) + { + } + + public override void ValidateUserInfoResponse(OpenIdConnectProtocolValidationContext validationContext) + { + } + } + + private class TestBackchannel : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (string.Equals("/tokens", request.RequestUri.AbsolutePath, StringComparison.Ordinal)) + { + return Task.FromResult(new HttpResponseMessage() { Content = + new StringContent("{ \"id_token\": \"my_id_token\", \"access_token\": \"my_access_token\" }", Encoding.ASCII, "application/json") }); + } + if (string.Equals("/user", request.RequestUri.AbsolutePath, StringComparison.Ordinal)) + { + return Task.FromResult(new HttpResponseMessage() { Content = new StringContent("{ }", Encoding.ASCII, "application/json") }); + } + + throw new NotImplementedException(request.RequestUri.ToString()); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs deleted file mode 100644 index ebc59b9ee..000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs +++ /dev/null @@ -1,554 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Security.Claims; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using System.Xml.Linq; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; -using Xunit; - -namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect -{ - public class OpenIdConnectMiddlewareTests - { - static string noncePrefix = "OpenIdConnect." + "Nonce."; - static string nonceDelimiter = "."; - const string Challenge = "/challenge"; - const string ChallengeWithOutContext = "/challengeWithOutContext"; - const string ChallengeWithProperties = "/challengeWithProperties"; - const string DefaultHost = @"https://example.com"; - const string DefaultAuthority = @"https://example.com/common"; - const string ExpectedAuthorizeRequest = @"https://example.com/common/oauth2/signin"; - const string ExpectedLogoutRequest = @"https://example.com/common/oauth2/logout"; - const string Logout = "/logout"; - const string Signin = "/signin"; - const string Signout = "/signout"; - - [Fact] - public async Task ChallengeWillIssueHtmlFormWhenEnabled() - { - var server = CreateServer(new OpenIdConnectOptions - { - Authority = DefaultAuthority, - ClientId = "Test Id", - Configuration = TestUtilities.DefaultOpenIdConnectConfiguration, - AuthenticationMethod = OpenIdConnectRedirectBehavior.FormPost - }); - var transaction = await SendAsync(server, DefaultHost + Challenge); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - Assert.Equal("text/html", transaction.Response.Content.Headers.ContentType.MediaType); - Assert.Contains("form", transaction.ResponseText); - } - - [Fact] - public async Task ChallengeWillSetDefaults() - { - var stateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); - var queryValues = ExpectedQueryValues.Defaults(DefaultAuthority); - queryValues.State = OpenIdConnectDefaults.AuthenticationPropertiesKey + "=" + stateDataFormat.Protect(new AuthenticationProperties()); - var server = CreateServer(GetOptions(DefaultParameters(), queryValues)); - - var transaction = await SendAsync(server, DefaultHost + Challenge); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters()); - } - - [Fact] - public async Task ChallengeWillSetNonceAndStateCookies() - { - var server = CreateServer(new OpenIdConnectOptions - { - Authority = DefaultAuthority, - ClientId = "Test Id", - Configuration = TestUtilities.DefaultOpenIdConnectConfiguration - }); - var transaction = await SendAsync(server, DefaultHost + Challenge); - - var firstCookie = transaction.SetCookie.First(); - Assert.Contains(OpenIdConnectDefaults.CookieNoncePrefix, firstCookie); - Assert.Contains("expires", firstCookie); - - var secondCookie = transaction.SetCookie.Skip(1).First(); - Assert.StartsWith(".AspNetCore.Correlation.OpenIdConnect.", secondCookie); - Assert.Contains("expires", secondCookie); - } - - [Fact] - public async Task ChallengeWillUseOptionsProperties() - { - var queryValues = new ExpectedQueryValues(DefaultAuthority); - var server = CreateServer(GetOptions(DefaultParameters(), queryValues)); - - var transaction = await SendAsync(server, DefaultHost + Challenge); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters()); - } - - /// - /// Tests RedirectForAuthenticationContext replaces the OpenIdConnectMesssage correctly. - /// - /// Task - [Fact] - public async Task ChallengeSettingMessage() - { - var configuration = new OpenIdConnectConfiguration - { - AuthorizationEndpoint = ExpectedAuthorizeRequest, - }; - - var queryValues = new ExpectedQueryValues(DefaultAuthority, configuration) - { - RequestType = OpenIdConnectRequestType.AuthenticationRequest - }; - var server = CreateServer(GetProtocolMessageOptions()); - var transaction = await SendAsync(server, DefaultHost + Challenge); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, new string[] {}); - } - - /// - /// Tests RedirectForSignOutContext replaces the OpenIdConnectMesssage correctly. - /// - /// Task - [Fact] - public async Task SignOutSettingMessage() - { - var configuration = new OpenIdConnectConfiguration - { - EndSessionEndpoint = ExpectedLogoutRequest - }; - - var queryValues = new ExpectedQueryValues(DefaultAuthority, configuration) - { - RequestType = OpenIdConnectRequestType.LogoutRequest - }; - var server = CreateServer(GetProtocolMessageOptions()); - var transaction = await SendAsync(server, DefaultHost + Signout); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, new string[] { }); - } - - private static OpenIdConnectOptions GetProtocolMessageOptions() - { - var options = new OpenIdConnectOptions(); - var fakeOpenIdRequestMessage = new FakeOpenIdConnectMessage(ExpectedAuthorizeRequest, ExpectedLogoutRequest); - options.AutomaticChallenge = true; - options.Events = new OpenIdConnectEvents() - { - OnRedirectToIdentityProvider = (context) => - { - context.ProtocolMessage = fakeOpenIdRequestMessage; - return Task.FromResult(0); - }, - OnRedirectToIdentityProviderForSignOut = (context) => - { - context.ProtocolMessage = fakeOpenIdRequestMessage; - return Task.FromResult(0); - } - }; - return options; - } - - private class FakeOpenIdConnectMessage : OpenIdConnectMessage - { - private readonly string _authorizeRequest; - private readonly string _logoutRequest; - - public FakeOpenIdConnectMessage(string authorizeRequest, string logoutRequest) - { - _authorizeRequest = authorizeRequest; - _logoutRequest = logoutRequest; - } - - public override string CreateAuthenticationRequestUrl() - { - return _authorizeRequest; - } - - public override string CreateLogoutRequestUrl() - { - return _logoutRequest; - } - } - - /// - /// Tests for users who want to add 'state'. There are two ways to do it. - /// 1. Users set 'state' (OpenIdConnectMessage.State) in the event. The runtime appends to that state. - /// 2. Users add to the AuthenticationProperties (context.AuthenticationProperties), values will be serialized. - /// - /// - /// - [Theory, MemberData("StateDataSet")] - public async Task ChallengeSettingState(string userState, string challenge) - { - var queryValues = new ExpectedQueryValues(DefaultAuthority); - var stateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); - var properties = new AuthenticationProperties(); - if (challenge == ChallengeWithProperties) - { - properties.Items.Add("item1", Guid.NewGuid().ToString()); - } - - var options = GetOptions(DefaultParameters(new string[] { OpenIdConnectParameterNames.State }), queryValues, stateDataFormat); - options.AutomaticChallenge = challenge.Equals(ChallengeWithOutContext); - options.Events = new OpenIdConnectEvents() - { - OnRedirectToIdentityProvider = context => - { - context.ProtocolMessage.State = userState; - context.ProtocolMessage.RedirectUri = queryValues.RedirectUri; - return Task.FromResult(null); - } - - }; - var server = CreateServer(options, null, properties); - - var transaction = await SendAsync(server, DefaultHost + challenge); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - - if (challenge != ChallengeWithProperties) - { - if (userState != null) - { - properties.Items.Add(OpenIdConnectDefaults.UserstatePropertiesKey, userState); - } - properties.Items.Add(OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, queryValues.RedirectUri); - } - - queryValues.State = stateDataFormat.Protect(properties); - queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters(new string[] { OpenIdConnectParameterNames.State })); - } - - public static TheoryData StateDataSet - { - get - { - var dataset = new TheoryData(); - dataset.Add(Guid.NewGuid().ToString(), Challenge); - dataset.Add(null, Challenge); - dataset.Add(Guid.NewGuid().ToString(), ChallengeWithOutContext); - dataset.Add(null, ChallengeWithOutContext); - dataset.Add(Guid.NewGuid().ToString(), ChallengeWithProperties); - dataset.Add(null, ChallengeWithProperties); - - return dataset; - } - } - - [Fact] - public async Task ChallengeWillUseEvents() - { - var queryValues = new ExpectedQueryValues(DefaultAuthority); - var queryValuesSetInEvent = new ExpectedQueryValues(DefaultAuthority); - var options = GetOptions(DefaultParameters(), queryValues); - options.Events = new OpenIdConnectEvents() - { - OnRedirectToIdentityProvider = context => - { - context.ProtocolMessage.ClientId = queryValuesSetInEvent.ClientId; - context.ProtocolMessage.RedirectUri = queryValuesSetInEvent.RedirectUri; - context.ProtocolMessage.Resource = queryValuesSetInEvent.Resource; - context.ProtocolMessage.Scope = queryValuesSetInEvent.Scope; - return Task.FromResult(null); - } - }; - var server = CreateServer(options); - - var transaction = await SendAsync(server, DefaultHost + Challenge); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - queryValuesSetInEvent.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters()); - } - - private OpenIdConnectOptions GetOptions(List parameters, ExpectedQueryValues queryValues, ISecureDataFormat secureDataFormat = null) - { - var options = new OpenIdConnectOptions(); - foreach (var param in parameters) - { - if (param.Equals(OpenIdConnectParameterNames.ClientId)) - options.ClientId = queryValues.ClientId; - else if (param.Equals(OpenIdConnectParameterNames.Resource)) - options.Resource = queryValues.Resource; - else if (param.Equals(OpenIdConnectParameterNames.Scope)) { - options.Scope.Clear(); - - foreach (var scope in queryValues.Scope.Split(' ')) { - options.Scope.Add(scope); - } - } - } - - options.Authority = queryValues.Authority; - options.Configuration = queryValues.Configuration; - options.StateDataFormat = secureDataFormat ?? new AuthenticationPropertiesFormaterKeyValue(); - - return options; - } - - private List DefaultParameters(string[] additionalParams = null) - { - var parameters = - new List - { - OpenIdConnectParameterNames.ClientId, - OpenIdConnectParameterNames.Resource, - OpenIdConnectParameterNames.ResponseMode, - OpenIdConnectParameterNames.Scope, - }; - - if (additionalParams != null) - parameters.AddRange(additionalParams); - - return parameters; - } - - private static void DefaultChallengeOptions(OpenIdConnectOptions options) - { - options.AuthenticationScheme = "OpenIdConnectHandlerTest"; - options.AutomaticChallenge = true; - options.ClientId = Guid.NewGuid().ToString(); - options.ConfigurationManager = TestUtilities.DefaultOpenIdConnectConfigurationManager; - options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); - } - - [Fact] - public async Task SignOutWithDefaultRedirectUri() - { - var configuration = TestUtilities.DefaultOpenIdConnectConfiguration; - var server = CreateServer(new OpenIdConnectOptions - { - Authority = DefaultAuthority, - ClientId = "Test Id", - Configuration = configuration - }); - - var transaction = await SendAsync(server, DefaultHost + Signout); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal(configuration.EndSessionEndpoint, transaction.Response.Headers.Location.AbsoluteUri); - } - - [Fact] - public async Task SignOutWithCustomRedirectUri() - { - var configuration = TestUtilities.DefaultOpenIdConnectConfiguration; - var server = CreateServer(new OpenIdConnectOptions - { - Authority = DefaultAuthority, - ClientId = "Test Id", - Configuration = configuration, - PostLogoutRedirectUri = "https://example.com/logout" - }); - - var transaction = await SendAsync(server, DefaultHost + Signout); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Contains(UrlEncoder.Default.Encode("https://example.com/logout"), transaction.Response.Headers.Location.AbsoluteUri); - } - - [Fact] - public async Task SignOutWith_Specific_RedirectUri_From_Authentication_Properites() - { - var configuration = TestUtilities.DefaultOpenIdConnectConfiguration; - var server = CreateServer(new OpenIdConnectOptions - { - Authority = DefaultAuthority, - ClientId = "Test Id", - Configuration = configuration, - PostLogoutRedirectUri = "https://example.com/logout" - }); - - var transaction = await SendAsync(server, "https://example.com/signout_with_specific_redirect_uri"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Contains(UrlEncoder.Default.Encode("http://www.example.com/specific_redirect_uri"), transaction.Response.Headers.Location.AbsoluteUri); - } - - private static TestServer CreateServer(OpenIdConnectOptions options, Func handler = null, AuthenticationProperties properties = null) - { - var builder = new WebHostBuilder() - .Configure(app => - { - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme - }); - app.UseOpenIdConnectAuthentication(options); - app.Use(async (context, next) => - { - var req = context.Request; - var res = context.Response; - - if (req.Path == new PathString(Challenge)) - { - await context.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme); - } - else if (req.Path == new PathString(ChallengeWithProperties)) - { - await context.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, properties); - } - else if (req.Path == new PathString(ChallengeWithOutContext)) - { - res.StatusCode = 401; - } - else if (req.Path == new PathString(Signin)) - { - // REVIEW: this used to just be res.SignIn() - await context.Authentication.SignInAsync(OpenIdConnectDefaults.AuthenticationScheme, new ClaimsPrincipal()); - } - else if (req.Path == new PathString(Signout)) - { - await context.Authentication.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); - } - else if (req.Path == new PathString("/signout_with_specific_redirect_uri")) - { - await context.Authentication.SignOutAsync( - OpenIdConnectDefaults.AuthenticationScheme, - new AuthenticationProperties() { RedirectUri = "http://www.example.com/specific_redirect_uri" }); - } - else if (handler != null) - { - await handler(context); - } - else - { - await next(); - } - }); - }) - .ConfigureServices(services => - { - services.AddAuthentication(); - services.Configure(authOptions => - { - authOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; - }); - }); - return new TestServer(builder); - } - - private static async Task SendAsync(TestServer server, string uri, string cookieHeader = null) - { - var request = new HttpRequestMessage(HttpMethod.Get, uri); - if (!string.IsNullOrEmpty(cookieHeader)) - { - request.Headers.Add("Cookie", cookieHeader); - } - - var transaction = new Transaction - { - Request = request, - Response = await server.CreateClient().SendAsync(request), - }; - - if (transaction.Response.Headers.Contains("Set-Cookie")) - { - transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); - } - - transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); - if (transaction.Response.Content != null && - transaction.Response.Content.Headers.ContentType != null && - transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") - { - transaction.ResponseElement = XElement.Parse(transaction.ResponseText); - } - - return transaction; - } - - private class Transaction - { - public HttpRequestMessage Request { get; set; } - - public HttpResponseMessage Response { get; set; } - - public IList SetCookie { get; set; } - - public string ResponseText { get; set; } - - public XElement ResponseElement { get; set; } - - public string AuthenticationCookieValue - { - get - { - if (SetCookie != null && SetCookie.Count > 0) - { - var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNetCore.Cookie=")); - if (authCookie != null) - { - return authCookie.Substring(0, authCookie.IndexOf(';')); - } - } - - return null; - } - } - } - - [Fact] - // Test Cases for calculating the expiration time of cookie from cookie name - public void NonceCookieExpirationTime() - { - DateTime utcNow = DateTime.UtcNow; - - Assert.Equal(DateTime.MaxValue, GetNonceExpirationTime(noncePrefix + DateTime.MaxValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); - - Assert.Equal(DateTime.MinValue + TimeSpan.FromHours(1), GetNonceExpirationTime(noncePrefix + DateTime.MinValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); - - Assert.Equal(utcNow + TimeSpan.FromHours(1), GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); - - Assert.Equal(DateTime.MinValue, GetNonceExpirationTime(noncePrefix, TimeSpan.FromHours(1))); - - Assert.Equal(DateTime.MinValue, GetNonceExpirationTime("", TimeSpan.FromHours(1))); - - Assert.Equal(DateTime.MinValue, GetNonceExpirationTime(noncePrefix + noncePrefix, TimeSpan.FromHours(1))); - - Assert.Equal(utcNow + TimeSpan.FromHours(1), GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); - - Assert.Equal(DateTime.MinValue, GetNonceExpirationTime(utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); - } - - private static DateTime GetNonceExpirationTime(string keyname, TimeSpan nonceLifetime) - { - DateTime nonceTime = DateTime.MinValue; - string timestamp = null; - int endOfTimestamp; - if (keyname.StartsWith(noncePrefix, StringComparison.Ordinal)) - { - timestamp = keyname.Substring(noncePrefix.Length); - endOfTimestamp = timestamp.IndexOf('.'); - - if (endOfTimestamp != -1) - { - timestamp = timestamp.Substring(0, endOfTimestamp); - try - { - nonceTime = DateTime.FromBinary(Convert.ToInt64(timestamp, CultureInfo.InvariantCulture)); - if ((nonceTime >= DateTime.UtcNow) && ((DateTime.MaxValue - nonceTime) < nonceLifetime)) - nonceTime = DateTime.MaxValue; - else - nonceTime += nonceLifetime; - } - catch - { - } - } - } - - return nonceTime; - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectTests.cs new file mode 100644 index 000000000..da52e0e4c --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectTests.cs @@ -0,0 +1,351 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + public class OpenIdConnectTests + { + static string noncePrefix = "OpenIdConnect." + "Nonce."; + static string nonceDelimiter = "."; + const string DefaultHost = @"https://example.com"; + const string Logout = "/logout"; + const string Signin = "/signin"; + const string Signout = "/signout"; + + /// + /// Tests RedirectForSignOutContext replaces the OpenIdConnectMesssage correctly. + /// + /// Task + [Fact] + public async Task SignOutSettingMessage() + { + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.Configuration = new OpenIdConnectConfiguration + { + EndSessionEndpoint = "https://example.com/signout_test/signout_request" + }; + }); + + var server = setting.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + setting.ValidateSignoutRedirect( + transaction.Response.Headers.Location, + OpenIdConnectParameterNames.SkuTelemetry, + OpenIdConnectParameterNames.VersionTelemetry); + } + + [Fact] + public async Task RedirectToIdentityProvider_SetsNonceCookiePath_ToCallBackPath() + { + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.Configuration = new OpenIdConnectConfiguration + { + AuthorizationEndpoint = "https://example.com/provider/login" + }; + }); + + var server = setting.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Challenge); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie"); + var nonce = Assert.Single(setCookie.Value, v => v.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix)); + Assert.Contains("path=/signin-oidc", nonce); + } + + [Fact] + public async Task RedirectToIdentityProvider_NonceCookieOptions_CanBeOverriden() + { + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.Configuration = new OpenIdConnectConfiguration + { + AuthorizationEndpoint = "https://example.com/provider/login" + }; + opt.NonceCookie.Path = "/"; + }); + + var server = setting.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Challenge); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie"); + var nonce = Assert.Single(setCookie.Value, v => v.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix)); + Assert.Contains("path=/", nonce); + } + + [Fact] + public async Task RedirectToIdentityProvider_SetsCorrelationIdCookiePath_ToCallBackPath() + { + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.Configuration = new OpenIdConnectConfiguration + { + AuthorizationEndpoint = "https://example.com/provider/login" + }; + }); + + var server = setting.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Challenge); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie"); + var correlation = Assert.Single(setCookie.Value, v => v.StartsWith(".AspNetCore.Correlation.")); + Assert.Contains("path=/signin-oidc", correlation); + } + + [Fact] + public async Task RedirectToIdentityProvider_CorrelationIdCookieOptions_CanBeOverriden() + { + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.Configuration = new OpenIdConnectConfiguration + { + AuthorizationEndpoint = "https://example.com/provider/login" + }; + opt.CorrelationCookie.Path = "/"; + }); + + var server = setting.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Challenge); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + var setCookie = Assert.Single(res.Headers, h => h.Key == "Set-Cookie"); + var correlation = Assert.Single(setCookie.Value, v => v.StartsWith(".AspNetCore.Correlation.")); + Assert.Contains("path=/", correlation); + } + + [Fact] + public async Task EndSessionRequestDoesNotIncludeTelemetryParametersWhenDisabled() + { + var configuration = TestServerBuilder.CreateDefaultOpenIdConnectConfiguration(); + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Configuration = configuration; + opt.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opt.DisableTelemetry = true; + }); + + var server = setting.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.DoesNotContain(OpenIdConnectParameterNames.SkuTelemetry, res.Headers.Location.Query); + Assert.DoesNotContain(OpenIdConnectParameterNames.VersionTelemetry, res.Headers.Location.Query); + setting.ValidateSignoutRedirect(transaction.Response.Headers.Location); + } + + [Fact] + public async Task SignOutFormPostWithDefaultRedirectUri() + { + var settings = new TestSettings(o => + { + o.AuthenticationMethod = OpenIdConnectRedirectBehavior.FormPost; + o.Authority = TestServerBuilder.DefaultAuthority; + o.ClientId = "Test Id"; + }); + var server = settings.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + + settings.ValidateSignoutFormPost(transaction, + OpenIdConnectParameterNames.PostLogoutRedirectUri); + } + + [Fact] + public async Task SignOutRedirectWithDefaultRedirectUri() + { + var settings = new TestSettings(o => + { + o.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet; + o.Authority = TestServerBuilder.DefaultAuthority; + o.ClientId = "Test Id"; + }); + var server = settings.CreateTestServer(); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + settings.ValidateSignoutRedirect(transaction.Response.Headers.Location, + OpenIdConnectParameterNames.PostLogoutRedirectUri); + } + + [Fact] + public async Task SignOutWithCustomRedirectUri() + { + var configuration = TestServerBuilder.CreateDefaultOpenIdConnectConfiguration(); + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("OIDCTest")); + var server = TestServerBuilder.CreateServer(o => + { + o.Authority = TestServerBuilder.DefaultAuthority; + o.ClientId = "Test Id"; + o.Configuration = configuration; + o.StateDataFormat = stateFormat; + o.SignedOutCallbackPath = "/thelogout"; + o.SignedOutRedirectUri = "https://example.com/postlogout"; + }); + + var transaction = await server.SendAsync(DefaultHost + TestServerBuilder.Signout); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + var query = transaction.Response.Headers.Location.Query.Substring(1).Split('&') + .Select(each => each.Split('=')) + .ToDictionary(pair => pair[0], pair => pair[1]); + + string redirectUri; + Assert.True(query.TryGetValue("post_logout_redirect_uri", out redirectUri)); + Assert.Equal(UrlEncoder.Default.Encode("https://example.com/thelogout"), redirectUri, true); + + string state; + Assert.True(query.TryGetValue("state", out state)); + var properties = stateFormat.Unprotect(state); + Assert.Equal("https://example.com/postlogout", properties.RedirectUri, true); + } + + [Fact] + public async Task SignOutWith_Specific_RedirectUri_From_Authentication_Properites() + { + var configuration = TestServerBuilder.CreateDefaultOpenIdConnectConfiguration(); + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("OIDCTest")); + var server = TestServerBuilder.CreateServer(o => + { + o.Authority = TestServerBuilder.DefaultAuthority; + o.StateDataFormat = stateFormat; + o.ClientId = "Test Id"; + o.Configuration = configuration; + o.SignedOutRedirectUri = "https://example.com/postlogout"; + }); + + var transaction = await server.SendAsync("https://example.com/signout_with_specific_redirect_uri"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + var query = transaction.Response.Headers.Location.Query.Substring(1).Split('&') + .Select(each => each.Split('=')) + .ToDictionary(pair => pair[0], pair => pair[1]); + + string redirectUri; + Assert.True(query.TryGetValue("post_logout_redirect_uri", out redirectUri)); + Assert.Equal(UrlEncoder.Default.Encode("https://example.com/signout-callback-oidc"), redirectUri, true); + + string state; + Assert.True(query.TryGetValue("state", out state)); + var properties = stateFormat.Unprotect(state); + Assert.Equal("http://www.example.com/specific_redirect_uri", properties.RedirectUri, true); + } + + [Fact] + public async Task SignOut_WithMissingConfig_Throws() + { + var setting = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Configuration = new OpenIdConnectConfiguration(); + }); + var server = setting.CreateTestServer(); + + var exception = await Assert.ThrowsAsync(() => server.SendAsync(DefaultHost + TestServerBuilder.Signout)); + Assert.Equal("Cannot redirect to the end session endpoint, the configuration may be missing or invalid.", exception.Message); + } + + // Test Cases for calculating the expiration time of cookie from cookie name + [Fact] + public void NonceCookieExpirationTime() + { + DateTime utcNow = DateTime.UtcNow; + + Assert.Equal(DateTime.MaxValue, GetNonceExpirationTime(noncePrefix + DateTime.MaxValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); + + Assert.Equal(DateTime.MinValue + TimeSpan.FromHours(1), GetNonceExpirationTime(noncePrefix + DateTime.MinValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); + + Assert.Equal(utcNow + TimeSpan.FromHours(1), GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); + + Assert.Equal(DateTime.MinValue, GetNonceExpirationTime(noncePrefix, TimeSpan.FromHours(1))); + + Assert.Equal(DateTime.MinValue, GetNonceExpirationTime("", TimeSpan.FromHours(1))); + + Assert.Equal(DateTime.MinValue, GetNonceExpirationTime(noncePrefix + noncePrefix, TimeSpan.FromHours(1))); + + Assert.Equal(utcNow + TimeSpan.FromHours(1), GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); + + Assert.Equal(DateTime.MinValue, GetNonceExpirationTime(utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1))); + } + + private static DateTime GetNonceExpirationTime(string keyname, TimeSpan nonceLifetime) + { + DateTime nonceTime = DateTime.MinValue; + string timestamp = null; + int endOfTimestamp; + if (keyname.StartsWith(noncePrefix, StringComparison.Ordinal)) + { + timestamp = keyname.Substring(noncePrefix.Length); + endOfTimestamp = timestamp.IndexOf('.'); + + if (endOfTimestamp != -1) + { + timestamp = timestamp.Substring(0, endOfTimestamp); + try + { + nonceTime = DateTime.FromBinary(Convert.ToInt64(timestamp, CultureInfo.InvariantCulture)); + if ((nonceTime >= DateTime.UtcNow) && ((DateTime.MaxValue - nonceTime) < nonceLifetime)) + nonceTime = DateTime.MaxValue; + else + nonceTime += nonceLifetime; + } + catch + { + } + } + } + + return nonceTime; + } + + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerBuilder.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerBuilder.cs new file mode 100644 index 000000000..c37da8c04 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerBuilder.cs @@ -0,0 +1,120 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + internal class TestServerBuilder + { + public static readonly string DefaultAuthority = @"https://login.microsoftonline.com/common"; + public static readonly string TestHost = @"https://example.com"; + public static readonly string Challenge = "/challenge"; + public static readonly string ChallengeWithOutContext = "/challengeWithOutContext"; + public static readonly string ChallengeWithProperties = "/challengeWithProperties"; + public static readonly string Signin = "/signin"; + public static readonly string Signout = "/signout"; + + public static OpenIdConnectOptions CreateOpenIdConnectOptions() => + new OpenIdConnectOptions + { + Authority = DefaultAuthority, + ClientId = Guid.NewGuid().ToString(), + Configuration = CreateDefaultOpenIdConnectConfiguration() + }; + + public static OpenIdConnectOptions CreateOpenIdConnectOptions(Action update) + { + var options = CreateOpenIdConnectOptions(); + update?.Invoke(options); + return options; + } + + public static OpenIdConnectConfiguration CreateDefaultOpenIdConnectConfiguration() => + new OpenIdConnectConfiguration() + { + AuthorizationEndpoint = DefaultAuthority + "/oauth2/authorize", + EndSessionEndpoint = DefaultAuthority + "/oauth2/endsessionendpoint", + TokenEndpoint = DefaultAuthority + "/oauth2/token" + }; + + public static IConfigurationManager CreateDefaultOpenIdConnectConfigurationManager() => + new StaticConfigurationManager(CreateDefaultOpenIdConnectConfiguration()); + + public static TestServer CreateServer(Action options) + { + return CreateServer(options, handler: null, properties: null); + } + + public static TestServer CreateServer( + Action options, + Func handler, + AuthenticationProperties properties) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + + if (req.Path == new PathString(Challenge)) + { + await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString(ChallengeWithProperties)) + { + await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, properties); + } + else if (req.Path == new PathString(ChallengeWithOutContext)) + { + res.StatusCode = 401; + } + else if (req.Path == new PathString(Signin)) + { + await context.SignInAsync(OpenIdConnectDefaults.AuthenticationScheme, new ClaimsPrincipal()); + } + else if (req.Path == new PathString(Signout)) + { + await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString("/signout_with_specific_redirect_uri")) + { + await context.SignOutAsync( + OpenIdConnectDefaults.AuthenticationScheme, + new AuthenticationProperties() { RedirectUri = "http://www.example.com/specific_redirect_uri" }); + } + else if (handler != null) + { + await handler(context); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie() + .AddOpenIdConnect(options); + }); + + return new TestServer(builder); + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerExtensions.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerExtensions.cs new file mode 100644 index 000000000..609aed6f6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerExtensions.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.TestHost; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + internal static class TestServerExtensions + { + public static Task SendAsync(this TestServer server, string url) + { + return SendAsync(server, url, cookieHeader: null); + } + + public static async Task SendAsync(this TestServer server, string uri, string cookieHeader) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(cookieHeader)) + { + request.Headers.Add("Cookie", cookieHeader); + } + + var transaction = new TestTransaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); + } + + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + + return transaction; + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs new file mode 100644 index 000000000..eb045b438 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs @@ -0,0 +1,350 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.TestHost; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + /// + /// This helper class is used to check that query string parameters are as expected. + /// + internal class TestSettings + { + private readonly Action _configureOptions; + private OpenIdConnectOptions _options; + + public TestSettings() : this(configure: null) + { + } + + public TestSettings(Action configure) + { + _configureOptions = o => + { + configure?.Invoke(o); + _options = o; + _options.BackchannelHttpHandler = new MockBackchannel(); + }; + } + + public UrlEncoder Encoder => UrlEncoder.Default; + + public string ExpectedState { get; set; } + + public TestServer CreateTestServer(AuthenticationProperties properties = null) => TestServerBuilder.CreateServer(_configureOptions, handler: null, properties: properties); + + public IDictionary ValidateChallengeFormPost(string responseBody, params string[] parametersToValidate) + { + IDictionary formInputs = null; + var errors = new List(); + var xdoc = XDocument.Parse(responseBody.Replace("doctype", "DOCTYPE")); + var forms = xdoc.Descendants("form"); + if (forms.Count() != 1) + { + errors.Add("Only one form element is expected in response body."); + } + else + { + formInputs = forms.Single() + .Elements("input") + .ToDictionary(elem => elem.Attribute("name").Value, + elem => elem.Attribute("value").Value); + + ValidateParameters(formInputs, parametersToValidate, errors, htmlEncoded: false); + } + + if (errors.Any()) + { + var buf = new StringBuilder(); + buf.AppendLine($"The challenge form post is not valid."); + // buf.AppendLine(); + + foreach (var error in errors) + { + buf.AppendLine(error); + } + + Debug.WriteLine(buf.ToString()); + Assert.True(false, buf.ToString()); + } + + return formInputs; + } + + public IDictionary ValidateSignoutFormPost(TestTransaction transaction, params string[] parametersToValidate) + { + IDictionary formInputs = null; + var errors = new List(); + var xdoc = XDocument.Parse(transaction.ResponseText.Replace("doctype", "DOCTYPE")); + var forms = xdoc.Descendants("form"); + if (forms.Count() != 1) + { + errors.Add("Only one form element is expected in response body."); + } + else + { + formInputs = forms.Single() + .Elements("input") + .ToDictionary(elem => elem.Attribute("name").Value, + elem => elem.Attribute("value").Value); + + ValidateParameters(formInputs, parametersToValidate, errors, htmlEncoded: false); + } + + if (errors.Any()) + { + var buf = new StringBuilder(); + buf.AppendLine($"The signout form post is not valid."); + // buf.AppendLine(); + + foreach (var error in errors) + { + buf.AppendLine(error); + } + + Debug.WriteLine(buf.ToString()); + Assert.True(false, buf.ToString()); + } + + return formInputs; + } + + public IDictionary ValidateChallengeRedirect(Uri redirectUri, params string[] parametersToValidate) => + ValidateRedirectCore(redirectUri, OpenIdConnectRequestType.Authentication, parametersToValidate); + + public IDictionary ValidateSignoutRedirect(Uri redirectUri, params string[] parametersToValidate) => + ValidateRedirectCore(redirectUri, OpenIdConnectRequestType.Logout, parametersToValidate); + + private IDictionary ValidateRedirectCore(Uri redirectUri, OpenIdConnectRequestType requestType, string[] parametersToValidate) + { + var errors = new List(); + + // Validate the authority + ValidateExpectedAuthority(redirectUri.AbsoluteUri, errors, requestType); + + // Convert query to dictionary + var queryDict = string.IsNullOrEmpty(redirectUri.Query) ? + new Dictionary() : + redirectUri.Query.TrimStart('?').Split('&').Select(part => part.Split('=')).ToDictionary(parts => parts[0], parts => parts[1]); + + // Validate the query string parameters + ValidateParameters(queryDict, parametersToValidate, errors, htmlEncoded: true); + + if (errors.Any()) + { + var buf = new StringBuilder(); + buf.AppendLine($"The redirect uri is not valid."); + buf.AppendLine(redirectUri.AbsoluteUri); + + foreach (var error in errors) + { + buf.AppendLine(error); + } + + Debug.WriteLine(buf.ToString()); + Assert.True(false, buf.ToString()); + } + + return queryDict; + } + + private void ValidateParameters( + IDictionary actualValues, + IEnumerable parametersToValidate, + ICollection errors, + bool htmlEncoded) + { + foreach (var paramToValidate in parametersToValidate) + { + switch (paramToValidate) + { + case OpenIdConnectParameterNames.ClientId: + ValidateClientId(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.ResponseType: + ValidateResponseType(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.ResponseMode: + ValidateResponseMode(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.Scope: + ValidateScope(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.RedirectUri: + ValidateRedirectUri(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.Resource: + ValidateResource(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.State: + ValidateState(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.SkuTelemetry: + ValidateSkuTelemetry(actualValues, errors); + break; + case OpenIdConnectParameterNames.VersionTelemetry: + ValidateVersionTelemetry(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.PostLogoutRedirectUri: + ValidatePostLogoutRedirectUri(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.MaxAge: + ValidateMaxAge(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.Prompt: + ValidatePrompt(actualValues, errors, htmlEncoded); + break; + default: + throw new InvalidOperationException($"Unknown parameter \"{paramToValidate}\"."); + } + } + } + + private void ValidateExpectedAuthority(string absoluteUri, ICollection errors, OpenIdConnectRequestType requestType) + { + string expectedAuthority; + switch (requestType) + { + case OpenIdConnectRequestType.Token: + expectedAuthority = _options.Configuration?.TokenEndpoint ?? _options.Authority + @"/oauth2/token"; + break; + case OpenIdConnectRequestType.Logout: + expectedAuthority = _options.Configuration?.EndSessionEndpoint ?? _options.Authority + @"/oauth2/logout"; + break; + default: + expectedAuthority = _options.Configuration?.AuthorizationEndpoint ?? _options.Authority + @"/oauth2/authorize"; + break; + } + + if (!absoluteUri.StartsWith(expectedAuthority)) + { + errors.Add($"ExpectedAuthority: {expectedAuthority}"); + } + } + + private void ValidateClientId(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.ClientId, _options.ClientId, actualParams, errors, htmlEncoded); + + private void ValidateResponseType(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.ResponseType, _options.ResponseType, actualParams, errors, htmlEncoded); + + private void ValidateResponseMode(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.ResponseMode, _options.ResponseMode, actualParams, errors, htmlEncoded); + + private void ValidateScope(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.Scope, string.Join(" ", _options.Scope), actualParams, errors, htmlEncoded); + + private void ValidateRedirectUri(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.RedirectUri, TestServerBuilder.TestHost + _options.CallbackPath, actualParams, errors, htmlEncoded); + + private void ValidateResource(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.RedirectUri, _options.Resource, actualParams, errors, htmlEncoded); + + private void ValidateState(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.State, ExpectedState, actualParams, errors, htmlEncoded); + + private static void ValidateSkuTelemetry(IDictionary actualParams, ICollection errors) + { + if (!actualParams.ContainsKey(OpenIdConnectParameterNames.SkuTelemetry)) + { + errors.Add($"Parameter {OpenIdConnectParameterNames.SkuTelemetry} is missing"); + } + } + + private void ValidateVersionTelemetry(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.VersionTelemetry, typeof(OpenIdConnectMessage).GetTypeInfo().Assembly.GetName().Version.ToString(), actualParams, errors, htmlEncoded); + + private void ValidatePostLogoutRedirectUri(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.PostLogoutRedirectUri, "https://example.com/signout-callback-oidc", actualParams, errors, htmlEncoded); + + private void ValidateMaxAge(IDictionary actualQuery, ICollection errors, bool htmlEncoded) + { + if(_options.MaxAge.HasValue) + { + Assert.Equal(TimeSpan.FromMinutes(20), _options.MaxAge.Value); + string expectedMaxAge = "1200"; + ValidateParameter(OpenIdConnectParameterNames.MaxAge, expectedMaxAge, actualQuery, errors, htmlEncoded); + } + else if(actualQuery.ContainsKey(OpenIdConnectParameterNames.MaxAge)) + { + errors.Add($"Parameter {OpenIdConnectParameterNames.MaxAge} is present but it should be absent"); + } + } + + private void ValidatePrompt(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.Prompt, _options.Prompt, actualParams, errors, htmlEncoded); + + private void ValidateParameter( + string parameterName, + string expectedValue, + IDictionary actualParams, + ICollection errors, + bool htmlEncoded) + { + string actualValue; + if (actualParams.TryGetValue(parameterName, out actualValue)) + { + if (htmlEncoded) + { + expectedValue = Encoder.Encode(expectedValue); + } + + if (actualValue != expectedValue) + { + errors.Add($"Parameter {parameterName}'s expected value is '{expectedValue}' but its actual value is '{actualValue}'"); + } + } + else + { + errors.Add($"Parameter {parameterName} is missing"); + } + } + + private class MockBackchannel : HttpMessageHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.RequestUri.AbsoluteUri.Equals("https://login.microsoftonline.com/common/.well-known/openid-configuration")) + { + return await ReturnResource("wellknownconfig.json"); + } + if (request.RequestUri.AbsoluteUri.Equals("https://login.microsoftonline.com/common/discovery/keys")) + { + return await ReturnResource("wellknownkeys.json"); + } + + throw new NotImplementedException(); + } + + private async Task ReturnResource(string resource) + { + var resourceName = "Microsoft.AspNetCore.Authentication.Test.OpenIdConnect." + resource; + using (var stream = typeof(MockBackchannel).Assembly.GetManifestResourceStream(resourceName)) + using (var reader = new StreamReader(stream)) + { + var body = await reader.ReadToEndAsync(); + var content = new StringContent(body, Encoding.UTF8, "application/json"); + return new HttpResponseMessage() + { + Content = content, + }; + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestTransaction.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestTransaction.cs new file mode 100644 index 000000000..4f924172c --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestTransaction.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Xml.Linq; + +namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect +{ + internal class TestTransaction + { + public HttpRequestMessage Request { get; set; } + + public HttpResponseMessage Response { get; set; } + + public IList SetCookie { get; set; } + + public string ResponseText { get; set; } + + public XElement ResponseElement { get; set; } + + public string AuthenticationCookieValue + { + get + { + if (SetCookie != null && SetCookie.Count > 0) + { + var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNetCore.Cookie=")); + if (authCookie != null) + { + return authCookie.Substring(0, authCookie.IndexOf(';')); + } + } + + return null; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestUtilities.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestUtilities.cs deleted file mode 100644 index 6247c85b4..000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestUtilities.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.IdentityModel.Protocols; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; - -namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect -{ - /// - /// These utilities are designed to test openidconnect related flows - /// - public class TestUtilities - { - public const string DefaultHost = @"http://localhost"; - - public static IConfigurationManager DefaultOpenIdConnectConfigurationManager - { - get - { - return new StaticConfigurationManager(DefaultOpenIdConnectConfiguration); - } - } - - public static OpenIdConnectConfiguration DefaultOpenIdConnectConfiguration - { - get - { - return new OpenIdConnectConfiguration() - { - AuthorizationEndpoint = @"https://login.microsoftonline.com/common/oauth2/authorize", - EndSessionEndpoint = @"https://login.microsoftonline.com/common/oauth2/endsessionendpoint", - TokenEndpoint = @"https://login.microsoftonline.com/common/oauth2/token", - }; - } - } - } -} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownconfig.json b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownconfig.json new file mode 100644 index 000000000..4d46a8cf0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownconfig.json @@ -0,0 +1,23 @@ +{ + "authorization_endpoint": "https://login.microsoftonline.com/common/oauth2/authorize", + "token_endpoint": "https://login.microsoftonline.com/common/oauth2/token", + "token_endpoint_auth_methods_supported": [ "client_secret_post", "private_key_jwt", "client_secret_basic" ], + "jwks_uri": "https://login.microsoftonline.com/common/discovery/keys", + "response_modes_supported": [ "query", "fragment", "form_post" ], + "subject_types_supported": [ "pairwise" ], + "id_token_signing_alg_values_supported": [ "RS256" ], + "http_logout_supported": true, + "frontchannel_logout_supported": true, + "end_session_endpoint": "https://login.microsoftonline.com/common/oauth2/logout", + "response_types_supported": [ "code", "id_token", "code id_token", "token id_token", "token" ], + "scopes_supported": [ "openid" ], + "issuer": "https://sts.windows.net/{tenantid}/", + "claims_supported": [ "sub", "iss", "cloud_instance_name", "cloud_instance_host_name", "cloud_graph_host_name", "msgraph_host", "aud", "exp", "iat", "auth_time", "acr", "amr", "nonce", "email", "given_name", "family_name", "nickname" ], + "microsoft_multi_refresh_token": true, + "check_session_iframe": "https://login.microsoftonline.com/common/oauth2/checksession", + "userinfo_endpoint": "https://login.microsoftonline.com/common/openid/userinfo", + "tenant_region_scope": null, + "cloud_instance_name": "microsoftonline.com", + "cloud_graph_host_name": "graph.windows.net", + "msgraph_host": "graph.microsoft.com" +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownkeys.json b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownkeys.json new file mode 100644 index 000000000..77cc5562a --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/wellknownkeys.json @@ -0,0 +1,31 @@ +{ + "keys": [ + { + "kty": "RSA", + "use": "sig", + "kid": "SSQdhI1cKvhQEDSJxE2gGYs40Q0", + "x5t": "SSQdhI1cKvhQEDSJxE2gGYs40Q0", + "n": "pJUB90EMxiNjgkVz5CLLUuG5bYwirL2LXfVsq_nnY686WzbinkvFnNs6LvrJ6DWD5NV1-0Tq2eZj7WU8H9ytmDPsRnJ0b49gRCJYOg6-SdOe9Tl0lB0IBJE1aWh3OdCVrZLE4LH4-LGIDrkwnCV8dKFkO3EIUYPaEysL4g4wLx-TCfpMWE37XC09P-nBRVkRNcihrzY38_MC42NkRdDwByZemXkQKddnn5Y5o4rVzPGqQy3vjmTjKolYEIBYa7n3yF0848MG0k338bjnyceJgmZzjxttkWTVDikQXSldbu3QCrCAlipbWPUAXaZK8buY8LP80G4U_wx4LuZ_Krq5OQ", + "e": "AQAB", + "x5c": [ "MIIDBTCCAe2gAwIBAgIQHJ7yHxNEM7tBeqcRTMBhhTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDEwODAwMDAwMFoXDTIwMDEwOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKSVAfdBDMYjY4JFc+Qiy1LhuW2MIqy9i131bKv552OvOls24p5LxZzbOi76yeg1g+TVdftE6tnmY+1lPB/crZgz7EZydG+PYEQiWDoOvknTnvU5dJQdCASRNWlodznQla2SxOCx+PixiA65MJwlfHShZDtxCFGD2hMrC+IOMC8fkwn6TFhN+1wtPT/pwUVZETXIoa82N/PzAuNjZEXQ8AcmXpl5ECnXZ5+WOaOK1czxqkMt745k4yqJWBCAWGu598hdPOPDBtJN9/G458nHiYJmc48bbZFk1Q4pEF0pXW7t0AqwgJYqW1j1AF2mSvG7mPCz/NBuFP8MeC7mfyq6uTkCAwEAAaMhMB8wHQYDVR0OBBYEFFVWt/4iTcBiWk+EX1dCKZGoAlBQMA0GCSqGSIb3DQEBCwUAA4IBAQCJrLUiCfldfDFhZLG08DpLcwpn8Mvhm61ZpPANyCawRgaoBOsxFJew1qpC2wfXh3yUwJEIkkTGXAsiWENkeqmjd2XXQwZuIHIo5/KyZLfo2m1CmEH/QXL6uRx1f8liPr4efC8H+7/sduf6nPfk0UpNsAHA6RxJFy2jg2wHU+Ux4jK4Gc5d/rJPhJhvyS9Zax1hbTlf+N32ZvS780VMDb/nf2LdtACL0ya/+KSDGVXS3GhS9FLEXrBNjq921ghVIFJhPzm54yfeupIwz+zfISoTHIYw37i7iNUbkvDrm14a27pwLS/tfSuJHKcGPt1sjMu6SS/pf1BlvdoFkKdLLaUb" ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "FSimuFrFNoC0sJXGmv13nNZceDc", + "x5t": "FSimuFrFNoC0sJXGmv13nNZceDc", + "n": "yCYaJF8uHoV2L31cjZUDdcodK1Y1EsTLkDD-DEXFyGeHaQ92T9t6MU6zazBzHvJRarG6OMI1GwsFxZ9opSVOeuRjuL3H2ehmUyuKOAnL8uT4cfkdfbg9AIN_63COccfFn0br_xUszZ7lkF5mb63sze-G66YQcbdTCWgsXpxR6491b57Gc4HVTV8cEgU4byezhJIiirrPDmt23QJIjr6XtvUMSNW88u0kX7PKOUnVCns2AG8DB2I-JExTiXwhFVu5JUqgpgmjIngvd5eyNzOgFJMnpWNXabKDP3oMLvQxjdq9xwWuTu0IQLpmUxEF9jVc8vKV1Pu2xHcS7ON5xJrUzw", + "e": "AQAB", + "x5c": [ "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk" ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "2S4SCVGs8Sg9LS6AqLIq6DpW-g8", + "x5t": "2S4SCVGs8Sg9LS6AqLIq6DpW-g8", + "n": "oZ-QQrNuB4ei9ATYrT61ebPtvwwYWnsrTpp4ISSp6niZYb92XM0oUTNgqd_C1vGN8J-y9wCbaJWkpBf46CjdZehrqczPhzhHau8WcRXocSB1u_tuZhv1ooAZ4bAcy79UkeLiG60HkuTNJJC8CfaTp1R97szBhuk0Vz5yt4r5SpfewIlBCnZUYwkDS172H9WapQu-3P2Qjh0l-JLyCkdrhvizZUk0atq5_AIDKRU-A0pRGc-EZhUL0LqUMz6c6M2s_4GnQaScv44A5iZUDD15B6e8Apb2yARohkWmOnmRcTVfes8EkfxjzZEzm3cNkvP0ogILyISHKlkzy2OmlU6iXw", + "e": "AQAB", + "x5c": [ "MIIDKDCCAhCgAwIBAgIQBHJvVNxP1oZO4HYKh+rypDANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMTYxMTE2MDgwMDAwWhcNMTgxMTE2MDgwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChn5BCs24Hh6L0BNitPrV5s+2/DBhaeytOmnghJKnqeJlhv3ZczShRM2Cp38LW8Y3wn7L3AJtolaSkF/joKN1l6GupzM+HOEdq7xZxFehxIHW7+25mG/WigBnhsBzLv1SR4uIbrQeS5M0kkLwJ9pOnVH3uzMGG6TRXPnK3ivlKl97AiUEKdlRjCQNLXvYf1ZqlC77c/ZCOHSX4kvIKR2uG+LNlSTRq2rn8AgMpFT4DSlEZz4RmFQvQupQzPpzozaz/gadBpJy/jgDmJlQMPXkHp7wClvbIBGiGRaY6eZFxNV96zwSR/GPNkTObdw2S8/SiAgvIhIcqWTPLY6aVTqJfAgMBAAGjWDBWMFQGA1UdAQRNMEuAEDUj0BrjP0RTbmoRPTRMY3WhJTAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXOCEARyb1TcT9aGTuB2Cofq8qQwDQYJKoZIhvcNAQELBQADggEBAGnLhDHVz2gLDiu9L34V3ro/6xZDiSWhGyHcGqky7UlzQH3pT5so8iF5P0WzYqVtogPsyC2LPJYSTt2vmQugD4xlu/wbvMFLcV0hmNoTKCF1QTVtEQiAiy0Aq+eoF7Al5fV1S3Sune0uQHimuUFHCmUuF190MLcHcdWnPAmzIc8fv7quRUUsExXmxSX2ktUYQXzqFyIOSnDCuWFm6tpfK5JXS8fW5bpqTlrysXXz/OW/8NFGq/alfjrya4ojrOYLpunGriEtNPwK7hxj1AlCYEWaRHRXaUIW1ByoSff/6Y6+ZhXPUe0cDlNRt/qIz5aflwO7+W8baTS4O8m/icu7ItE=" ] + } + ] +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/PolicyTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/PolicyTests.cs new file mode 100644 index 000000000..368026beb --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/PolicyTests.cs @@ -0,0 +1,487 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public class PolicyTests + { + [Fact] + public async Task CanDispatch() + { + var server = CreateServer(services => + { + services.AddLogging().AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + o.AddScheme("auth3", "auth3"); + }) + .AddPolicyScheme("policy1", "policy1", p => + { + p.ForwardDefault = "auth1"; + }) + .AddPolicyScheme("policy2", "policy2", p => + { + p.ForwardAuthenticate = "auth2"; + }); + }); + + var transaction = await server.SendAsync("http://example.com/auth/policy1"); + Assert.Equal("auth1", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth1")); + + transaction = await server.SendAsync("http://example.com/auth/auth1"); + Assert.Equal("auth1", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth1")); + + transaction = await server.SendAsync("http://example.com/auth/auth2"); + Assert.Equal("auth2", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth2")); + + transaction = await server.SendAsync("http://example.com/auth/auth3"); + Assert.Equal("auth3", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth3")); + + transaction = await server.SendAsync("http://example.com/auth/policy2"); + Assert.Equal("auth2", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth2")); + } + + [Fact] + public async Task DefaultTargetSelectorWinsOverDefaultTarget() + { + var services = new ServiceCollection().AddOptions().AddLogging(); + services.AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + }) + .AddPolicyScheme("forward", "forward", p => { + p.ForwardDefault= "auth2"; + p.ForwardDefaultSelector = ctx => "auth1"; + }); + + var handler1 = new TestHandler(); + services.AddSingleton(handler1); + var handler2 = new TestHandler2(); + services.AddSingleton(handler2); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, handler1.AuthenticateCount); + Assert.Equal(0, handler1.ForbidCount); + Assert.Equal(0, handler1.ChallengeCount); + Assert.Equal(0, handler1.SignInCount); + Assert.Equal(0, handler1.SignOutCount); + Assert.Equal(0, handler2.AuthenticateCount); + Assert.Equal(0, handler2.ForbidCount); + Assert.Equal(0, handler2.ChallengeCount); + Assert.Equal(0, handler2.SignInCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.AuthenticateAsync("forward"); + Assert.Equal(1, handler1.AuthenticateCount); + Assert.Equal(0, handler2.AuthenticateCount); + + await context.ForbidAsync("forward"); + Assert.Equal(1, handler1.ForbidCount); + Assert.Equal(0, handler2.ForbidCount); + + await context.ChallengeAsync("forward"); + Assert.Equal(1, handler1.ChallengeCount); + Assert.Equal(0, handler2.ChallengeCount); + + await context.SignOutAsync("forward"); + Assert.Equal(1, handler1.SignOutCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.SignInAsync("forward", new ClaimsPrincipal()); + Assert.Equal(1, handler1.SignInCount); + Assert.Equal(0, handler2.SignInCount); + } + + [Fact] + public async Task NullDefaultTargetSelectorFallsBacktoDefaultTarget() + { + var services = new ServiceCollection().AddOptions().AddLogging(); + services.AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + }) + .AddPolicyScheme("forward", "forward", p => { + p.ForwardDefault= "auth1"; + p.ForwardDefaultSelector = ctx => null; + }); + + var handler1 = new TestHandler(); + services.AddSingleton(handler1); + var handler2 = new TestHandler2(); + services.AddSingleton(handler2); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, handler1.AuthenticateCount); + Assert.Equal(0, handler1.ForbidCount); + Assert.Equal(0, handler1.ChallengeCount); + Assert.Equal(0, handler1.SignInCount); + Assert.Equal(0, handler1.SignOutCount); + Assert.Equal(0, handler2.AuthenticateCount); + Assert.Equal(0, handler2.ForbidCount); + Assert.Equal(0, handler2.ChallengeCount); + Assert.Equal(0, handler2.SignInCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.AuthenticateAsync("forward"); + Assert.Equal(1, handler1.AuthenticateCount); + Assert.Equal(0, handler2.AuthenticateCount); + + await context.ForbidAsync("forward"); + Assert.Equal(1, handler1.ForbidCount); + Assert.Equal(0, handler2.ForbidCount); + + await context.ChallengeAsync("forward"); + Assert.Equal(1, handler1.ChallengeCount); + Assert.Equal(0, handler2.ChallengeCount); + + await context.SignOutAsync("forward"); + Assert.Equal(1, handler1.SignOutCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.SignInAsync("forward", new ClaimsPrincipal()); + Assert.Equal(1, handler1.SignInCount); + Assert.Equal(0, handler2.SignInCount); + } + + [Fact] + public async Task SpecificTargetAlwaysWinsOverDefaultTarget() + { + var services = new ServiceCollection().AddOptions().AddLogging(); + services.AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + }) + .AddPolicyScheme("forward", "forward", p => { + p.ForwardDefault= "auth2"; + p.ForwardDefaultSelector = ctx => "auth2"; + p.ForwardAuthenticate = "auth1"; + p.ForwardSignIn = "auth1"; + p.ForwardSignOut = "auth1"; + p.ForwardForbid = "auth1"; + p.ForwardChallenge = "auth1"; + }); + + var handler1 = new TestHandler(); + services.AddSingleton(handler1); + var handler2 = new TestHandler2(); + services.AddSingleton(handler2); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, handler1.AuthenticateCount); + Assert.Equal(0, handler1.ForbidCount); + Assert.Equal(0, handler1.ChallengeCount); + Assert.Equal(0, handler1.SignInCount); + Assert.Equal(0, handler1.SignOutCount); + Assert.Equal(0, handler2.AuthenticateCount); + Assert.Equal(0, handler2.ForbidCount); + Assert.Equal(0, handler2.ChallengeCount); + Assert.Equal(0, handler2.SignInCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.AuthenticateAsync("forward"); + Assert.Equal(1, handler1.AuthenticateCount); + Assert.Equal(0, handler2.AuthenticateCount); + + await context.ForbidAsync("forward"); + Assert.Equal(1, handler1.ForbidCount); + Assert.Equal(0, handler2.ForbidCount); + + await context.ChallengeAsync("forward"); + Assert.Equal(1, handler1.ChallengeCount); + Assert.Equal(0, handler2.ChallengeCount); + + await context.SignOutAsync("forward"); + Assert.Equal(1, handler1.SignOutCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.SignInAsync("forward", new ClaimsPrincipal()); + Assert.Equal(1, handler1.SignInCount); + Assert.Equal(0, handler2.SignInCount); + } + + [Fact] + public async Task VirtualSchemeTargetsForwardWithDefaultTarget() + { + var services = new ServiceCollection().AddOptions().AddLogging(); + services.AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + }) + .AddPolicyScheme("forward", "forward", p => p.ForwardDefault= "auth1"); + + var handler1 = new TestHandler(); + services.AddSingleton(handler1); + var handler2 = new TestHandler2(); + services.AddSingleton(handler2); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, handler1.AuthenticateCount); + Assert.Equal(0, handler1.ForbidCount); + Assert.Equal(0, handler1.ChallengeCount); + Assert.Equal(0, handler1.SignInCount); + Assert.Equal(0, handler1.SignOutCount); + Assert.Equal(0, handler2.AuthenticateCount); + Assert.Equal(0, handler2.ForbidCount); + Assert.Equal(0, handler2.ChallengeCount); + Assert.Equal(0, handler2.SignInCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.AuthenticateAsync("forward"); + Assert.Equal(1, handler1.AuthenticateCount); + Assert.Equal(0, handler2.AuthenticateCount); + + await context.ForbidAsync("forward"); + Assert.Equal(1, handler1.ForbidCount); + Assert.Equal(0, handler2.ForbidCount); + + await context.ChallengeAsync("forward"); + Assert.Equal(1, handler1.ChallengeCount); + Assert.Equal(0, handler2.ChallengeCount); + + await context.SignOutAsync("forward"); + Assert.Equal(1, handler1.SignOutCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.SignInAsync("forward", new ClaimsPrincipal()); + Assert.Equal(1, handler1.SignInCount); + Assert.Equal(0, handler2.SignInCount); + } + + [Fact] + public async Task VirtualSchemeTargetsOverrideDefaultTarget() + { + var services = new ServiceCollection().AddOptions().AddLogging(); + services.AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + }) + .AddPolicyScheme("forward", "forward", p => + { + p.ForwardDefault= "auth1"; + p.ForwardChallenge = "auth2"; + p.ForwardSignIn = "auth2"; + }); + + var handler1 = new TestHandler(); + services.AddSingleton(handler1); + var handler2 = new TestHandler2(); + services.AddSingleton(handler2); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, handler1.AuthenticateCount); + Assert.Equal(0, handler1.ForbidCount); + Assert.Equal(0, handler1.ChallengeCount); + Assert.Equal(0, handler1.SignInCount); + Assert.Equal(0, handler1.SignOutCount); + Assert.Equal(0, handler2.AuthenticateCount); + Assert.Equal(0, handler2.ForbidCount); + Assert.Equal(0, handler2.ChallengeCount); + Assert.Equal(0, handler2.SignInCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.AuthenticateAsync("forward"); + Assert.Equal(1, handler1.AuthenticateCount); + Assert.Equal(0, handler2.AuthenticateCount); + + await context.ForbidAsync("forward"); + Assert.Equal(1, handler1.ForbidCount); + Assert.Equal(0, handler2.ForbidCount); + + await context.ChallengeAsync("forward"); + Assert.Equal(0, handler1.ChallengeCount); + Assert.Equal(1, handler2.ChallengeCount); + + await context.SignOutAsync("forward"); + Assert.Equal(1, handler1.SignOutCount); + Assert.Equal(0, handler2.SignOutCount); + + await context.SignInAsync("forward", new ClaimsPrincipal()); + Assert.Equal(0, handler1.SignInCount); + Assert.Equal(1, handler2.SignInCount); + } + + [Fact] + public async Task CanDynamicTargetBasedOnQueryString() + { + var server = CreateServer(services => + { + services.AddAuthentication(o => + { + o.AddScheme("auth1", "auth1"); + o.AddScheme("auth2", "auth2"); + o.AddScheme("auth3", "auth3"); + }) + .AddPolicyScheme("dynamic", "dynamic", p => + { + p.ForwardDefaultSelector = c => c.Request.QueryString.Value.Substring(1); + }); + }); + + var transaction = await server.SendAsync("http://example.com/auth/dynamic?auth1"); + Assert.Equal("auth1", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth1")); + transaction = await server.SendAsync("http://example.com/auth/dynamic?auth2"); + Assert.Equal("auth2", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth2")); + transaction = await server.SendAsync("http://example.com/auth/dynamic?auth3"); + Assert.Equal("auth3", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth3")); + } + + private class TestHandler : IAuthenticationSignInHandler + { + public AuthenticationScheme Scheme { get; set; } + public int SignInCount { get; set; } + public int SignOutCount { get; set; } + public int ForbidCount { get; set; } + public int ChallengeCount { get; set; } + public int AuthenticateCount { get; set; } + + public Task AuthenticateAsync() + { + AuthenticateCount++; + var principal = new ClaimsPrincipal(); + var id = new ClaimsIdentity(); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name)); + principal.AddIdentity(id); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + ChallengeCount++; + return Task.CompletedTask; + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + ForbidCount++; + return Task.CompletedTask; + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + Scheme = scheme; + return Task.CompletedTask; + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + SignInCount++; + return Task.CompletedTask; + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + SignOutCount++; + return Task.CompletedTask; + } + } + + private class TestHandler2 : IAuthenticationSignInHandler + { + public AuthenticationScheme Scheme { get; set; } + public int SignInCount { get; set; } + public int SignOutCount { get; set; } + public int ForbidCount { get; set; } + public int ChallengeCount { get; set; } + public int AuthenticateCount { get; set; } + + public Task AuthenticateAsync() + { + AuthenticateCount++; + var principal = new ClaimsPrincipal(); + var id = new ClaimsIdentity(); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name)); + principal.AddIdentity(id); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + ChallengeCount++; + return Task.CompletedTask; + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + ForbidCount++; + return Task.CompletedTask; + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + Scheme = scheme; + return Task.CompletedTask; + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + SignInCount++; + return Task.CompletedTask; + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + SignOutCount++; + return Task.CompletedTask; + } + } + + private static TestServer CreateServer(Action configure = null, string defaultScheme = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path.StartsWithSegments(new PathString("/auth"), out var remainder)) + { + var name = (remainder.Value.Length > 0) ? remainder.Value.Substring(1) : null; + var result = await context.AuthenticateAsync(name); + res.Describe(result?.Ticket?.Principal); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + configure?.Invoke(services); + }); + return new TestServer(builder); + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/RemoteAuthenticationTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/RemoteAuthenticationTests.cs new file mode 100644 index 000000000..d477e7534 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/RemoteAuthenticationTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public abstract class RemoteAuthenticationTests : SharedAuthenticationTests where TOptions : RemoteAuthenticationOptions + { + protected override string DisplayName => DefaultScheme; + + private TestServer CreateServer(Action configureOptions, Func testpath = null, bool isDefault = true) + => CreateServerWithServices(s => + { + var builder = s.AddAuthentication(); + if (isDefault) + { + s.Configure(o => o.DefaultScheme = DefaultScheme); + } + RegisterAuth(builder, configureOptions); + s.AddSingleton(Clock); + }, testpath); + + + protected virtual TestServer CreateServerWithServices(Action configureServices, Func testpath = null) + { + //private static TestServer CreateServer(Action configure, Action configureServices, Func> handler) + var builder = new WebHostBuilder() + .Configure(app => + { + app.Use(async (context, next) => + { + if (testpath != null) + { + await testpath(context); + } + await next(); + }); + }) + .ConfigureServices(configureServices); + return new TestServer(builder); + } + + protected abstract void ConfigureDefaults(TOptions o); + + [Fact] + public async Task VerifySignInSchemeCannotBeSetToSelf() + { + var server = CreateServer( + o => + { + ConfigureDefaults(o); + o.SignInScheme = DefaultScheme; + }, + context => context.ChallengeAsync(DefaultScheme)); + var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); + Assert.Contains("cannot be set to itself", error.Message); + } + + [Fact] + public async Task VerifySignInSchemeCannotBeSetToSelfUsingDefaultScheme() + { + var server = CreateServer( + o => o.SignInScheme = null, + context => context.ChallengeAsync(DefaultScheme), + isDefault: true); + var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); + Assert.Contains("cannot be set to itself", error.Message); + } + + [Fact] + public async Task VerifySignInSchemeCannotBeSetToSelfUsingDefaultSignInScheme() + { + var server = CreateServerWithServices( + services => + { + var builder = services.AddAuthentication(o => o.DefaultSignInScheme = DefaultScheme); + RegisterAuth(builder, o => o.SignInScheme = null); + }, + context => context.ChallengeAsync(DefaultScheme)); + var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); + Assert.Contains("cannot be set to itself", error.Message); + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/DataHandler/SecureDataFormatTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/SecureDataFormatTests.cs similarity index 100% rename from test/Microsoft.AspNetCore.Authentication.Test/DataHandler/SecureDataFormatTests.cs rename to test/Microsoft.AspNetCore.Authentication.Test/SecureDataFormatTests.cs diff --git a/test/Microsoft.AspNetCore.Authentication.Test/SharedAuthenticationTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/SharedAuthenticationTests.cs new file mode 100644 index 000000000..4590c4915 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/SharedAuthenticationTests.cs @@ -0,0 +1,510 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authentication.Tests; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication +{ + public abstract class SharedAuthenticationTests where TOptions : AuthenticationSchemeOptions + { + protected TestClock Clock { get; } = new TestClock(); + + protected abstract string DefaultScheme { get; } + protected virtual string DisplayName { get; } + protected abstract Type HandlerType { get; } + + protected virtual bool SupportsSignIn { get => true; } + protected virtual bool SupportsSignOut { get => true; } + + protected abstract void RegisterAuth(AuthenticationBuilder services, Action configure); + + [Fact] + public async Task CanForwardDefault() + { + var services = new ServiceCollection().AddLogging(); + + var builder = services.AddAuthentication(o => + { + o.DefaultScheme = DefaultScheme; + o.AddScheme("auth1", "auth1"); + }); + RegisterAuth(builder, o => o.ForwardDefault = "auth1"); + + var forwardDefault = new TestHandler(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + if (SupportsSignOut) + { + await context.SignOutAsync(); + Assert.Equal(1, forwardDefault.SignOutCount); + } + else + { + await Assert.ThrowsAsync(() => context.SignOutAsync()); + } + + if (SupportsSignIn) + { + await context.SignInAsync(new ClaimsPrincipal()); + Assert.Equal(1, forwardDefault.SignInCount); + } + else + { + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + } + + [Fact] + public async Task ForwardSignInWinsOverDefault() + { + if (SupportsSignIn) + { + var services = new ServiceCollection().AddLogging(); + + var builder = services.AddAuthentication(o => + { + o.DefaultScheme = DefaultScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }); + RegisterAuth(builder, o => + { + o.ForwardDefault = "auth1"; + o.ForwardSignIn = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.SignInAsync(new ClaimsPrincipal()); + Assert.Equal(1, specific.SignInCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignOutCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + } + + [Fact] + public async Task ForwardSignOutWinsOverDefault() + { + if (SupportsSignOut) + { + var services = new ServiceCollection().AddLogging(); + var builder = services.AddAuthentication(o => + { + o.DefaultScheme = DefaultScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }); + RegisterAuth(builder, o => + { + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.SignOutAsync(); + Assert.Equal(1, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + } + + [Fact] + public async Task ForwardForbidWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + var builder = services.AddAuthentication(o => + { + o.DefaultScheme = DefaultScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }); + RegisterAuth(builder, o => + { + o.ForwardDefault = "auth1"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ForbidAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(1, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardAuthenticateWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + var builder = services.AddAuthentication(o => + { + o.DefaultScheme = DefaultScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }); + RegisterAuth(builder, o => + { + o.ForwardDefault = "auth1"; + o.ForwardAuthenticate = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(1, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardChallengeWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + var builder = services.AddAuthentication(o => + { + o.DefaultScheme = DefaultScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }); + RegisterAuth(builder, o => + { + o.ForwardDefault = "auth1"; + o.ForwardChallenge = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ChallengeAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(1, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardSelectorWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + var builder = services.AddAuthentication(o => + { + o.DefaultScheme = DefaultScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }); + RegisterAuth(builder, o => + { + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, selector.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, selector.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, selector.ChallengeCount); + + if (SupportsSignOut) + { + await context.SignOutAsync(); + Assert.Equal(1, selector.SignOutCount); + } + else + { + await Assert.ThrowsAsync(() => context.SignOutAsync()); + } + + if (SupportsSignIn) + { + await context.SignInAsync(new ClaimsPrincipal()); + Assert.Equal(1, selector.SignInCount); + } + else + { + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task NullForwardSelectorUsesDefault() + { + var services = new ServiceCollection().AddLogging(); + var builder = services.AddAuthentication(o => + { + o.DefaultScheme = DefaultScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }); + RegisterAuth(builder, o => + { + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => null; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + if (SupportsSignOut) + { + await context.SignOutAsync(); + Assert.Equal(1, forwardDefault.SignOutCount); + } + else + { + await Assert.ThrowsAsync(() => context.SignOutAsync()); + } + + if (SupportsSignIn) + { + await context.SignInAsync(new ClaimsPrincipal()); + Assert.Equal(1, forwardDefault.SignInCount); + } + else + { + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task SpecificForwardWinsOverSelectorAndDefault() + { + var services = new ServiceCollection().AddLogging(); + var builder = services.AddAuthentication(o => + { + o.DefaultScheme = DefaultScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }); + RegisterAuth(builder, o => + { + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + o.ForwardAuthenticate = "specific"; + o.ForwardChallenge = "specific"; + o.ForwardSignIn = "specific"; + o.ForwardSignOut = "specific"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, specific.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, specific.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, specific.ChallengeCount); + + if (SupportsSignOut) + { + await context.SignOutAsync(); + Assert.Equal(1, specific.SignOutCount); + } + else + { + await Assert.ThrowsAsync(() => context.SignOutAsync()); + } + + if (SupportsSignIn) + { + await context.SignInAsync(new ClaimsPrincipal()); + Assert.Equal(1, specific.SignInCount); + } + else + { + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + } + + [Fact] + public async Task VerifySchemeDefaults() + { + var services = new ServiceCollection(); + var builder = services.AddAuthentication(); + RegisterAuth(builder, o => { }); + var sp = services.BuildServiceProvider(); + var schemeProvider = sp.GetRequiredService(); + var scheme = await schemeProvider.GetSchemeAsync(DefaultScheme); + Assert.NotNull(scheme); + Assert.Equal(HandlerType, scheme.HandlerType); + Assert.Equal(DisplayName, scheme.DisplayName); + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/TestHandlers.cs b/test/Microsoft.AspNetCore.Authentication.Test/TestHandlers.cs new file mode 100644 index 000000000..cd9fe9fb1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/TestHandlers.cs @@ -0,0 +1,115 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication.Tests +{ + public class TestAuthHandler : AuthenticationHandler, IAuthenticationSignInHandler + { + public TestAuthHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { } + + public int SignInCount { get; set; } + public int SignOutCount { get; set; } + public int ForbidCount { get; set; } + public int ChallengeCount { get; set; } + public int AuthenticateCount { get; set; } + + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + ChallengeCount++; + return Task.CompletedTask; + } + + protected override Task HandleForbiddenAsync(AuthenticationProperties properties) + { + ForbidCount++; + return Task.CompletedTask; + } + + protected override Task HandleAuthenticateAsync() + { + AuthenticateCount++; + var principal = new ClaimsPrincipal(); + var id = new ClaimsIdentity(); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name)); + principal.AddIdentity(id); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + SignInCount++; + return Task.CompletedTask; + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + SignOutCount++; + return Task.CompletedTask; + } + } + + public class TestHandler : IAuthenticationSignInHandler + { + public AuthenticationScheme Scheme { get; set; } + public int SignInCount { get; set; } + public int SignOutCount { get; set; } + public int ForbidCount { get; set; } + public int ChallengeCount { get; set; } + public int AuthenticateCount { get; set; } + + public Task AuthenticateAsync() + { + AuthenticateCount++; + var principal = new ClaimsPrincipal(); + var id = new ClaimsIdentity(); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name)); + principal.AddIdentity(id); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + ChallengeCount++; + return Task.CompletedTask; + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + ForbidCount++; + return Task.CompletedTask; + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + Scheme = scheme; + return Task.CompletedTask; + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + SignInCount++; + return Task.CompletedTask; + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + SignOutCount++; + return Task.CompletedTask; + } + } + + public class TestHandler2 : TestHandler + { + } + + public class TestHandler3 : TestHandler + { + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/DataHandler/TicketSerializerTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/TicketSerializerTests.cs similarity index 85% rename from test/Microsoft.AspNetCore.Authentication.Test/DataHandler/TicketSerializerTests.cs rename to test/Microsoft.AspNetCore.Authentication.Test/TicketSerializerTests.cs index 8eeedde0d..a1e58743b 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/DataHandler/TicketSerializerTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/TicketSerializerTests.cs @@ -5,7 +5,7 @@ using System.IO; using System.Linq; using System.Security.Claims; -using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Testing.xunit; using Xunit; @@ -28,7 +28,7 @@ public void CanRoundTripEmptyPrincipal() serializer.Write(writer, ticket); stream.Position = 0; var readTicket = serializer.Read(reader); - Assert.Equal(0, readTicket.Principal.Identities.Count()); + Assert.Empty(readTicket.Principal.Identities); Assert.Equal("bye", readTicket.Properties.RedirectUri); Assert.Equal("Hello", readTicket.AuthenticationScheme); } @@ -50,7 +50,7 @@ public void CanRoundTripBootstrapContext() serializer.Write(writer, ticket); stream.Position = 0; var readTicket = serializer.Read(reader); - Assert.Equal(1, readTicket.Principal.Identities.Count()); + Assert.Single(readTicket.Principal.Identities); Assert.Equal("misc", readTicket.Principal.Identity.AuthenticationType); Assert.Equal("bootstrap", readTicket.Principal.Identities.First().BootstrapContext); } @@ -73,12 +73,12 @@ public void CanRoundTripActorIdentity() serializer.Write(writer, ticket); stream.Position = 0; var readTicket = serializer.Read(reader); - Assert.Equal(1, readTicket.Principal.Identities.Count()); + Assert.Single(readTicket.Principal.Identities); Assert.Equal("misc", readTicket.Principal.Identity.AuthenticationType); var identity = (ClaimsIdentity) readTicket.Principal.Identity; Assert.NotNull(identity.Actor); - Assert.Equal(identity.Actor.AuthenticationType, "actor"); + Assert.Equal("actor", identity.Actor.AuthenticationType); } } @@ -108,22 +108,22 @@ public void CanRoundTripClaimProperties() serializer.Write(writer, ticket); stream.Position = 0; var readTicket = serializer.Read(reader); - Assert.Equal(1, readTicket.Principal.Identities.Count()); + Assert.Single(readTicket.Principal.Identities); Assert.Equal("misc", readTicket.Principal.Identity.AuthenticationType); var readClaim = readTicket.Principal.FindFirst("type"); Assert.NotNull(claim); - Assert.Equal(claim.Type, "type"); - Assert.Equal(claim.Value, "value"); - Assert.Equal(claim.ValueType, "valueType"); - Assert.Equal(claim.Issuer, "issuer"); - Assert.Equal(claim.OriginalIssuer, "original-issuer"); + Assert.Equal("type", claim.Type); + Assert.Equal("value", claim.Value); + Assert.Equal("valueType", claim.ValueType); + Assert.Equal("issuer", claim.Issuer); + Assert.Equal("original-issuer", claim.OriginalIssuer); var property1 = readClaim.Properties["property-1"]; - Assert.Equal(property1, "property-value"); + Assert.Equal("property-value", property1); var property2 = readClaim.Properties["property-2"]; - Assert.Equal(property2, string.Empty); + Assert.Equal(string.Empty, property2); } } } diff --git a/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs index e8f2c1657..4d4023bee 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/TokenExtensionTests.cs @@ -1,14 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Http.Features.Authentication; using Xunit; namespace Microsoft.AspNetCore.Authentication @@ -51,10 +45,10 @@ public void SubsequentStoreTokenDeletesPreviousTokens() props.StoreTokens(new[] { new AuthenticationToken { Name = "Zero", Value = "0" } }); Assert.Equal("0", props.GetTokenValue("Zero")); - Assert.Equal(null, props.GetTokenValue("One")); - Assert.Equal(null, props.GetTokenValue("Two")); - Assert.Equal(null, props.GetTokenValue("Three")); - Assert.Equal(1, props.GetTokens().Count()); + Assert.Null(props.GetTokenValue("One")); + Assert.Null(props.GetTokenValue("Two")); + Assert.Null(props.GetTokenValue("Three")); + Assert.Single(props.GetTokens()); } [Fact] @@ -81,43 +75,31 @@ public void CanUpdateTokens() Assert.Equal(3, props.GetTokens().Count()); } - public class TestAuthHandler : IAuthenticationHandler + [Fact] + public void CanUpdateTokenValues() { - private readonly AuthenticationProperties _props; - public TestAuthHandler(AuthenticationProperties props) - { - _props = props; - } - - public Task AuthenticateAsync(AuthenticateContext context) - { - context.Authenticated(new ClaimsPrincipal(), _props.Items, new Dictionary()); - return Task.FromResult(0); - } - - public Task ChallengeAsync(ChallengeContext context) - { - throw new NotImplementedException(); - } - - public void GetDescriptions(DescribeSchemesContext context) - { - throw new NotImplementedException(); - } - - public Task SignInAsync(SignInContext context) - { - throw new NotImplementedException(); - } - - public Task SignOutAsync(SignOutContext context) - { - throw new NotImplementedException(); - } + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + Assert.True(props.UpdateTokenValue("One", ".11")); + Assert.True(props.UpdateTokenValue("Two", ".22")); + Assert.True(props.UpdateTokenValue("Three", ".33")); + + Assert.Equal(".11", props.GetTokenValue("One")); + Assert.Equal(".22", props.GetTokenValue("Two")); + Assert.Equal(".33", props.GetTokenValue("Three")); + Assert.Equal(3, props.GetTokens().Count()); } [Fact] - public async Task CanGetTokenFromContext() + public void UpdateTokenValueReturnsFalseForUnknownToken() { var props = new AuthenticationProperties(); var tokens = new List(); @@ -129,14 +111,71 @@ public async Task CanGetTokenFromContext() tokens.Add(tok3); props.StoreTokens(tokens); - var context = new DefaultHttpContext(); - var handler = new TestAuthHandler(props); - context.Features.Set(new HttpAuthenticationFeature() { Handler = handler }); + Assert.False(props.UpdateTokenValue("ONE", ".11")); + Assert.False(props.UpdateTokenValue("Jigglypuff", ".11")); + + Assert.Null(props.GetTokenValue("ONE")); + Assert.Null(props.GetTokenValue("Jigglypuff")); + Assert.Equal(3, props.GetTokens().Count()); - Assert.Equal("1", await context.Authentication.GetTokenAsync("One")); - Assert.Equal("2", await context.Authentication.GetTokenAsync("Two")); - Assert.Equal("3", await context.Authentication.GetTokenAsync("Three")); } + //public class TestAuthHandler : IAuthenticationHandler + //{ + // private readonly AuthenticationProperties _props; + // public TestAuthHandler(AuthenticationProperties props) + // { + // _props = props; + // } + + // public Task AuthenticateAsync(AuthenticateContext context) + // { + // context.Authenticated(new ClaimsPrincipal(), _props.Items, new Dictionary()); + // return Task.FromResult(0); + // } + + // public Task ChallengeAsync(AuthenticationProperties properties) + // { + // throw new NotImplementedException(); + // } + + // public void GetDescriptions(DescribeSchemesContext context) + // { + // throw new NotImplementedException(); + // } + + // public Task SignInAsync(ClaimsPrincipal principal, AuthenticationProperties properties) + // { + // throw new NotImplementedException(); + // } + + // public Task SignOutAsync(AuthenticationProperties properties) + // { + // throw new NotImplementedException(); + // } + //} + + //[Fact] + //public async Task CanGetTokenFromContext() + //{ + // var props = new AuthenticationProperties(); + // var tokens = new List(); + // var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + // var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + // var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + // tokens.Add(tok1); + // tokens.Add(tok2); + // tokens.Add(tok3); + // props.StoreTokens(tokens); + + // var context = new DefaultHttpContext(); + // var handler = new TestAuthHandler(props); + // context.Features.Set(new HttpAuthenticationFeature() { Handler = handler }); + + // Assert.Equal("1", await context.GetTokenAsync("One")); + // Assert.Equal("2", await context.GetTokenAsync("Two")); + // Assert.Equal("3", await context.GetTokenAsync("Three")); + //} + } } diff --git a/test/Microsoft.AspNetCore.Authentication.Test/Twitter/TwitterMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/Twitter/TwitterMiddlewareTests.cs deleted file mode 100644 index 2ca2223b9..000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/Twitter/TwitterMiddlewareTests.cs +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Net; -using System.Net.Http; -using System.Security.Claims; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace Microsoft.AspNetCore.Authentication.Twitter -{ - public class TwitterMiddlewareTests - { - [Fact] - public async Task ChallengeWillTriggerApplyRedirectEvent() - { - var server = CreateServer(new TwitterOptions - { - ConsumerKey = "Test Consumer Key", - ConsumerSecret = "Test Consumer Secret", - Events = new TwitterEvents - { - OnRedirectToAuthorizationEndpoint = context => - { - context.Response.Redirect(context.RedirectUri + "&custom=test"); - return Task.FromResult(0); - } - }, - BackchannelHttpHandler = new TestHttpMessageHandler - { - Sender = req => - { - if (req.RequestUri.AbsoluteUri == "https://api.twitter.com/oauth/request_token") - { - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = - new StringContent("oauth_callback_confirmed=true&oauth_token=test_oauth_token&oauth_token_secret=test_oauth_token_secret", - Encoding.UTF8, - "application/x-www-form-urlencoded") - }; - } - return null; - } - } - }, - context => - { - // REVIEW: Gross - context.Authentication.ChallengeAsync("Twitter").GetAwaiter().GetResult(); - return true; - }); - var transaction = await server.SendAsync("http://example.com/challenge"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var query = transaction.Response.Headers.Location.Query; - Assert.Contains("custom=test", query); - } - - [Fact] - public async Task BadSignInWillThrow() - { - var server = CreateServer(new TwitterOptions - { - ConsumerKey = "Test Consumer Key", - ConsumerSecret = "Test Consumer Secret" - }); - - // Send a bogus sign in - var error = await Assert.ThrowsAnyAsync(() => server.SendAsync("https://example.com/signin-twitter")); - Assert.Equal("Invalid state cookie.", error.GetBaseException().Message); - } - - [Fact] - public async Task SignInThrows() - { - var server = CreateServer(new TwitterOptions - { - ConsumerKey = "Test Consumer Key", - ConsumerSecret = "Test Consumer Secret" - }); - var transaction = await server.SendAsync("https://example.com/signIn"); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - } - - [Fact] - public async Task SignOutThrows() - { - var server = CreateServer(new TwitterOptions - { - ConsumerKey = "Test Consumer Key", - ConsumerSecret = "Test Consumer Secret" - }); - var transaction = await server.SendAsync("https://example.com/signOut"); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - } - - [Fact] - public async Task ForbidThrows() - { - var server = CreateServer(new TwitterOptions - { - ConsumerKey = "Test Consumer Key", - ConsumerSecret = "Test Consumer Secret" - }); - var transaction = await server.SendAsync("https://example.com/signOut"); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - } - - - [Fact] - public async Task ChallengeWillTriggerRedirection() - { - var server = CreateServer(new TwitterOptions - { - ConsumerKey = "Test Consumer Key", - ConsumerSecret = "Test Consumer Secret", - BackchannelHttpHandler = new TestHttpMessageHandler - { - Sender = req => - { - if (req.RequestUri.AbsoluteUri == "https://api.twitter.com/oauth/request_token") - { - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = - new StringContent("oauth_callback_confirmed=true&oauth_token=test_oauth_token&oauth_token_secret=test_oauth_token_secret", - Encoding.UTF8, - "application/x-www-form-urlencoded") - }; - } - return null; - } - } - }, - context => - { - // REVIEW: gross - context.Authentication.ChallengeAsync("Twitter").GetAwaiter().GetResult(); - return true; - }); - var transaction = await server.SendAsync("http://example.com/challenge"); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - var location = transaction.Response.Headers.Location.AbsoluteUri; - Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location); - } - - private static TestServer CreateServer(TwitterOptions options, Func handler = null) - { - var builder = new WebHostBuilder() - .Configure(app => - { - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AuthenticationScheme = "External" - }); - app.UseTwitterAuthentication(options); - app.Use(async (context, next) => - { - var req = context.Request; - var res = context.Response; - if (req.Path == new PathString("/signIn")) - { - await Assert.ThrowsAsync(() => context.Authentication.SignInAsync("Twitter", new ClaimsPrincipal())); - } - else if (req.Path == new PathString("/signOut")) - { - await Assert.ThrowsAsync(() => context.Authentication.SignOutAsync("Twitter")); - } - else if (req.Path == new PathString("/forbid")) - { - await Assert.ThrowsAsync(() => context.Authentication.ForbidAsync("Twitter")); - } - else if (handler == null || !handler(context)) - { - await next(); - } - }); - }) - .ConfigureServices(services => - { - services.AddAuthentication(); - services.Configure(authOptions => - { - authOptions.SignInScheme = "External"; - }); - }); - return new TestServer(builder); - } - } -} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/TwitterTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/TwitterTests.cs new file mode 100644 index 000000000..8eb7a5cd7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/TwitterTests.cs @@ -0,0 +1,372 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Twitter +{ + public class TwitterTests : RemoteAuthenticationTests + { + protected override string DefaultScheme => TwitterDefaults.AuthenticationScheme; + protected override Type HandlerType => typeof(TwitterHandler); + protected override bool SupportsSignIn { get => false; } + protected override bool SupportsSignOut { get => false; } + + protected override void RegisterAuth(AuthenticationBuilder services, Action configure) + { + services.AddTwitter(o => + { + ConfigureDefaults(o); + configure.Invoke(o); + }); + } + + protected override void ConfigureDefaults(TwitterOptions o) + { + o.ConsumerKey = "whatever"; + o.ConsumerSecret = "whatever"; + o.SignInScheme = "auth1"; + } + + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + o.Events = new TwitterEvents + { + OnRedirectToAuthorizationEndpoint = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + return Task.FromResult(0); + } + }; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = BackchannelRequestToken + }; + }, + async context => + { + await context.ChallengeAsync("Twitter"); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var query = transaction.Response.Headers.Location.Query; + Assert.Contains("custom=test", query); + } + + /// + /// Validates the Twitter Options to check if the Consumer Key is missing in the TwitterOptions and if so throws the ArgumentException + /// + /// + [Fact] + public async Task ThrowsIfClientIdMissing() + { + var server = CreateServer(o => + { + o.ConsumerSecret = "Test Consumer Secret"; + }); + + await Assert.ThrowsAsync("ConsumerKey", async () => await server.SendAsync("http://example.com/challenge")); + } + + /// + /// Validates the Twitter Options to check if the Consumer Secret is missing in the TwitterOptions and if so throws the ArgumentException + /// + /// + [Fact] + public async Task ThrowsIfClientSecretMissing() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + }); + + await Assert.ThrowsAsync("ConsumerSecret", async () => await server.SendAsync("http://example.com/challenge")); + } + + [Fact] + public async Task BadSignInWillThrow() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + }); + + // Send a bogus sign in + var error = await Assert.ThrowsAnyAsync(() => server.SendAsync("https://example.com/signin-twitter")); + Assert.Equal("Invalid state cookie.", error.GetBaseException().Message); + } + + [Fact] + public async Task SignInThrows() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signIn"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task SignOutThrows() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ForbidThrows() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signOut"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = BackchannelRequestToken + }; + }, + async context => + { + await context.ChallengeAsync("Twitter"); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location); + } + + [Fact] + public async Task HandleRequestAsync_RedirectsToAccessDeniedPathWhenExplicitlySet() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = BackchannelRequestToken + }; + o.AccessDeniedPath = "/access-denied"; + o.Events.OnRemoteFailure = context => throw new InvalidOperationException("This event should not be called."); + }, + async context => + { + var properties = new AuthenticationProperties(); + properties.Items["testkey"] = "testvalue"; + await context.ChallengeAsync("Twitter", properties); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location); + Assert.True(transaction.Response.Headers.TryGetValues(HeaderNames.SetCookie, out var setCookie)); + Assert.True(SetCookieHeaderValue.TryParseList(setCookie.ToList(), out var setCookieValues)); + Assert.Single(setCookieValues); + var setCookieValue = setCookieValues.Single(); + var cookie = new CookieHeaderValue(setCookieValue.Name, setCookieValue.Value); + + var request = new HttpRequestMessage(HttpMethod.Get, "/signin-twitter?denied=ABCDEFG"); + request.Headers.Add(HeaderNames.Cookie, cookie.ToString()); + var client = server.CreateClient(); + var response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal("/access-denied?ReturnUrl=%2Fchallenge", response.Headers.Location.ToString()); + } + + [Fact] + public async Task BadCallbackCallsAccessDeniedWithState() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = BackchannelRequestToken + }; + o.Events = new TwitterEvents() + { + OnAccessDenied = context => + { + Assert.NotNull(context.Properties); + Assert.Equal("testvalue", context.Properties.Items["testkey"]); + context.Response.StatusCode = StatusCodes.Status406NotAcceptable; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + }, + async context => + { + var properties = new AuthenticationProperties(); + properties.Items["testkey"] = "testvalue"; + await context.ChallengeAsync("Twitter", properties); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location); + Assert.True(transaction.Response.Headers.TryGetValues(HeaderNames.SetCookie, out var setCookie)); + Assert.True(SetCookieHeaderValue.TryParseList(setCookie.ToList(), out var setCookieValues)); + Assert.Single(setCookieValues); + var setCookieValue = setCookieValues.Single(); + var cookie = new CookieHeaderValue(setCookieValue.Name, setCookieValue.Value); + + var request = new HttpRequestMessage(HttpMethod.Get, "/signin-twitter?denied=ABCDEFG"); + request.Headers.Add(HeaderNames.Cookie, cookie.ToString()); + var client = server.CreateClient(); + var response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode); + } + + [Fact] + public async Task BadCallbackCallsRemoteAuthFailedWithState() + { + var server = CreateServer(o => + { + o.ConsumerKey = "Test Consumer Key"; + o.ConsumerSecret = "Test Consumer Secret"; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = BackchannelRequestToken + }; + o.Events = new TwitterEvents() + { + OnRemoteFailure = context => + { + Assert.NotNull(context.Failure); + Assert.Equal("Access was denied by the resource owner or by the remote server.", context.Failure.Message); + Assert.NotNull(context.Properties); + Assert.Equal("testvalue", context.Properties.Items["testkey"]); + context.Response.StatusCode = StatusCodes.Status406NotAcceptable; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + }, + async context => + { + var properties = new AuthenticationProperties(); + properties.Items["testkey"] = "testvalue"; + await context.ChallengeAsync("Twitter", properties); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.AbsoluteUri; + Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location); + Assert.True(transaction.Response.Headers.TryGetValues(HeaderNames.SetCookie, out var setCookie)); + Assert.True(SetCookieHeaderValue.TryParseList(setCookie.ToList(), out var setCookieValues)); + Assert.Single(setCookieValues); + var setCookieValue = setCookieValues.Single(); + var cookie = new CookieHeaderValue(setCookieValue.Name, setCookieValue.Value); + + var request = new HttpRequestMessage(HttpMethod.Get, "/signin-twitter?denied=ABCDEFG"); + request.Headers.Add(HeaderNames.Cookie, cookie.ToString()); + var client = server.CreateClient(); + var response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode); + } + + private static TestServer CreateServer(Action options, Func> handler = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/signIn")) + { + await Assert.ThrowsAsync(() => context.SignInAsync("Twitter", new ClaimsPrincipal())); + } + else if (req.Path == new PathString("/signOut")) + { + await Assert.ThrowsAsync(() => context.SignOutAsync("Twitter")); + } + else if (req.Path == new PathString("/forbid")) + { + await Assert.ThrowsAsync(() => context.ForbidAsync("Twitter")); + } + else if (handler == null || ! await handler(context)) + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + Action wrapOptions = o => + { + o.SignInScheme = "External"; + options(o); + }; + services.AddAuthentication() + .AddCookie("External", _ => { }) + .AddTwitter(wrapOptions); + }); + return new TestServer(builder); + } + + private HttpResponseMessage BackchannelRequestToken(HttpRequestMessage req) + { + if (req.RequestUri.AbsoluteUri == "https://api.twitter.com/oauth/request_token") + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = + new StringContent("oauth_callback_confirmed=true&oauth_token=test_oauth_token&oauth_token_secret=test_oauth_token_secret", + Encoding.UTF8, + "application/x-www-form-urlencoded") + }; + } + throw new NotImplementedException(req.RequestUri.AbsoluteUri); + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/CustomStateDataFormat.cs b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/CustomStateDataFormat.cs new file mode 100644 index 000000000..0de867d28 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/CustomStateDataFormat.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Runtime.Serialization; +using System.Text; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + public class CustomStateDataFormat : ISecureDataFormat + { + public const string ValidStateData = "ValidStateData"; + + private string lastSavedAuthenticationProperties; + private DataContractSerializer serializer = new DataContractSerializer(typeof(AuthenticationProperties)); + + public string Protect(AuthenticationProperties data) + { + lastSavedAuthenticationProperties = Serialize(data); + return ValidStateData; + } + + public string Protect(AuthenticationProperties data, string purpose) + { + return Protect(data); + } + + public AuthenticationProperties Unprotect(string state) + { + return state == ValidStateData ? DeSerialize(lastSavedAuthenticationProperties) : null; + } + + public AuthenticationProperties Unprotect(string protectedText, string purpose) + { + return Unprotect(protectedText); + } + + private string Serialize(AuthenticationProperties data) + { + using (MemoryStream memoryStream = new MemoryStream()) + { + serializer.WriteObject(memoryStream, data); + memoryStream.Position = 0; + return new StreamReader(memoryStream).ReadToEnd(); + } + } + + private AuthenticationProperties DeSerialize(string state) + { + var stateDataAsBytes = Encoding.UTF8.GetBytes(state); + + using (var ms = new MemoryStream(stateDataAsBytes, false)) + { + return (AuthenticationProperties)serializer.ReadObject(ms); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/InvalidToken.xml b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/InvalidToken.xml new file mode 100644 index 000000000..dfdb0d68d --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/InvalidToken.xml @@ -0,0 +1,83 @@ + + + 2014-04-18T20:21:17.341Z + 2014-04-19T08:21:17.341Z + + + +
http://automation1/
+
+
+ + + https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/ + + + + + + + + + + + Lkq0wTyTFxLUU2cyx0XybJqhka5RzRGj6kC4aIpFg+g= + + + bPwNswOB/B9xcdAljIkin9A2vjq+u94JdyvK03mf8vZFGUYNu9uN/Q6ims1DvW1FnP7SgFBwhIvW5OjZyW8fdYGhC2bq36izkxH6ulkWbciOcyELkyHDACLudvh8kP/Q+IwpicefKzAeI2Qu/5MFq16vFg5YgI+dovg8u1fYPPEPmmptW893RNTHWeh9mLRpLYnHyg7aLG6emNRkEu7w9rzeoICeMFybb9BvJl/q/8MFCW/Z5WemQhCi6YXFSEwCO6zJzCFi/3T6ChU/xYgXbFykDLqulsNOCQxdgutyqxJzugt+3PH5IKHHuoqe7UZNUIyELJ4BgwE1sXCGYIi24rg== + + + MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng + + + + + t0ch1TsP0pi5VoW8q5CGWsCXVZoNtpsg0mbMZPOYb4I + + + + + http://Automation1 + + + + + Test + + + Test + + + user1@praburajgmail.onmicrosoft.com + + + 4afbc689-805b-48cf-a24c-d4aa3248a248 + + + c2f0cd49-5e53-4520-8ed9-4e178dc488c5 + + + https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/ + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + + + _660ec874-f70a-4997-a9c4-bd591f1c7469 + + + + + _660ec874-f70a-4997-a9c4-bd591f1c7469 + + + http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0 + http://schemas.xmlsoap.org/ws/2005/02/trust/Issue + http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey +
\ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityToken.cs b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityToken.cs new file mode 100644 index 000000000..dfe860724 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityToken.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + internal class TestSecurityToken : SecurityToken + { + public override string Id => "id"; + + public override string Issuer => "issuer"; + + public override SecurityKey SecurityKey => throw new NotImplementedException(); + + public override SecurityKey SigningKey + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public override DateTime ValidFrom => new DateTime(2008, 3, 22); + + public override DateTime ValidTo => new DateTime(2017, 3, 22); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityTokenValidator.cs b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityTokenValidator.cs new file mode 100644 index 000000000..05882518f --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/TestSecurityTokenValidator.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + internal class TestSecurityTokenValidator : ISecurityTokenValidator + { + public bool CanValidateToken => true; + + public int MaximumTokenSizeInBytes { get; set; } = 1024 * 5; + + public bool CanReadToken(string securityToken) + { + return true; + } + + public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) + { + if (!string.IsNullOrEmpty(securityToken) && securityToken.Contains("ThisIsAValidToken")) + { + validatedToken = new TestSecurityToken(); + return new ClaimsPrincipal(new ClaimsIdentity("Test")); + } + + throw new SecurityTokenException("The security token did not contain ThisIsAValidToken"); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/ValidToken.xml b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/ValidToken.xml new file mode 100644 index 000000000..2addae96c --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/ValidToken.xml @@ -0,0 +1,83 @@ + + + 2014-04-18T20:21:17.341Z + 2014-04-19T08:21:17.341Z + + + +
http://automation1/
+
+
+ + + https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/ + + + + + + + + + + + Lkq0wTyTFxLUU2cyx0XybJqhka5RzRGj6kC4aIpFg+g= + + + bPwNswOB/B9xcdAljIkin9A2vjq+u94JdyvK03mf8vZFGUYNu9uN/Q6ims1DvW1FnP7SgFBwhIvW5OjZyW8fdYGhC2bq36izkxH6ulkWbciOcyELkyHDACLudvh8kP/Q+IwpicefKzAeI2Qu/5MFq16vFg5YgI+dovg8u1fYPPEPmmptW893RNTHWeh9mLRpLYnHyg7aLG6emNRkEu7w9rzeoICeMFybb9BvJl/q/8MFCW/Z5WemQhCi6YXFSEwCO6zJzCFi/3T6ChU/xYgXbFykDLqulsNOCQxdgutyqxJzugt+3PH5IKHHuoqe7UZNUIyELJ4BgwE1sXCGYIi24rg== + + + ThisIsAValidToken + + + + + t0ch1TsP0pi5VoW8q5CGWsCXVZoNtpsg0mbMZPOYb4I + + + + + http://Automation1 + + + + + Test + + + Test + + + user1@praburajgmail.onmicrosoft.com + + + 4afbc689-805b-48cf-a24c-d4aa3248a248 + + + c2f0cd49-5e53-4520-8ed9-4e178dc488c5 + + + https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/ + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + + + _660ec874-f70a-4997-a9c4-bd591f1c7469 + + + + + _660ec874-f70a-4997-a9c4-bd591f1c7469 + + + http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0 + http://schemas.xmlsoap.org/ws/2005/02/trust/Issue + http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey +
\ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/WsFederationTest.cs b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/WsFederationTest.cs new file mode 100644 index 000000000..bc1ef757f --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/WsFederationTest.cs @@ -0,0 +1,443 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.WsFederation +{ + public class WsFederationTest + { + [Fact] + public async Task VerifySchemeDefaults() + { + var services = new ServiceCollection(); + services.AddAuthentication().AddWsFederation(); + var sp = services.BuildServiceProvider(); + var schemeProvider = sp.GetRequiredService(); + var scheme = await schemeProvider.GetSchemeAsync(WsFederationDefaults.AuthenticationScheme); + Assert.NotNull(scheme); + Assert.Equal("WsFederationHandler", scheme.HandlerType.Name); + Assert.Equal(WsFederationDefaults.AuthenticationScheme, scheme.DisplayName); + } + + [Fact] + public async Task MissingConfigurationThrows() + { + var builder = new WebHostBuilder() + .Configure(ConfigureApp) + .ConfigureServices(services => + { + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddWsFederation(); + }); + var server = new TestServer(builder); + var httpClient = server.CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var exception = await Assert.ThrowsAsync(() => httpClient.GetAsync("/")); + Assert.Equal("Provide MetadataAddress, Configuration, or ConfigurationManager to WsFederationOptions", exception.Message); + } + + [Fact] + public async Task ChallengeRedirects() + { + var httpClient = CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/"); + Assert.Equal("https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed", response.Headers.Location.GetLeftPart(System.UriPartial.Path)); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + Assert.Equal("http://Automation1", queryItems["wtrealm"]); + Assert.True(queryItems["wctx"].ToString().Equals(CustomStateDataFormat.ValidStateData), "wctx does not equal ValidStateData"); + Assert.Equal(httpClient.BaseAddress + "signin-wsfed", queryItems["wreply"]); + Assert.Equal("wsignin1.0", queryItems["wa"]); + } + + [Fact] + public async Task MapWillNotAffectRedirect() + { + var httpClient = CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/mapped-challenge"); + Assert.Equal("https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed", response.Headers.Location.GetLeftPart(System.UriPartial.Path)); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + Assert.Equal("http://Automation1", queryItems["wtrealm"]); + Assert.True(queryItems["wctx"].ToString().Equals(CustomStateDataFormat.ValidStateData), "wctx does not equal ValidStateData"); + Assert.Equal(httpClient.BaseAddress + "signin-wsfed", queryItems["wreply"]); + Assert.Equal("wsignin1.0", queryItems["wa"]); + } + + [Fact] + public async Task PreMappedWillAffectRedirect() + { + var httpClient = CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/premapped-challenge"); + Assert.Equal("https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed", response.Headers.Location.GetLeftPart(System.UriPartial.Path)); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + Assert.Equal("http://Automation1", queryItems["wtrealm"]); + Assert.True(queryItems["wctx"].ToString().Equals(CustomStateDataFormat.ValidStateData), "wctx does not equal ValidStateData"); + Assert.Equal(httpClient.BaseAddress + "premapped-challenge/signin-wsfed", queryItems["wreply"]); + Assert.Equal("wsignin1.0", queryItems["wa"]); + } + + [Fact] + public async Task ValidTokenIsAccepted() + { + var httpClient = CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/"); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + var request = new HttpRequestMessage(HttpMethod.Post, queryItems["wreply"]); + CopyCookies(response, request); + request.Content = CreateSignInContent("WsFederation/ValidToken.xml", queryItems["wctx"]); + response = await httpClient.SendAsync(request); + + Assert.Equal(HttpStatusCode.Found, response.StatusCode); + + request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location); + CopyCookies(response, request); + response = await httpClient.SendAsync(request); + + // Did the request end in the actual resource requested for + Assert.Equal(WsFederationDefaults.AuthenticationScheme, await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task ValidUnsolicitedTokenIsRefused() + { + var httpClient = CreateClient(); + var form = CreateSignInContent("WsFederation/ValidToken.xml", suppressWctx: true); + var exception = await Assert.ThrowsAsync(() => httpClient.PostAsync(httpClient.BaseAddress + "signin-wsfed", form)); + Assert.Contains("Unsolicited logins are not allowed.", exception.InnerException.Message); + } + + [Fact] + public async Task ValidUnsolicitedTokenIsAcceptedWhenAllowed() + { + var httpClient = CreateClient(allowUnsolicited: true); + + var form = CreateSignInContent("WsFederation/ValidToken.xml", suppressWctx: true); + var response = await httpClient.PostAsync(httpClient.BaseAddress + "signin-wsfed", form); + + Assert.Equal(HttpStatusCode.Found, response.StatusCode); + + var request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location); + CopyCookies(response, request); + response = await httpClient.SendAsync(request); + + // Did the request end in the actual resource requested for + Assert.Equal(WsFederationDefaults.AuthenticationScheme, await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task InvalidTokenIsRejected() + { + var httpClient = CreateClient(); + + // Verify if the request is redirected to STS with right parameters + var response = await httpClient.GetAsync("/"); + var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query); + + var request = new HttpRequestMessage(HttpMethod.Post, queryItems["wreply"]); + CopyCookies(response, request); + request.Content = CreateSignInContent("WsFederation/InvalidToken.xml", queryItems["wctx"]); + response = await httpClient.SendAsync(request); + + // Did the request end in the actual resource requested for + Assert.Equal("AuthenticationFailed", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task RemoteSignoutRequestTriggersSignout() + { + var httpClient = CreateClient(); + + var response = await httpClient.GetAsync("/signin-wsfed?wa=wsignoutcleanup1.0"); + response.EnsureSuccessStatusCode(); + + var cookie = response.Headers.GetValues(HeaderNames.SetCookie).Single(); + Assert.Equal(".AspNetCore.Cookies=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; samesite=lax", cookie); + Assert.Equal("OnRemoteSignOut", response.Headers.GetValues("EventHeader").Single()); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task EventsResolvedFromDI() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddWsFederation(options => + { + options.Wtrealm = "http://Automation1"; + options.MetadataAddress = "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/federationmetadata/2007-06/federationmetadata.xml"; + options.BackchannelHttpHandler = new WaadMetadataDocumentHandler(); + options.EventsType = typeof(MyWsFedEvents); + }); + }) + .Configure(app => + { + app.Run(context => context.ChallengeAsync()); + }); + var server = new TestServer(builder); + + var result = await server.CreateClient().GetAsync(""); + Assert.Contains("CustomKey=CustomValue", result.Headers.Location.Query); + } + + private class MyWsFedEvents : WsFederationEvents + { + public override Task RedirectToIdentityProvider(RedirectContext context) + { + context.ProtocolMessage.SetParameter("CustomKey", "CustomValue"); + return base.RedirectToIdentityProvider(context); + } + } + + private FormUrlEncodedContent CreateSignInContent(string tokenFile, string wctx = null, bool suppressWctx = false) + { + var kvps = new List>(); + kvps.Add(new KeyValuePair("wa", "wsignin1.0")); + kvps.Add(new KeyValuePair("wresult", File.ReadAllText(tokenFile))); + if (!string.IsNullOrEmpty(wctx)) + { + kvps.Add(new KeyValuePair("wctx", wctx)); + } + if (suppressWctx) + { + kvps.Add(new KeyValuePair("suppressWctx", "true")); + } + return new FormUrlEncodedContent(kvps); + } + + private void CopyCookies(HttpResponseMessage response, HttpRequestMessage request) + { + var cookies = SetCookieHeaderValue.ParseList(response.Headers.GetValues(HeaderNames.SetCookie).ToList()); + foreach (var cookie in cookies) + { + if (cookie.Value.HasValue) + { + request.Headers.Add(HeaderNames.Cookie, new CookieHeaderValue(cookie.Name, cookie.Value).ToString()); + } + } + } + + private HttpClient CreateClient(bool allowUnsolicited = false) + { + var builder = new WebHostBuilder() + .Configure(ConfigureApp) + .ConfigureServices(services => + { + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme; + }) + .AddCookie() + .AddWsFederation(options => + { + options.Wtrealm = "http://Automation1"; + options.MetadataAddress = "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/federationmetadata/2007-06/federationmetadata.xml"; + options.BackchannelHttpHandler = new WaadMetadataDocumentHandler(); + options.StateDataFormat = new CustomStateDataFormat(); + options.SecurityTokenHandlers = new List() { new TestSecurityTokenValidator() }; + options.UseTokenLifetime = false; + options.AllowUnsolicitedLogins = allowUnsolicited; + options.Events = new WsFederationEvents() + { + OnMessageReceived = context => + { + if (!context.ProtocolMessage.Parameters.TryGetValue("suppressWctx", out var suppress)) + { + Assert.True(context.ProtocolMessage.Wctx.Equals("customValue"), "wctx is not my custom value"); + } + context.HttpContext.Items["MessageReceived"] = true; + return Task.FromResult(0); + }, + OnRedirectToIdentityProvider = context => + { + if (context.ProtocolMessage.IsSignInMessage) + { + // Sign in message + context.ProtocolMessage.Wctx = "customValue"; + } + + return Task.FromResult(0); + }, + OnSecurityTokenReceived = context => + { + context.HttpContext.Items["SecurityTokenReceived"] = true; + return Task.FromResult(0); + }, + OnSecurityTokenValidated = context => + { + Assert.True((bool)context.HttpContext.Items["MessageReceived"], "MessageReceived notification not invoked"); + Assert.True((bool)context.HttpContext.Items["SecurityTokenReceived"], "SecurityTokenReceived notification not invoked"); + + if (context.Principal != null) + { + var identity = context.Principal.Identities.Single(); + identity.AddClaim(new Claim("ReturnEndpoint", "true")); + identity.AddClaim(new Claim("Authenticated", "true")); + identity.AddClaim(new Claim(identity.RoleClaimType, "Guest", ClaimValueTypes.String)); + } + + return Task.FromResult(0); + }, + OnAuthenticationFailed = context => + { + context.HttpContext.Items["AuthenticationFailed"] = true; + //Change the request url to something different and skip Wsfed. This new url will handle the request and let us know if this notification was invoked. + context.HttpContext.Request.Path = new PathString("/AuthenticationFailed"); + context.SkipHandler(); + return Task.FromResult(0); + }, + OnRemoteSignOut = context => + { + context.Response.Headers["EventHeader"] = "OnRemoteSignOut"; + return Task.FromResult(0); + } + }; + }); + }); + var server = new TestServer(builder); + return server.CreateClient(); + } + + private void ConfigureApp(IApplicationBuilder app) + { + app.Map("/PreMapped-Challenge", mapped => + { + mapped.UseAuthentication(); + mapped.Run(async context => + { + await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme); + }); + }); + + app.UseAuthentication(); + + app.Map("/Logout", subApp => + { + subApp.Run(async context => + { + if (context.User.Identity.IsAuthenticated) + { + var authProperties = new AuthenticationProperties() { RedirectUri = context.Request.GetEncodedUrl() }; + await context.SignOutAsync(WsFederationDefaults.AuthenticationScheme, authProperties); + await context.Response.WriteAsync("Signing out..."); + } + else + { + await context.Response.WriteAsync("SignedOut"); + } + }); + }); + + app.Map("/AuthenticationFailed", subApp => + { + subApp.Run(async context => + { + await context.Response.WriteAsync("AuthenticationFailed"); + }); + }); + + app.Map("/signout-wsfed", subApp => + { + subApp.Run(async context => + { + await context.Response.WriteAsync("signout-wsfed"); + }); + }); + + app.Map("/mapped-challenge", subApp => + { + subApp.Run(async context => + { + await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme); + }); + }); + + app.Run(async context => + { + var result = context.AuthenticateAsync(); + if (context.User == null || !context.User.Identity.IsAuthenticated) + { + await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme); + await context.Response.WriteAsync("Unauthorized"); + } + else + { + var identity = context.User.Identities.Single(); + if (identity.NameClaimType == "Name_Failed" && identity.RoleClaimType == "Role_Failed") + { + context.Response.StatusCode = 500; + await context.Response.WriteAsync("SignIn_Failed"); + } + else if (!identity.HasClaim("Authenticated", "true") || !identity.HasClaim("ReturnEndpoint", "true") || !identity.HasClaim(identity.RoleClaimType, "Guest")) + { + await context.Response.WriteAsync("Provider not invoked"); + return; + } + else + { + await context.Response.WriteAsync(WsFederationDefaults.AuthenticationScheme); + } + } + }); + } + + private class WaadMetadataDocumentHandler : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var metadata = File.ReadAllText(@"WsFederation/federationmetadata.xml"); + var newResponse = new HttpResponseMessage() { Content = new StringContent(metadata, Encoding.UTF8, "text/xml") }; + return Task.FromResult(newResponse); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/federationmetadata.xml b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/federationmetadata.xml new file mode 100644 index 000000000..920ed66a4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/WsFederation/federationmetadata.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + wFJy/A1QstqtLHauYGcqwwHvn3HUW25DcWI/XLOmXOM= + + + R6fPw+BiFS9XYdkhwNJRjGxVftA2j9TdkF5d5jgR8uG1QMyuEA/Eizeq1HnnUj2Yi+sqNG+HzaZQclECeiJfi88Ry+keorDCo9KgdnjlZZc+WFzrJZeHjaDIvFD6B4OAN0mTq5kbpwr7+idzSbvyRXAnpvJxOrViZKE4HpwltGAZGDTkjsVkd8Z/wfoN7ehN4Ei7u/mOAiEU4FkWYFU/BfSVRVIUDyyQ7DGfQFJvCwHWFvsq+M1wfOUzQO5K+M9EU2m4VEP1qqbexXaZMAbcjqyUn4eN7doWjWE59jkXGbn+GR8qgCJqLOaYwXnH5XD0pMjy71aKGyLNaUb3wCwjkA== + + + MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng + + + + + + + + MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ + + + + + + + MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng + + + + + + UPN + UPN of the user + + + Name + The display name for the user + + + Given Name + First name of the user + + + Surname + Last name of the user + + + Authentication Instant + The time (UTC) at which the user authenticated to the identity provider + + + Authentication Method + The method of authentication used by the identity provider + + + TenantId + Identifier for the user's tenant + + + IdentityProvider + Identity provider for the user. + + + + +
https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed
+
+
+ + +
https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed
+
+
+
+ + + + + MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ + + + + + + + MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng + + + + + +
https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/
+
+
+ + +
https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed
+
+
+ + +
https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/wsfed
+
+
+
+ + + + + MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ + + + + + + + MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng + + + + + + +
\ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/project.json b/test/Microsoft.AspNetCore.Authentication.Test/project.json deleted file mode 100644 index 931b20be1..000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/project.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "compilationOptions": { - "warningsAsErrors": true - }, - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1-*", - "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-*", - "Microsoft.AspNetCore.Authentication.Facebook": "1.0.0-*", - "Microsoft.AspNetCore.Authentication.Google": "1.0.0-*", - "Microsoft.AspNetCore.Authentication.JwtBearer": "1.0.0-*", - "Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.0.0-*", - "Microsoft.AspNetCore.Authentication.OpenIdConnect": "0.1.0-*", - "Microsoft.AspNetCore.Authentication.Twitter": "1.0.0-*", - "Microsoft.AspNetCore.TestHost": "1.0.0-*", - "Microsoft.AspNetCore.Testing": "1.0.0-*", - "xunit": "2.1.0" - }, - "frameworks": { - "netcoreapp1.0": { - "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.0.0-*", - "type": "platform" - }, - "dotnet-test-xunit": "1.0.0-*", - "System.Diagnostics.Process": "4.1.0-*" - }, - "imports": [ - "dnxcore50", - "portable-net451+win8" - ] - }, - "net451": { - "frameworkAssemblies": { - "System.Runtime": "", - "System.Threading.Tasks": "" - }, - "dependencies": { - "xunit.runner.console": "2.1.0" - } - } - }, - "testRunner": "xunit" -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationAppBuilderExtensionsTests.cs b/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationAppBuilderExtensionsTests.cs new file mode 100644 index 000000000..c6fb596f1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationAppBuilderExtensionsTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization.Test.TestObjects; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authorization.Test +{ + public class AuthorizationAppBuilderExtensionsTests + { + [Fact] + public async Task UseAuthorization_RegistersMiddleware() + { + // Arrange + var authenticationService = new TestAuthenticationService(); + var services = CreateServices(authenticationService); + + var app = new ApplicationBuilder(services); + + app.UseAuthorization(); + + var appFunc = app.Build(); + + var endpoint = new Endpoint( + null, + new EndpointMetadataCollection(new AuthorizeAttribute()), + "Test endpoint"); + + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = services; + httpContext.SetEndpoint(endpoint); + + // Act + await appFunc(httpContext); + + // Assert + Assert.True(authenticationService.ChallengeCalled); + } + + private IServiceProvider CreateServices(IAuthenticationService authenticationService) + { + var services = new ServiceCollection(); + + services.AddAuthorization(options => { }); + services.AddAuthorizationPolicyEvaluator(); + services.AddLogging(); + services.AddSingleton(authenticationService); + + var serviceProvder = services.BuildServiceProvider(); + + return serviceProvder; + } + } +} diff --git a/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationEndpointConventionBuilderExtensionsTests.cs b/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationEndpointConventionBuilderExtensionsTests.cs new file mode 100644 index 000000000..a2ab8fa8b --- /dev/null +++ b/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationEndpointConventionBuilderExtensionsTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Xunit; + +namespace Microsoft.AspNetCore.Authorization.Test +{ + public class AuthorizationEndpointConventionBuilderExtensionsTests + { + [Fact] + public void RequireAuthorization_IAuthorizeData() + { + // Arrange + var builder = new TestEndpointConventionBuilder(); + var metadata = new AuthorizeAttribute(); + + // Act + builder.RequireAuthorization(metadata); + + // Assert + var convention = Assert.Single(builder.Conventions); + + var endpointModel = new RouteEndpointModel((context) => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0); + convention(endpointModel); + + Assert.Equal(metadata, Assert.Single(endpointModel.Metadata)); + } + + [Fact] + public void RequireAuthorization_PolicyName() + { + // Arrange + var builder = new TestEndpointConventionBuilder(); + + // Act + builder.RequireAuthorization("policy"); + + // Assert + var convention = Assert.Single(builder.Conventions); + + var endpointModel = new RouteEndpointModel((context) => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0); + convention(endpointModel); + + Assert.Equal("policy", Assert.IsAssignableFrom(Assert.Single(endpointModel.Metadata)).Policy); + } + + private class TestEndpointConventionBuilder : IEndpointConventionBuilder + { + public IList> Conventions { get; } = new List>(); + + public void Apply(Action convention) + { + Conventions.Add(convention); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationMiddlewareTests.cs new file mode 100644 index 000000000..655a4fbf8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationMiddlewareTests.cs @@ -0,0 +1,459 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Infrastructure; +using Microsoft.AspNetCore.Authorization.Test.TestObjects; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Authorization.Test +{ + public class AuthorizationMiddlewareTests + { + [Fact] + public async Task NoEndpoint_AnonymousUser_Allows() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(anonymous: true); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.True(next.Called); + } + + [Fact] + public async Task NoEndpointWithRequired_AnonymousUser_Challenges() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetRequiredPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(anonymous: true); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.False(next.Called); + } + + [Fact] + public async Task HasEndpointWithoutAuth_AnonymousUser_Allows() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(anonymous: true, endpoint: CreateEndpoint()); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.True(next.Called); + } + + [Fact] + public async Task HasEndpointWithRequiredWithoutAuth_AnonymousUser_Challenges() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + policyProvider.Setup(p => p.GetRequiredPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(anonymous: true, endpoint: CreateEndpoint()); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.False(next.Called); + } + + [Fact] + public async Task HasEndpointWithAuth_AnonymousUser_Challenges() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + var authenticationService = new TestAuthenticationService(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(anonymous: true, endpoint: CreateEndpoint(new AuthorizeAttribute()), authenticationService: authenticationService); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.False(next.Called); + Assert.True(authenticationService.ChallengeCalled); + } + + [Fact] + public async Task HasEndpointWithAuth_AnonymousUser_ChallengePerScheme() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().AddAuthenticationSchemes("schema1", "schema2").Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + var authenticationService = new TestAuthenticationService(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(anonymous: true, endpoint: CreateEndpoint(new AuthorizeAttribute()), authenticationService: authenticationService); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.False(next.Called); + Assert.Equal(2, authenticationService.ChallengeCount); + } + + [Fact] + public async Task OnAuthorizationAsync_WillCallPolicyProvider() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build(); + var policyProvider = new Mock(); + var getPolicyCount = 0; + var getRequiredPolicyCount = 0; + policyProvider.Setup(p => p.GetPolicyAsync(It.IsAny())).ReturnsAsync(policy) + .Callback(() => getPolicyCount++); + policyProvider.Setup(p => p.GetRequiredPolicyAsync()).ReturnsAsync(policy) + .Callback(() => getRequiredPolicyCount++); + var next = new TestRequestDelegate(); + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(anonymous: true, endpoint: CreateEndpoint(new AuthorizeAttribute("whatever"))); + + // Act & Assert + await middleware.Invoke(context); + Assert.Equal(1, getPolicyCount); + Assert.Equal(1, getRequiredPolicyCount); + Assert.Equal(1, next.CalledCount); + + await middleware.Invoke(context); + Assert.Equal(2, getPolicyCount); + Assert.Equal(2, getRequiredPolicyCount); + Assert.Equal(2, next.CalledCount); + + await middleware.Invoke(context); + Assert.Equal(3, getPolicyCount); + Assert.Equal(3, getRequiredPolicyCount); + Assert.Equal(3, next.CalledCount); + } + + [Fact] + public async Task Invoke_ValidClaimShouldNotFail() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireClaim("Permission", "CanViewPage").Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(endpoint: CreateEndpoint(new AuthorizeAttribute())); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.True(next.Called); + } + + [Fact] + public async Task HasEndpointWithAuthAndAllowAnonymous_AnonymousUser_Allows() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + var authenticationService = new TestAuthenticationService(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(anonymous: true, endpoint: CreateEndpoint(new AuthorizeAttribute(), new AllowAnonymousAttribute()), authenticationService: authenticationService); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.True(next.Called); + Assert.False(authenticationService.ChallengeCalled); + } + + [Fact] + public async Task HasEndpointWithAuth_AuthenticatedUser_Allows() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + var authenticationService = new TestAuthenticationService(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(endpoint: CreateEndpoint(new AuthorizeAttribute()), authenticationService: authenticationService); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.True(next.Called); + Assert.False(authenticationService.ChallengeCalled); + } + + [Fact] + public async Task Invoke_AuthSchemesFailShouldSetEmptyPrincipalOnContext() + { + // Arrange + var policy = new AuthorizationPolicyBuilder("Fails").RequireAuthenticatedUser().Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + var authenticationService = new TestAuthenticationService(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(endpoint: CreateEndpoint(new AuthorizeAttribute()), authenticationService: authenticationService); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.False(next.Called); + Assert.NotNull(context.User?.Identity); + Assert.True(authenticationService.AuthenticateCalled); + Assert.True(authenticationService.ChallengeCalled); + } + + [Fact] + public async Task Invoke_SingleValidClaimShouldSucceed() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireClaim("Permission", "CanViewComment", "CanViewPage").Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(endpoint: CreateEndpoint(new AuthorizeAttribute())); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.True(next.Called); + } + + [Fact] + public async Task AuthZResourceShouldBeEndpoint() + { + // Arrange + object resource = null; + var policy = new AuthorizationPolicyBuilder().RequireAssertion(c => + { + resource = c.Resource; + return true; + }).Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var endpoint = CreateEndpoint(new AuthorizeAttribute()); + var context = GetHttpContext(endpoint: endpoint); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(endpoint, resource); + } + + [Fact] + public async Task Invoke_RequireUnknownRoleShouldForbid() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireRole("Wut").Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + var authenticationService = new TestAuthenticationService(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(endpoint: CreateEndpoint(new AuthorizeAttribute()), authenticationService: authenticationService); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.False(next.Called); + Assert.False(authenticationService.ChallengeCalled); + Assert.True(authenticationService.ForbidCalled); + } + + [Fact] + public async Task Invoke_RequireUnknownRole_ForbidPerScheme() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireRole("Wut").AddAuthenticationSchemes("Basic", "Bearer").Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + var authenticationService = new TestAuthenticationService(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(endpoint: CreateEndpoint(new AuthorizeAttribute()), authenticationService: authenticationService); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.False(next.Called); + Assert.Equal(2, authenticationService.ForbidCount); + } + + [Fact] + public async Task Invoke_InvalidClaimShouldForbid() + { + // Arrange + var policy = new AuthorizationPolicyBuilder() + .RequireClaim("Permission", "CanViewComment") + .Build(); + var policyProvider = new Mock(); + policyProvider.Setup(p => p.GetDefaultPolicyAsync()).ReturnsAsync(policy); + var next = new TestRequestDelegate(); + var authenticationService = new TestAuthenticationService(); + + var middleware = CreateMiddleware(next.Invoke, policyProvider.Object); + var context = GetHttpContext(endpoint: CreateEndpoint(new AuthorizeAttribute()), authenticationService: authenticationService); + + // Act + await middleware.Invoke(context); + + // Assert + Assert.False(next.Called); + Assert.False(authenticationService.ChallengeCalled); + Assert.True(authenticationService.ForbidCalled); + } + + private AuthorizationMiddleware CreateMiddleware(RequestDelegate requestDelegate = null, IAuthorizationPolicyProvider policyProvider = null) + { + requestDelegate = requestDelegate ?? ((context) => Task.CompletedTask); + + return new AuthorizationMiddleware(requestDelegate, policyProvider); + } + + private Endpoint CreateEndpoint(params object[] metadata) + { + return new Endpoint(context => Task.CompletedTask, new EndpointMetadataCollection(metadata), "Test endpoint"); + } + + private HttpContext GetHttpContext( + bool anonymous = false, + Action registerServices = null, + Endpoint endpoint = null, + IAuthenticationService authenticationService = null) + { + var basicPrincipal = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewPage"), + new Claim(ClaimTypes.Role, "Administrator"), + new Claim(ClaimTypes.Role, "User"), + new Claim(ClaimTypes.NameIdentifier, "John")}, + "Basic")); + + var validUser = basicPrincipal; + + var bearerIdentity = new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CupBearer"), + new Claim(ClaimTypes.Role, "Token"), + new Claim(ClaimTypes.NameIdentifier, "John Bear")}, + "Bearer"); + + validUser.AddIdentity(bearerIdentity); + + // ServiceProvider + var serviceCollection = new ServiceCollection(); + + authenticationService = authenticationService ?? Mock.Of(); + + serviceCollection.AddSingleton(authenticationService); + serviceCollection.AddOptions(); + serviceCollection.AddLogging(); + serviceCollection.AddAuthorization(); + serviceCollection.AddAuthorizationPolicyEvaluator(); + registerServices?.Invoke(serviceCollection); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + //// HttpContext + var httpContext = new DefaultHttpContext(); + if (endpoint != null) + { + httpContext.SetEndpoint(endpoint); + } + httpContext.RequestServices = serviceProvider; + if (!anonymous) + { + httpContext.User = validUser; + } + + return httpContext; + } + + private class TestRequestDelegate + { + private readonly int _statusCode; + + public bool Called => CalledCount > 0; + public int CalledCount { get; private set; } + + public TestRequestDelegate(int statusCode = 200) + { + _statusCode = statusCode; + } + + public Task Invoke(HttpContext context) + { + CalledCount++; + context.Response.StatusCode = _statusCode; + return Task.CompletedTask; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationPolicyFacts.cs b/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationPolicyFacts.cs index f74e46152..143be1b9b 100644 --- a/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationPolicyFacts.cs +++ b/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationPolicyFacts.cs @@ -3,11 +3,13 @@ using System; using System.Linq; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; +using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.AspNetCore.Authroization.Test +namespace Microsoft.AspNetCore.Authorization.Test { public class AuthorizationPolicyFacts { @@ -18,130 +20,140 @@ public void RequireRoleThrowsIfEmpty() } [Fact] - public void CanCombineAuthorizeAttributes() + public async Task CanCombineAuthorizeAttributes() { // Arrange var attributes = new AuthorizeAttribute[] { new AuthorizeAttribute(), - new AuthorizeAttribute("1") { ActiveAuthenticationSchemes = "dupe" }, - new AuthorizeAttribute("2") { ActiveAuthenticationSchemes = "dupe" }, - new AuthorizeAttribute { Roles = "r1,r2", ActiveAuthenticationSchemes = "roles" }, + new AuthorizeAttribute("1") { AuthenticationSchemes = "dupe" }, + new AuthorizeAttribute("2") { AuthenticationSchemes = "dupe" }, + new AuthorizeAttribute { Roles = "r1,r2", AuthenticationSchemes = "roles" }, }; var options = new AuthorizationOptions(); options.AddPolicy("1", policy => policy.RequireClaim("1")); options.AddPolicy("2", policy => policy.RequireClaim("2")); + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options)); + // Act - var combined = AuthorizationPolicy.Combine(options, attributes); + var combined = await AuthorizationPolicy.CombineAsync(provider, attributes); // Assert Assert.Equal(2, combined.AuthenticationSchemes.Count()); - Assert.True(combined.AuthenticationSchemes.Contains("dupe")); - Assert.True(combined.AuthenticationSchemes.Contains("roles")); + Assert.Contains("dupe", combined.AuthenticationSchemes); + Assert.Contains("roles", combined.AuthenticationSchemes); Assert.Equal(4, combined.Requirements.Count()); - Assert.True(combined.Requirements.Any(r => r is DenyAnonymousAuthorizationRequirement)); + Assert.Contains(combined.Requirements, r => r is DenyAnonymousAuthorizationRequirement); Assert.Equal(2, combined.Requirements.OfType().Count()); - Assert.Equal(1, combined.Requirements.OfType().Count()); + Assert.Single(combined.Requirements.OfType()); } [Fact] - public void CanReplaceDefaultPolicy() + public async Task CanReplaceDefaultPolicy() { // Arrange var attributes = new AuthorizeAttribute[] { new AuthorizeAttribute(), - new AuthorizeAttribute("2") { ActiveAuthenticationSchemes = "dupe" } + new AuthorizeAttribute("2") { AuthenticationSchemes = "dupe" } }; var options = new AuthorizationOptions(); options.DefaultPolicy = new AuthorizationPolicyBuilder("default").RequireClaim("default").Build(); options.AddPolicy("2", policy => policy.RequireClaim("2")); + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options)); + // Act - var combined = AuthorizationPolicy.Combine(options, attributes); + var combined = await AuthorizationPolicy.CombineAsync(provider, attributes); // Assert Assert.Equal(2, combined.AuthenticationSchemes.Count()); - Assert.True(combined.AuthenticationSchemes.Contains("dupe")); - Assert.True(combined.AuthenticationSchemes.Contains("default")); + Assert.Contains("dupe", combined.AuthenticationSchemes); + Assert.Contains("default", combined.AuthenticationSchemes); Assert.Equal(2, combined.Requirements.Count()); - Assert.False(combined.Requirements.Any(r => r is DenyAnonymousAuthorizationRequirement)); + Assert.DoesNotContain(combined.Requirements, r => r is DenyAnonymousAuthorizationRequirement); Assert.Equal(2, combined.Requirements.OfType().Count()); } [Fact] - public void CombineMustTrimRoles() + public async Task CombineMustTrimRoles() { // Arrange var attributes = new AuthorizeAttribute[] { new AuthorizeAttribute() { Roles = "r1 , r2" } }; var options = new AuthorizationOptions(); + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options)); // Act - var combined = AuthorizationPolicy.Combine(options, attributes); + var combined = await AuthorizationPolicy.CombineAsync(provider, attributes); // Assert - Assert.True(combined.Requirements.Any(r => r is RolesAuthorizationRequirement)); + Assert.Contains(combined.Requirements, r => r is RolesAuthorizationRequirement); var rolesAuthorizationRequirement = combined.Requirements.OfType().First(); Assert.Equal(2, rolesAuthorizationRequirement.AllowedRoles.Count()); - Assert.True(rolesAuthorizationRequirement.AllowedRoles.Any(r => r.Equals("r1"))); - Assert.True(rolesAuthorizationRequirement.AllowedRoles.Any(r => r.Equals("r2"))); + Assert.Contains(rolesAuthorizationRequirement.AllowedRoles, r => r.Equals("r1")); + Assert.Contains(rolesAuthorizationRequirement.AllowedRoles, r => r.Equals("r2")); } [Fact] - public void CombineMustTrimAuthenticationScheme() + public async Task CombineMustTrimAuthenticationScheme() { // Arrange var attributes = new AuthorizeAttribute[] { - new AuthorizeAttribute() { ActiveAuthenticationSchemes = "a1 , a2" } + new AuthorizeAttribute() { AuthenticationSchemes = "a1 , a2" } }; var options = new AuthorizationOptions(); + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options)); + // Act - var combined = AuthorizationPolicy.Combine(options, attributes); + var combined = await AuthorizationPolicy.CombineAsync(provider, attributes); // Assert Assert.Equal(2, combined.AuthenticationSchemes.Count()); - Assert.True(combined.AuthenticationSchemes.Any(a => a.Equals("a1"))); - Assert.True(combined.AuthenticationSchemes.Any(a => a.Equals("a2"))); + Assert.Contains(combined.AuthenticationSchemes, a => a.Equals("a1")); + Assert.Contains(combined.AuthenticationSchemes, a => a.Equals("a2")); } [Fact] - public void CombineMustIgnoreEmptyAuthenticationScheme() + public async Task CombineMustIgnoreEmptyAuthenticationScheme() { // Arrange var attributes = new AuthorizeAttribute[] { - new AuthorizeAttribute() { ActiveAuthenticationSchemes = "a1 , , ,,, a2" } + new AuthorizeAttribute() { AuthenticationSchemes = "a1 , , ,,, a2" } }; var options = new AuthorizationOptions(); + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options)); + // Act - var combined = AuthorizationPolicy.Combine(options, attributes); + var combined = await AuthorizationPolicy.CombineAsync(provider, attributes); // Assert Assert.Equal(2, combined.AuthenticationSchemes.Count()); - Assert.True(combined.AuthenticationSchemes.Any(a => a.Equals("a1"))); - Assert.True(combined.AuthenticationSchemes.Any(a => a.Equals("a2"))); + Assert.Contains(combined.AuthenticationSchemes, a => a.Equals("a1")); + Assert.Contains(combined.AuthenticationSchemes, a => a.Equals("a2")); } [Fact] - public void CombineMustIgnoreEmptyRoles() + public async Task CombineMustIgnoreEmptyRoles() { // Arrange var attributes = new AuthorizeAttribute[] { new AuthorizeAttribute() { Roles = "r1 , ,, , r2" } }; var options = new AuthorizationOptions(); + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(options)); // Act - var combined = AuthorizationPolicy.Combine(options, attributes); + var combined = await AuthorizationPolicy.CombineAsync(provider, attributes); // Assert - Assert.True(combined.Requirements.Any(r => r is RolesAuthorizationRequirement)); + Assert.Contains(combined.Requirements, r => r is RolesAuthorizationRequirement); var rolesAuthorizationRequirement = combined.Requirements.OfType().First(); Assert.Equal(2, rolesAuthorizationRequirement.AllowedRoles.Count()); - Assert.True(rolesAuthorizationRequirement.AllowedRoles.Any(r => r.Equals("r1"))); - Assert.True(rolesAuthorizationRequirement.AllowedRoles.Any(r => r.Equals("r2"))); + Assert.Contains(rolesAuthorizationRequirement.AllowedRoles, r => r.Equals("r1")); + Assert.Contains(rolesAuthorizationRequirement.AllowedRoles, r => r.Equals("r2")); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs b/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs index 7f1191694..230e95c4c 100644 --- a/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs +++ b/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization.Infrastructure; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Authorization.Test @@ -20,17 +21,17 @@ private IAuthorizationService BuildAuthorizationService(Action(); } [Fact] - public void AuthorizeCombineThrowsOnUnknownPolicy() + public async Task AuthorizeCombineThrowsOnUnknownPolicy() { - Assert.Throws(() => AuthorizationPolicy.Combine(new AuthorizationOptions(), new AuthorizeAttribute[] { + var provider = new DefaultAuthorizationPolicyProvider(Options.Create(new AuthorizationOptions())); + + // Act + await Assert.ThrowsAsync(() => AuthorizationPolicy.CombineAsync(provider, new AuthorizeAttribute[] { new AuthorizeAttribute { Policy = "Wut" } })); } @@ -52,7 +53,7 @@ public async Task Authorize_ShouldAllowIfClaimIsPresent() var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -75,7 +76,7 @@ public async Task Authorize_ShouldAllowIfClaimIsPresentWithSpecifiedAuthType() var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -102,7 +103,73 @@ public async Task Authorize_ShouldAllowIfClaimIsAmongValues() var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); + } + + [Fact] + public async Task Authorize_ShouldInvokeAllHandlersByDefault() + { + // Arrange + var handler1 = new FailHandler(); + var handler2 = new FailHandler(); + var authorizationService = BuildAuthorizationService(services => + { + services.AddSingleton(handler1); + services.AddSingleton(handler2); + services.AddAuthorization(options => + { + options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); + }); + }); + + // Act + var allowed = await authorizationService.AuthorizeAsync(new ClaimsPrincipal(), "Custom"); + + // Assert + Assert.False(allowed.Succeeded); + Assert.True(allowed.Failure.FailCalled); + Assert.True(handler1.Invoked); + Assert.True(handler2.Invoked); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Authorize_ShouldInvokeAllHandlersDependingOnSetting(bool invokeAllHandlers) + { + // Arrange + var handler1 = new FailHandler(); + var handler2 = new FailHandler(); + var authorizationService = BuildAuthorizationService(services => + { + services.AddSingleton(handler1); + services.AddSingleton(handler2); + services.AddAuthorization(options => + { + options.InvokeHandlersAfterFailure = invokeAllHandlers; + options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); + }); + }); + + // Act + var allowed = await authorizationService.AuthorizeAsync(new ClaimsPrincipal(), "Custom"); + + // Assert + Assert.False(allowed.Succeeded); + Assert.True(handler1.Invoked); + Assert.Equal(invokeAllHandlers, handler2.Invoked); + } + + private class FailHandler : IAuthorizationHandler + { + public bool Invoked { get; set; } + + public Task HandleAsync(AuthorizationHandlerContext context) + { + Invoked = true; + context.Fail(); + return Task.FromResult(0); + } } [Fact] @@ -128,7 +195,8 @@ public async Task Authorize_ShouldFailWhenAllRequirementsNotHandled() var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); + Assert.IsType(allowed.Failure.FailedRequirements.First()); } [Fact] @@ -154,7 +222,7 @@ public async Task Authorize_ShouldNotAllowIfClaimTypeIsNotPresent() var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); } [Fact] @@ -180,7 +248,7 @@ public async Task Authorize_ShouldNotAllowIfClaimValueIsNotPresent() var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); } [Fact] @@ -204,7 +272,7 @@ public async Task Authorize_ShouldNotAllowIfNoClaims() var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); } [Fact] @@ -223,7 +291,7 @@ public async Task Authorize_ShouldNotAllowIfUserIsNull() var allowed = await authorizationService.AuthorizeAsync(null, null, "Basic"); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); } [Fact] @@ -243,7 +311,7 @@ public async Task Authorize_ShouldNotAllowIfNotCorrectAuthType() var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); } [Fact] @@ -269,7 +337,7 @@ public async Task Authorize_ShouldAllowWithNoAuthType() var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -304,7 +372,7 @@ public async Task Authorize_CustomRolePolicy() var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -325,7 +393,7 @@ public async Task Authorize_HasAnyClaimOfTypePolicy() var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -342,7 +410,7 @@ public async Task Authorize_PolicyCanAuthenticationSchemeWithNameClaim() var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -359,7 +427,7 @@ public async Task RolePolicyCanRequireSingleRole() var allowed = await authorizationService.AuthorizeAsync(user, null, policy.Build()); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -375,7 +443,7 @@ public async Task RolePolicyCanRequireOneOfManyRoles() var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -396,7 +464,7 @@ public async Task RolePolicyCanBlockWrongRole() var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); } [Fact] @@ -421,7 +489,7 @@ public async Task RolePolicyCanBlockNoRole() var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); } [Fact] @@ -459,7 +527,7 @@ public async Task RequireUserNameFailsForWrongUserName() var allowed = await authorizationService.AuthorizeAsync(user, "Hao"); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); } [Fact] @@ -485,7 +553,7 @@ public async Task CanRequireUserName() var allowed = await authorizationService.AuthorizeAsync(user, "Hao"); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -507,7 +575,7 @@ public async Task CanRequireUserNameWithDiffClaimType() var allowed = await authorizationService.AuthorizeAsync(user, "Hao"); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -529,7 +597,7 @@ public async Task CanRequireRoleWithDiffClaimType() var allowed = await authorizationService.AuthorizeAsync(user, "Hao"); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -554,7 +622,7 @@ public async Task CanApproveAnyAuthenticatedUser() var allowed = await authorizationService.AuthorizeAsync(user, null, "Any"); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -574,15 +642,19 @@ public async Task CanBlockNonAuthenticatedUser() var allowed = await authorizationService.AuthorizeAsync(user, null, "Any"); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); } public class CustomRequirement : IAuthorizationRequirement { } public class CustomHandler : AuthorizationHandler { - protected override void Handle(AuthorizationContext context, CustomRequirement requirement) + public bool Invoked { get; set; } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomRequirement requirement) { + Invoked = true; context.Succeed(requirement); + return Task.FromResult(0); } } @@ -603,7 +675,7 @@ public async Task CustomReqWithNoHandlerFails() var allowed = await authorizationService.AuthorizeAsync(user, null, "Custom"); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); } [Fact] @@ -624,7 +696,7 @@ public async Task CustomReqWithHandlerSucceeds() var allowed = await authorizationService.AuthorizeAsync(user, null, "Custom"); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } public class PassThroughRequirement : AuthorizationHandler, IAuthorizationRequirement @@ -636,11 +708,12 @@ public PassThroughRequirement(bool succeed) public bool Succeed { get; set; } - protected override void Handle(AuthorizationContext context, PassThroughRequirement requirement) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PassThroughRequirement requirement) { if (Succeed) { context.Succeed(requirement); } + return Task.FromResult(0); } } @@ -663,7 +736,7 @@ public async Task PassThroughRequirementWillSucceedWithoutCustomHandler(bool sho var allowed = await authorizationService.AuthorizeAsync(user, null, "Passthrough"); // Assert - Assert.Equal(shouldSucceed, allowed); + Assert.Equal(shouldSucceed, allowed.Succeeded); } [Fact] @@ -691,7 +764,7 @@ public async Task CanCombinePolicies() var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined"); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -718,7 +791,7 @@ public async Task CombinePoliciesWillFailIfBasePolicyFails() var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined"); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); } [Fact] @@ -745,7 +818,7 @@ public async Task CombinedPoliciesWillFailIfExtraRequirementFails() var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined"); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); } public class ExpenseReport { } @@ -766,23 +839,25 @@ public ExpenseReportAuthorizationHandler(IEnumerable _allowed; - protected override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement, ExpenseReport resource) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, ExpenseReport resource) { if (_allowed.Contains(requirement)) { context.Succeed(requirement); } + return Task.FromResult(0); } } public class SuperUserHandler : AuthorizationHandler { - protected override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement) { if (context.User.HasClaim("SuperUser", "yes")) { context.Succeed(requirement); } + return Task.FromResult(0); } } @@ -805,14 +880,14 @@ public async Task CanAuthorizeAllSuperuserOperations() // Act // Assert - Assert.True(await authorizationService.AuthorizeAsync(user, null, Operations.Edit)); - Assert.True(await authorizationService.AuthorizeAsync(user, null, Operations.Delete)); - Assert.True(await authorizationService.AuthorizeAsync(user, null, Operations.Create)); + Assert.True((await authorizationService.AuthorizeAsync(user, null, Operations.Edit)).Succeeded); + Assert.True((await authorizationService.AuthorizeAsync(user, null, Operations.Delete)).Succeeded); + Assert.True((await authorizationService.AuthorizeAsync(user, null, Operations.Create)).Succeeded); } public class NotCalledHandler : AuthorizationHandler { - protected override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement, string resource) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, string resource) { throw new NotImplementedException(); } @@ -820,12 +895,13 @@ protected override void Handle(AuthorizationContext context, OperationAuthorizat public class EvenHandler : AuthorizationHandler { - protected override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement, int id) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, int id) { if (id % 2 == 0) { context.Succeed(requirement); } + return Task.FromResult(0); } } @@ -846,8 +922,8 @@ public async Task CanUseValueTypeResource() // Act // Assert - Assert.False(await authorizationService.AuthorizeAsync(user, 1, Operations.Edit)); - Assert.True(await authorizationService.AuthorizeAsync(user, 2, Operations.Edit)); + Assert.False((await authorizationService.AuthorizeAsync(user, 1, Operations.Edit)).Succeeded); + Assert.True((await authorizationService.AuthorizeAsync(user, 2, Operations.Edit)).Succeeded); } @@ -869,7 +945,7 @@ public async Task DoesNotCallHandlerWithWrongResourceType() // Act // Assert - Assert.False(await authorizationService.AuthorizeAsync(user, 1, Operations.Edit)); + Assert.False((await authorizationService.AuthorizeAsync(user, 1, Operations.Edit)).Succeeded); } [Fact] @@ -884,9 +960,9 @@ public async Task CanAuthorizeOnlyAllowedOperations() // Act // Assert - Assert.True(await authorizationService.AuthorizeAsync(user, new ExpenseReport(), Operations.Edit)); - Assert.False(await authorizationService.AuthorizeAsync(user, new ExpenseReport(), Operations.Delete)); - Assert.False(await authorizationService.AuthorizeAsync(user, new ExpenseReport(), Operations.Create)); + Assert.True((await authorizationService.AuthorizeAsync(user, new ExpenseReport(), Operations.Edit)).Succeeded); + Assert.False((await authorizationService.AuthorizeAsync(user, new ExpenseReport(), Operations.Delete)).Succeeded); + Assert.False((await authorizationService.AuthorizeAsync(user, new ExpenseReport(), Operations.Create)).Succeeded); } [Fact] @@ -901,7 +977,7 @@ public async Task AuthorizeHandlerNotCalledWithNullResource() // Act // Assert - Assert.False(await authorizationService.AuthorizeAsync(user, null, Operations.Edit)); + Assert.False((await authorizationService.AuthorizeAsync(user, null, Operations.Edit)).Succeeded); } [Fact] @@ -920,7 +996,7 @@ public async Task CanAuthorizeWithAssertionRequirement() var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } [Fact] @@ -939,11 +1015,21 @@ public async Task CanAuthorizeWithAsyncAssertionRequirement() var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); // Assert - Assert.True(allowed); + Assert.True(allowed.Succeeded); } public class StaticPolicyProvider : IAuthorizationPolicyProvider { + public Task GetDefaultPolicyAsync() + { + return Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); + } + + public Task GetRequiredPolicyAsync() + { + return Task.FromResult(null); + } + public Task GetPolicyAsync(string policyName) { return Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); @@ -968,11 +1054,21 @@ public async Task CanReplaceDefaultPolicyProvider() var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); // Assert - Assert.False(allowed); + Assert.False(allowed.Succeeded); } public class DynamicPolicyProvider : IAuthorizationPolicyProvider { + public Task GetDefaultPolicyAsync() + { + return Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); + } + + public Task GetRequiredPolicyAsync() + { + return Task.FromResult(null); + } + public Task GetPolicyAsync(string policyName) { return Task.FromResult(new AuthorizationPolicyBuilder().RequireClaim(policyName).Build()); @@ -995,10 +1091,88 @@ public async Task CanUseDynamicPolicyProvider() // Act // Assert - Assert.False(await authorizationService.AuthorizeAsync(user, "0")); - Assert.True(await authorizationService.AuthorizeAsync(user, "1")); - Assert.True(await authorizationService.AuthorizeAsync(user, "2")); - Assert.False(await authorizationService.AuthorizeAsync(user, "3")); + Assert.False((await authorizationService.AuthorizeAsync(user, "0")).Succeeded); + Assert.True((await authorizationService.AuthorizeAsync(user, "1")).Succeeded); + Assert.True((await authorizationService.AuthorizeAsync(user, "2")).Succeeded); + Assert.False((await authorizationService.AuthorizeAsync(user, "3")).Succeeded); + } + + public class SuccessEvaluator : IAuthorizationEvaluator + { + public AuthorizationResult Evaluate(AuthorizationHandlerContext context) => AuthorizationResult.Success(); + } + + [Fact] + public async Task CanUseCustomEvaluatorThatOverridesRequirement() + { + var authorizationService = BuildAuthorizationService(services => + { + services.AddSingleton(); + services.AddAuthorization(options => options.AddPolicy("Fail", p => p.RequireAssertion(c => false))); + }); + var result = await authorizationService.AuthorizeAsync(null, "Fail"); + Assert.True(result.Succeeded); + } + + + public class BadContextMaker : IAuthorizationHandlerContextFactory + { + public AuthorizationHandlerContext CreateContext(IEnumerable requirements, ClaimsPrincipal user, object resource) + { + return new BadContext(); + } + } + + public class BadContext : AuthorizationHandlerContext + { + public BadContext() : base(new List(), null, null) { } + + public override bool HasFailed + { + get + { + return true; + } + } + + public override bool HasSucceeded + { + get + { + return false; + } + } + } + + [Fact] + public async Task CanUseCustomContextThatAlwaysFails() + { + var authorizationService = BuildAuthorizationService(services => + { + services.AddSingleton(); + services.AddAuthorization(options => options.AddPolicy("Success", p => p.RequireAssertion(c => true))); + }); + Assert.False((await authorizationService.AuthorizeAsync(null, "Success")).Succeeded); } + + public class SadHandlerProvider : IAuthorizationHandlerProvider + { + public Task> GetHandlersAsync(AuthorizationHandlerContext context) + { + return Task.FromResult>(new IAuthorizationHandler[1] { new FailHandler() }); + } + } + + [Fact] + public async Task CanUseCustomHandlerProvider() + { + var authorizationService = BuildAuthorizationService(services => + { + services.AddSingleton(); + services.AddAuthorization(options => options.AddPolicy("Success", p => p.RequireAssertion(c => true))); + }); + Assert.False((await authorizationService.AuthorizeAsync(null, "Success")).Succeeded); + } + } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj b/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj new file mode 100644 index 000000000..9ff42a070 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.0 + $(NoWarn);NU1605 + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.xproj b/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.xproj deleted file mode 100644 index d596a5bb4..000000000 --- a/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.xproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 7af5ad96-eb6e-4d0e-8abe-c0b543c0f4c2 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - - - - \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authorization.Test/PolicyEvaluatorTests.cs b/test/Microsoft.AspNetCore.Authorization.Test/PolicyEvaluatorTests.cs new file mode 100644 index 000000000..2384e6db5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authorization.Test/PolicyEvaluatorTests.cs @@ -0,0 +1,209 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authorization.Policy.Test +{ + public class PolicyEvaluatorTests + { + [Fact] + public async Task AuthenticateFailsIfNoPrincipalReturned() + { + // Arrange + var evaluator = BuildEvaluator(); + var context = new DefaultHttpContext(); + var services = new ServiceCollection().AddSingleton(); + context.RequestServices = services.BuildServiceProvider(); + var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build(); + + // Act + var result = await evaluator.AuthenticateAsync(policy, context); + + // Assert + Assert.False(result.Succeeded); + } + + [Fact] + public async Task AuthenticateMergeSchemes() + { + // Arrange + var evaluator = BuildEvaluator(); + var context = new DefaultHttpContext(); + var services = new ServiceCollection().AddSingleton(); + context.RequestServices = services.BuildServiceProvider(); + var policy = new AuthorizationPolicyBuilder().AddAuthenticationSchemes("A","B","C").RequireAssertion(_ => true).Build(); + + // Act + var result = await evaluator.AuthenticateAsync(policy, context); + + // Assert + Assert.True(result.Succeeded); + Assert.Equal(3, result.Principal.Identities.Count()); + } + + + [Fact] + public async Task AuthorizeSucceedsEvenIfAuthenticationFails() + { + // Arrange + var evaluator = BuildEvaluator(); + var context = new DefaultHttpContext(); + var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build(); + + // Act + var result = await evaluator.AuthorizeAsync(policy, AuthenticateResult.Fail("Nooo"), context, resource: null); + + // Assert + Assert.True(result.Succeeded); + Assert.False(result.Challenged); + Assert.False(result.Forbidden); + } + + [Fact] + public async Task AuthorizeSucceedsOnlyIfResourceSpecified() + { + // Arrange + var evaluator = BuildEvaluator(); + var context = new DefaultHttpContext(); + var policy = new AuthorizationPolicyBuilder().RequireAssertion(c => c.Resource != null).Build(); + var success = AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "whatever")); + + // Act + var result = await evaluator.AuthorizeAsync(policy, success, context, resource: null); + var result2 = await evaluator.AuthorizeAsync(policy, success, context, resource: new object()); + + // Assert + Assert.False(result.Succeeded); + Assert.True(result2.Succeeded); + } + + [Fact] + public async Task AuthorizeChallengesIfAuthenticationFails() + { + // Arrange + var evaluator = BuildEvaluator(); + var context = new DefaultHttpContext(); + var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => false).Build(); + + // Act + var result = await evaluator.AuthorizeAsync(policy, AuthenticateResult.Fail("Nooo"), context, resource: null); + + // Assert + Assert.False(result.Succeeded); + Assert.True(result.Challenged); + Assert.False(result.Forbidden); + } + + [Fact] + public async Task AuthorizeForbidsIfAuthenticationSuceeds() + { + // Arrange + var evaluator = BuildEvaluator(); + var context = new DefaultHttpContext(); + var policy = new AuthorizationPolicyBuilder().RequireAssertion(_ => false).Build(); + + // Act + var result = await evaluator.AuthorizeAsync(policy, AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "scheme")), context, resource: null); + + // Assert + Assert.False(result.Succeeded); + Assert.False(result.Challenged); + Assert.True(result.Forbidden); + } + + private IPolicyEvaluator BuildEvaluator(Action setupServices = null) + { + var services = new ServiceCollection() + .AddAuthorization() + .AddAuthorizationPolicyEvaluator() + .AddLogging() + .AddOptions(); + setupServices?.Invoke(services); + return services.BuildServiceProvider().GetRequiredService(); + } + + public class HappyAuthorization : IAuthorizationService + { + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements) + => Task.FromResult(AuthorizationResult.Success()); + + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) + => Task.FromResult(AuthorizationResult.Success()); + } + + public class SadAuthorization : IAuthorizationService + { + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements) + => Task.FromResult(AuthorizationResult.Failed()); + + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) + => Task.FromResult(AuthorizationResult.Failed()); + } + + public class SadAuthentication : IAuthenticationService + { + public Task AuthenticateAsync(HttpContext context, string scheme) + { + return Task.FromResult(AuthenticateResult.Fail("Sad.")); + } + + public Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + + public Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + + public Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + + public Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + } + + public class EchoAuthentication : IAuthenticationService + { + public Task AuthenticateAsync(HttpContext context, string scheme) + { + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(scheme)), scheme))); + } + + public Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + + public Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + + public Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + + public Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + } + + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authorization.Test/TestObjects/TestAuthenticationService.cs b/test/Microsoft.AspNetCore.Authorization.Test/TestObjects/TestAuthenticationService.cs new file mode 100644 index 000000000..0c17693d1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authorization.Test/TestObjects/TestAuthenticationService.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authorization.Test.TestObjects +{ + public class TestAuthenticationService : IAuthenticationService + { + public bool ChallengeCalled => ChallengeCount > 0; + public bool ForbidCalled => ForbidCount > 0; + public bool AuthenticateCalled => AuthenticateCount > 0; + + public int ChallengeCount { get; private set; } + public int ForbidCount { get; private set; } + public int AuthenticateCount { get; private set; } + + public Task AuthenticateAsync(HttpContext context, string scheme) + { + AuthenticateCount++; + + var identity = context.User.Identities.SingleOrDefault(i => i.AuthenticationType == scheme); + if (identity != null) + { + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(identity), scheme))); + } + + return Task.FromResult(AuthenticateResult.Fail("Denied")); + } + + public Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + ChallengeCount++; + return Task.CompletedTask; + } + + public Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + ForbidCount++; + return Task.CompletedTask; + } + + public Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authorization.Test/project.json b/test/Microsoft.AspNetCore.Authorization.Test/project.json deleted file mode 100644 index a16a0ba37..000000000 --- a/test/Microsoft.AspNetCore.Authorization.Test/project.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "compilationOptions": { - "warningsAsErrors": true - }, - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1-*", - "Microsoft.AspNetCore.Authorization": "1.0.0-*", - "Microsoft.AspNetCore.Testing": "1.0.0-*", - "Microsoft.Extensions.DependencyInjection": "1.0.0-*", - "Microsoft.Extensions.Logging": "1.0.0-*", - "xunit": "2.1.0" - }, - "frameworks": { - "netcoreapp1.0": { - "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.0.0-*", - "type": "platform" - }, - "dotnet-test-xunit": "1.0.0-*", - "System.Diagnostics.Process": "4.1.0-*" - }, - "imports": [ - "dnxcore50", - "portable-net451+win8" - ] - }, - "net451": { - "frameworkAssemblies": { - "System.Runtime": "", - "System.Threading.Tasks": "" - }, - "dependencies": { - "xunit.runner.console": "2.1.0" - } - } - }, - "testRunner": "xunit" -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/Cookies/Infrastructure/CookieChunkingTests.cs b/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/CookieChunkingTests.cs similarity index 51% rename from test/Microsoft.AspNetCore.Authentication.Test/Cookies/Infrastructure/CookieChunkingTests.cs rename to test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/CookieChunkingTests.cs index 4b92f08c8..e645745b3 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/Cookies/Infrastructure/CookieChunkingTests.cs +++ b/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/CookieChunkingTests.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Http; using Xunit; -namespace Microsoft.AspNetCore.Authentication.Cookies.Infrastructure +namespace Microsoft.AspNetCore.Internal { public class CookieChunkingTests { @@ -15,10 +15,10 @@ public void AppendLargeCookie_Appended() HttpContext context = new DefaultHttpContext(); string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - new ChunkingCookieManager(null) { ChunkSize = null }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions()); + new ChunkingCookieManager() { ChunkSize = null }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions()); var values = context.Response.Headers["Set-Cookie"]; - Assert.Equal(1, values.Count); - Assert.Equal("TestCookie=" + testString + "; path=/", values[0]); + Assert.Single(values); + Assert.Equal("TestCookie=" + testString + "; path=/; samesite=lax", values[0]); } [Fact] @@ -27,43 +27,20 @@ public void AppendLargeCookieWithLimit_Chunked() HttpContext context = new DefaultHttpContext(); string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - new ChunkingCookieManager(null) { ChunkSize = 30 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions()); + new ChunkingCookieManager() { ChunkSize = 44 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions()); var values = context.Response.Headers["Set-Cookie"]; Assert.Equal(9, values.Count); Assert.Equal(new[] { - "TestCookie=chunks:8; path=/", - "TestCookieC1=abcdefgh; path=/", - "TestCookieC2=ijklmnop; path=/", - "TestCookieC3=qrstuvwx; path=/", - "TestCookieC4=yz012345; path=/", - "TestCookieC5=6789ABCD; path=/", - "TestCookieC6=EFGHIJKL; path=/", - "TestCookieC7=MNOPQRST; path=/", - "TestCookieC8=UVWXYZ; path=/", - }, values); - } - - [Fact] - public void AppendLargeQuotedCookieWithLimit_QuotedChunked() - { - HttpContext context = new DefaultHttpContext(); - - string testString = "\"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\""; - new ChunkingCookieManager(null) { ChunkSize = 32 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions()); - var values = context.Response.Headers["Set-Cookie"]; - Assert.Equal(9, values.Count); - Assert.Equal(new[] - { - "TestCookie=chunks:8; path=/", - "TestCookieC1=\"abcdefgh\"; path=/", - "TestCookieC2=\"ijklmnop\"; path=/", - "TestCookieC3=\"qrstuvwx\"; path=/", - "TestCookieC4=\"yz012345\"; path=/", - "TestCookieC5=\"6789ABCD\"; path=/", - "TestCookieC6=\"EFGHIJKL\"; path=/", - "TestCookieC7=\"MNOPQRST\"; path=/", - "TestCookieC8=\"UVWXYZ\"; path=/", + "TestCookie=chunks-8; path=/; samesite=lax", + "TestCookieC1=abcdefgh; path=/; samesite=lax", + "TestCookieC2=ijklmnop; path=/; samesite=lax", + "TestCookieC3=qrstuvwx; path=/; samesite=lax", + "TestCookieC4=yz012345; path=/; samesite=lax", + "TestCookieC5=6789ABCD; path=/; samesite=lax", + "TestCookieC6=EFGHIJKL; path=/; samesite=lax", + "TestCookieC7=MNOPQRST; path=/; samesite=lax", + "TestCookieC8=UVWXYZ; path=/; samesite=lax", }, values); } @@ -73,7 +50,7 @@ public void GetLargeChunkedCookie_Reassembled() HttpContext context = new DefaultHttpContext(); context.Request.Headers["Cookie"] = new[] { - "TestCookie=chunks:7", + "TestCookie=chunks-7", "TestCookieC1=abcdefghi", "TestCookieC2=jklmnopqr", "TestCookieC3=stuvwxyz0", @@ -83,39 +60,18 @@ public void GetLargeChunkedCookie_Reassembled() "TestCookieC7=STUVWXYZ" }; - string result = new ChunkingCookieManager(null).GetRequestCookie(context, "TestCookie"); + string result = new ChunkingCookieManager().GetRequestCookie(context, "TestCookie"); string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; Assert.Equal(testString, result); } - [Fact] - public void GetLargeChunkedCookieWithQuotes_Reassembled() - { - HttpContext context = new DefaultHttpContext(); - context.Request.Headers["Cookie"] = new[] - { - "TestCookie=chunks:7", - "TestCookieC1=\"abcdefghi\"", - "TestCookieC2=\"jklmnopqr\"", - "TestCookieC3=\"stuvwxyz0\"", - "TestCookieC4=\"123456789\"", - "TestCookieC5=\"ABCDEFGHI\"", - "TestCookieC6=\"JKLMNOPQR\"", - "TestCookieC7=\"STUVWXYZ\"" - }; - - string result = new ChunkingCookieManager(null).GetRequestCookie(context, "TestCookie"); - string testString = "\"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\""; - Assert.Equal(testString, result); - } - [Fact] public void GetLargeChunkedCookieWithMissingChunk_ThrowingEnabled_Throws() { HttpContext context = new DefaultHttpContext(); context.Request.Headers["Cookie"] = new[] { - "TestCookie=chunks:7", + "TestCookie=chunks-7", "TestCookieC1=abcdefghi", // Missing chunk "TestCookieC2=jklmnopqr", "TestCookieC3=stuvwxyz0", @@ -125,7 +81,8 @@ public void GetLargeChunkedCookieWithMissingChunk_ThrowingEnabled_Throws() "TestCookieC7=STUVWXYZ" }; - Assert.Throws(() => new ChunkingCookieManager(null).GetRequestCookie(context, "TestCookie")); + Assert.Throws(() => new ChunkingCookieManager() { ThrowForPartialCookies = true } + .GetRequestCookie(context, "TestCookie")); } [Fact] @@ -134,7 +91,7 @@ public void GetLargeChunkedCookieWithMissingChunk_ThrowingDisabled_NotReassemble HttpContext context = new DefaultHttpContext(); context.Request.Headers["Cookie"] = new[] { - "TestCookie=chunks:7", + "TestCookie=chunks-7", "TestCookieC1=abcdefghi", // Missing chunk "TestCookieC2=jklmnopqr", "TestCookieC3=stuvwxyz0", @@ -144,8 +101,8 @@ public void GetLargeChunkedCookieWithMissingChunk_ThrowingDisabled_NotReassemble "TestCookieC7=STUVWXYZ" }; - string result = new ChunkingCookieManager(null) { ThrowForPartialCookies = false }.GetRequestCookie(context, "TestCookie"); - string testString = "chunks:7"; + string result = new ChunkingCookieManager() { ThrowForPartialCookies = false }.GetRequestCookie(context, "TestCookie"); + string testString = "chunks-7"; Assert.Equal(testString, result); } @@ -153,21 +110,21 @@ public void GetLargeChunkedCookieWithMissingChunk_ThrowingDisabled_NotReassemble public void DeleteChunkedCookieWithOptions_AllDeleted() { HttpContext context = new DefaultHttpContext(); - context.Request.Headers.Append("Cookie", "TestCookie=chunks:7"); + context.Request.Headers.Append("Cookie", "TestCookie=chunks-7"); - new ChunkingCookieManager(null).DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com" }); + new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com" }); var cookies = context.Response.Headers["Set-Cookie"]; Assert.Equal(8, cookies.Count); Assert.Equal(new[] { - "TestCookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", - "TestCookieC1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", - "TestCookieC2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", - "TestCookieC3=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", - "TestCookieC4=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", - "TestCookieC5=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", - "TestCookieC6=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", - "TestCookieC7=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", + "TestCookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC3=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC4=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC5=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC6=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", + "TestCookieC7=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax", }, cookies); } } diff --git a/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj b/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj new file mode 100644 index 000000000..2f38abff9 --- /dev/null +++ b/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.0 + + + + + + + + + + + diff --git a/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs b/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs new file mode 100644 index 000000000..fffb8cc88 --- /dev/null +++ b/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs @@ -0,0 +1,660 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.CookiePolicy.Test +{ + public class CookieConsentTests + { + [Fact] + public async Task ConsentChecksOffByDefault() + { + var httpContext = await RunTestAsync(options => { }, requestContext => { }, context => + { + var feature = context.Features.Get(); + Assert.False(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.True(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task ConsentEnabledForTemplateScenario() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task NonEssentialCookiesWithOptionsExcluded() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = false }); + return Task.CompletedTask; + }); + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task NonEssentialCookiesCanBeAllowedViaOnAppendCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + options.OnAppendCookie = context => + { + Assert.True(context.IsConsentNeeded); + Assert.False(context.HasConsent); + Assert.False(context.IssueCookie); + context.IssueCookie = true; + }; + }, + requestContext => { }, context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = false }); + return Task.CompletedTask; + }); + Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task NeedsConsentDoesNotPreventEssentialCookies() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = true }); + return Task.CompletedTask; + }); + Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task EssentialCookiesCanBeExcludedByOnAppendCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + options.OnAppendCookie = context => + { + Assert.True(context.IsConsentNeeded); + Assert.True(context.HasConsent); + Assert.True(context.IssueCookie); + context.IssueCookie = false; + }; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes"; + }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = true }); + return Task.CompletedTask; + }); + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task HasConsentReadsRequestCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes"; + }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task HasConsentIgnoresInvalidRequestCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=IAmATeapot"; + }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task GrantConsentSetsCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + feature.GrantConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(2, cookies.Count); + var consentCookie = cookies[0]; + Assert.Equal(".AspNet.Consent", consentCookie.Name); + Assert.Equal("yes", consentCookie.Value); + Assert.True(consentCookie.Expires.HasValue); + Assert.True(consentCookie.Expires.Value > DateTimeOffset.Now + TimeSpan.FromDays(364)); + Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + var testCookie = cookies[1]; + Assert.Equal("Test", testCookie.Name); + Assert.Equal("Value", testCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite); + Assert.Null(testCookie.Expires); + } + + [Fact] + public async Task GrantConsentAppliesPolicyToConsentCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = Http.SameSiteMode.Strict; + options.OnAppendCookie = context => + { + Assert.Equal(".AspNet.Consent", context.CookieName); + Assert.Equal("yes", context.CookieValue); + Assert.Equal(Http.SameSiteMode.Strict, context.CookieOptions.SameSite); + context.CookieName += "1"; + context.CookieValue += "1"; + }; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + feature.GrantConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(1, cookies.Count); + var consentCookie = cookies[0]; + Assert.Equal(".AspNet.Consent1", consentCookie.Name); + Assert.Equal("yes1", consentCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + } + + [Fact] + public async Task GrantConsentWhenAlreadyHasItDoesNotSetCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes"; + }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + feature.GrantConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + + Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task GrantConsentAfterResponseStartsSetsHasConsentButDoesNotSetCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, + async context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + await context.Response.WriteAsync("Started."); + + feature.GrantConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + Assert.Throws(() => context.Response.Cookies.Append("Test", "Value")); + + await context.Response.WriteAsync("Granted."); + }); + + var reader = new StreamReader(httpContext.Response.Body); + Assert.Equal("Started.Granted.", await reader.ReadToEndAsync()); + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task WithdrawConsentWhenNotHasConsentNoOps() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + feature.WithdrawConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + context.Response.Cookies.Append("Test", "Value"); + return Task.CompletedTask; + }); + + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task WithdrawConsentDeletesCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes"; + }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value1"); + + feature.WithdrawConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + context.Response.Cookies.Append("Test", "Value2"); + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(2, cookies.Count); + var testCookie = cookies[0]; + Assert.Equal("Test", testCookie.Name); + Assert.Equal("Value1", testCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite); + Assert.Null(testCookie.Expires); + var consentCookie = cookies[1]; + Assert.Equal(".AspNet.Consent", consentCookie.Name); + Assert.Equal("", consentCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + } + + [Fact] + public async Task WithdrawConsentAppliesPolicyToDeleteCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = Http.SameSiteMode.Strict; + options.OnDeleteCookie = context => + { + Assert.Equal(".AspNet.Consent", context.CookieName); + context.CookieName += "1"; + }; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes"; + }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + feature.WithdrawConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(1, cookies.Count); + var consentCookie = cookies[0]; + Assert.Equal(".AspNet.Consent1", consentCookie.Name); + Assert.Equal("", consentCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + } + + [Fact] + public async Task WithdrawConsentAfterResponseHasStartedDoesNotDeleteCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => + { + requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes"; + }, + async context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + context.Response.Cookies.Append("Test", "Value1"); + + await context.Response.WriteAsync("Started."); + + feature.WithdrawConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + // Doesn't throw the normal InvalidOperationException because the cookie is never written + context.Response.Cookies.Append("Test", "Value2"); + + await context.Response.WriteAsync("Withdrawn."); + }); + + var reader = new StreamReader(httpContext.Response.Body); + Assert.Equal("Started.Withdrawn.", await reader.ReadToEndAsync()); + Assert.Equal("Test=Value1; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task DeleteCookieDoesNotRequireConsent() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Delete("Test"); + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(1, cookies.Count); + var testCookie = cookies[0]; + Assert.Equal("Test", testCookie.Name); + Assert.Equal("", testCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite); + Assert.NotNull(testCookie.Expires); + } + + [Fact] + public async Task OnDeleteCookieCanSuppressCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + options.OnDeleteCookie = context => + { + Assert.True(context.IsConsentNeeded); + Assert.False(context.HasConsent); + Assert.True(context.IssueCookie); + context.IssueCookie = false; + }; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + context.Response.Cookies.Delete("Test"); + return Task.CompletedTask; + }); + + Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); + } + + [Fact] + public async Task CreateConsentCookieMatchesGrantConsentCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + feature.GrantConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + var cookie = feature.CreateConsentCookie(); + context.Response.Headers["ManualCookie"] = cookie; + + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(1, cookies.Count); + var consentCookie = cookies[0]; + Assert.Equal(".AspNet.Consent", consentCookie.Name); + Assert.Equal("yes", consentCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + + cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers["ManualCookie"]); + Assert.Equal(1, cookies.Count); + var manualCookie = cookies[0]; + Assert.Equal(consentCookie.Name, manualCookie.Name); + Assert.Equal(consentCookie.Value, manualCookie.Value); + Assert.Equal(consentCookie.SameSite, manualCookie.SameSite); + Assert.NotNull(manualCookie.Expires); // Expires may not exactly match to the second. + } + + [Fact] + public async Task CreateConsentCookieAppliesPolicy() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = Http.SameSiteMode.Strict; + options.OnAppendCookie = context => + { + Assert.Equal(".AspNet.Consent", context.CookieName); + Assert.Equal("yes", context.CookieValue); + Assert.Equal(Http.SameSiteMode.Strict, context.CookieOptions.SameSite); + context.CookieName += "1"; + context.CookieValue += "1"; + }; + }, + requestContext => { }, + context => + { + var feature = context.Features.Get(); + Assert.True(feature.IsConsentNeeded); + Assert.False(feature.HasConsent); + Assert.False(feature.CanTrack); + + feature.GrantConsent(); + + Assert.True(feature.IsConsentNeeded); + Assert.True(feature.HasConsent); + Assert.True(feature.CanTrack); + + var cookie = feature.CreateConsentCookie(); + context.Response.Headers["ManualCookie"] = cookie; + + return Task.CompletedTask; + }); + + var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]); + Assert.Equal(1, cookies.Count); + var consentCookie = cookies[0]; + Assert.Equal(".AspNet.Consent1", consentCookie.Name); + Assert.Equal("yes1", consentCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + + cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers["ManualCookie"]); + Assert.Equal(1, cookies.Count); + var manualCookie = cookies[0]; + Assert.Equal(consentCookie.Name, manualCookie.Name); + Assert.Equal(consentCookie.Value, manualCookie.Value); + Assert.Equal(consentCookie.SameSite, manualCookie.SameSite); + Assert.NotNull(manualCookie.Expires); // Expires may not exactly match to the second. + } + + private Task RunTestAsync(Action configureOptions, Action configureRequest, RequestDelegate handleRequest) + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.Configure(configureOptions); + }) + .Configure(app => + { + app.UseCookiePolicy(); + app.Run(handleRequest); + }); + var server = new TestServer(builder); + return server.SendAsync(configureRequest); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs b/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs index 307002d1f..a2592e557 100644 --- a/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs +++ b/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs @@ -2,12 +2,18 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Security.Claims; +using System.Security.Principal; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; using Xunit; namespace Microsoft.AspNetCore.CookiePolicy.Test @@ -30,6 +36,15 @@ public class CookiePolicyTests context.Response.Cookies.Append("D", "D", new CookieOptions { HttpOnly = true }); return Task.FromResult(0); }; + private RequestDelegate SameSiteCookieAppends = context => + { + context.Response.Cookies.Append("A", "A"); + context.Response.Cookies.Append("B", "B", new CookieOptions { SameSite = Http.SameSiteMode.None }); + context.Response.Cookies.Append("C", "C", new CookieOptions()); + context.Response.Cookies.Append("D", "D", new CookieOptions { SameSite = Http.SameSiteMode.Lax }); + context.Response.Cookies.Append("E", "E", new CookieOptions { SameSite = Http.SameSiteMode.Strict }); + return Task.FromResult(0); + }; [Fact] public async Task SecureAlwaysSetsSecure() @@ -37,18 +52,18 @@ public async Task SecureAlwaysSetsSecure() await RunTest("/secureAlways", new CookiePolicyOptions { - Secure = SecurePolicy.Always + Secure = CookieSecurePolicy.Always }, SecureCookieAppends, new RequestTest("http://example.com/secureAlways", - transaction => - { - Assert.NotNull(transaction.SetCookie); - Assert.Equal("A=A; path=/; secure", transaction.SetCookie[0]); - Assert.Equal("B=B; path=/; secure", transaction.SetCookie[1]); - Assert.Equal("C=C; path=/; secure", transaction.SetCookie[2]); - Assert.Equal("D=D; path=/; secure", transaction.SetCookie[3]); - })); + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; secure; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; secure; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; secure; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]); + })); } [Fact] @@ -57,19 +72,18 @@ public async Task SecureNoneLeavesSecureUnchanged() await RunTest("/secureNone", new CookiePolicyOptions { - Secure = SecurePolicy.None + Secure = CookieSecurePolicy.None }, SecureCookieAppends, new RequestTest("http://example.com/secureNone", - transaction => - { - Assert.NotNull(transaction.SetCookie); - Assert.NotNull(transaction.SetCookie); - Assert.Equal("A=A; path=/", transaction.SetCookie[0]); - Assert.Equal("B=B; path=/", transaction.SetCookie[1]); - Assert.Equal("C=C; path=/", transaction.SetCookie[2]); - Assert.Equal("D=D; path=/; secure", transaction.SetCookie[3]); - })); + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]); + })); } [Fact] @@ -78,27 +92,27 @@ public async Task SecureSameUsesRequest() await RunTest("/secureSame", new CookiePolicyOptions { - Secure = SecurePolicy.SameAsRequest + Secure = CookieSecurePolicy.SameAsRequest }, SecureCookieAppends, new RequestTest("http://example.com/secureSame", - transaction => - { - Assert.NotNull(transaction.SetCookie); - Assert.Equal("A=A; path=/", transaction.SetCookie[0]); - Assert.Equal("B=B; path=/", transaction.SetCookie[1]); - Assert.Equal("C=C; path=/", transaction.SetCookie[2]); - Assert.Equal("D=D; path=/", transaction.SetCookie[3]); - }), + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]); + }), new RequestTest("https://example.com/secureSame", - transaction => - { - Assert.NotNull(transaction.SetCookie); - Assert.Equal("A=A; path=/; secure", transaction.SetCookie[0]); - Assert.Equal("B=B; path=/; secure", transaction.SetCookie[1]); - Assert.Equal("C=C; path=/; secure", transaction.SetCookie[2]); - Assert.Equal("D=D; path=/; secure", transaction.SetCookie[3]); - })); + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; secure; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; secure; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; secure; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]); + })); } [Fact] @@ -114,10 +128,10 @@ await RunTest("/httpOnlyAlways", transaction => { Assert.NotNull(transaction.SetCookie); - Assert.Equal("A=A; path=/; httponly", transaction.SetCookie[0]); - Assert.Equal("B=B; path=/; httponly", transaction.SetCookie[1]); - Assert.Equal("C=C; path=/; httponly", transaction.SetCookie[2]); - Assert.Equal("D=D; path=/; httponly", transaction.SetCookie[3]); + Assert.Equal("A=A; path=/; samesite=lax; httponly", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; samesite=lax; httponly", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; samesite=lax; httponly", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; samesite=lax; httponly", transaction.SetCookie[3]); })); } @@ -132,12 +146,75 @@ await RunTest("/httpOnlyNone", HttpCookieAppends, new RequestTest("http://example.com/httpOnlyNone", transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; samesite=lax; httponly", transaction.SetCookie[3]); + })); + } + + [Fact] + public async Task SameSiteStrictSetsItAlways() + { + await RunTest("/sameSiteStrict", + new CookiePolicyOptions + { + MinimumSameSitePolicy = Http.SameSiteMode.Strict + }, + SameSiteCookieAppends, + new RequestTest("http://example.com/sameSiteStrict", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; samesite=strict", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; samesite=strict", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; samesite=strict", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; samesite=strict", transaction.SetCookie[3]); + Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]); + })); + } + + [Fact] + public async Task SameSiteLaxSetsItAlways() + { + await RunTest("/sameSiteLax", + new CookiePolicyOptions + { + MinimumSameSitePolicy = Http.SameSiteMode.Lax + }, + SameSiteCookieAppends, + new RequestTest("http://example.com/sameSiteLax", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]); + Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]); + })); + } + + [Fact] + public async Task SameSiteNoneLeavesItAlone() + { + await RunTest("/sameSiteNone", + new CookiePolicyOptions + { + MinimumSameSitePolicy = Http.SameSiteMode.None + }, + SameSiteCookieAppends, + new RequestTest("http://example.com/sameSiteNone", + transaction => { Assert.NotNull(transaction.SetCookie); Assert.Equal("A=A; path=/", transaction.SetCookie[0]); Assert.Equal("B=B; path=/", transaction.SetCookie[1]); - Assert.Equal("C=C; path=/", transaction.SetCookie[2]); - Assert.Equal("D=D; path=/; httponly", transaction.SetCookie[3]); + Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]); + Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]); })); } @@ -165,10 +242,10 @@ public async Task CookiePolicyCanHijackAppend() var transaction = await server.SendAsync("http://example.com/login"); Assert.NotNull(transaction.SetCookie); - Assert.Equal("Hao=Hao; path=/", transaction.SetCookie[0]); - Assert.Equal("Hao=Hao; path=/", transaction.SetCookie[1]); - Assert.Equal("Hao=Hao; path=/", transaction.SetCookie[2]); - Assert.Equal("Hao=Hao; path=/; secure", transaction.SetCookie[3]); + Assert.Equal("Hao=Hao; path=/; samesite=lax", transaction.SetCookie[0]); + Assert.Equal("Hao=Hao; path=/; samesite=lax", transaction.SetCookie[1]); + Assert.Equal("Hao=Hao; path=/; samesite=lax", transaction.SetCookie[2]); + Assert.Equal("Hao=Hao; path=/; secure; samesite=lax", transaction.SetCookie[3]); } [Fact] @@ -196,7 +273,7 @@ public async Task CookiePolicyCanHijackDelete() Assert.NotNull(transaction.SetCookie); Assert.Equal(1, transaction.SetCookie.Count); - Assert.Equal("A=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/", transaction.SetCookie[0]); + Assert.Equal("A=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure; samesite=lax", transaction.SetCookie[0]); } [Fact] @@ -229,6 +306,100 @@ public async Task CookiePolicyCallsCookieFeature() Assert.Equal("Done", transaction.ResponseText); } + [Fact] + public async Task CookiePolicyAppliesToCookieAuth() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddAuthentication().AddCookie(o => + { + o.Cookie.Name = "TestCookie"; + o.Cookie.HttpOnly = false; + o.Cookie.SecurePolicy = CookieSecurePolicy.None; + }); + }) + .Configure(app => + { + app.UseCookiePolicy(new CookiePolicyOptions + { + HttpOnly = HttpOnlyPolicy.Always, + Secure = CookieSecurePolicy.Always, + }); + app.UseAuthentication(); + app.Run(context => + { + return context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("TestUser", "Cookies")))); + }); + }); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com/login"); + + Assert.NotNull(transaction.SetCookie); + Assert.Equal(1, transaction.SetCookie.Count); + var cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[0]); + Assert.Equal("TestCookie", cookie.Name); + Assert.True(cookie.HttpOnly); + Assert.True(cookie.Secure); + Assert.Equal("/", cookie.Path); + } + + [Fact] + public async Task CookiePolicyAppliesToCookieAuthChunks() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddAuthentication().AddCookie(o => + { + o.Cookie.Name = "TestCookie"; + o.Cookie.HttpOnly = false; + o.Cookie.SecurePolicy = CookieSecurePolicy.None; + }); + }) + .Configure(app => + { + app.UseCookiePolicy(new CookiePolicyOptions + { + HttpOnly = HttpOnlyPolicy.Always, + Secure = CookieSecurePolicy.Always, + }); + app.UseAuthentication(); + app.Run(context => + { + return context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity(new string('c', 1024 * 5), "Cookies")))); + }); + }); + var server = new TestServer(builder); + + var transaction = await server.SendAsync("http://example.com/login"); + + Assert.NotNull(transaction.SetCookie); + Assert.Equal(3, transaction.SetCookie.Count); + + var cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[0]); + Assert.Equal("TestCookie", cookie.Name); + Assert.Equal("chunks-2", cookie.Value); + Assert.True(cookie.HttpOnly); + Assert.True(cookie.Secure); + Assert.Equal("/", cookie.Path); + + cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[1]); + Assert.Equal("TestCookieC1", cookie.Name); + Assert.True(cookie.HttpOnly); + Assert.True(cookie.Secure); + Assert.Equal("/", cookie.Path); + + cookie = SetCookieHeaderValue.Parse(transaction.SetCookie[2]); + Assert.Equal("TestCookieC2", cookie.Name); + Assert.True(cookie.HttpOnly); + Assert.True(cookie.Secure); + Assert.Equal("/", cookie.Path); + } + private class TestCookieFeature : IResponseCookiesFeature { public IResponseCookies Cookies { get; } = new BadCookies(); @@ -283,13 +454,13 @@ private async Task RunTest( { var builder = new WebHostBuilder() .Configure(app => - { - app.Map(path, map => { - map.UseCookiePolicy(cookiePolicy); - map.Run(configureSetup); + app.Map(path, map => + { + map.UseCookiePolicy(cookiePolicy); + map.Run(configureSetup); + }); }); - }); var server = new TestServer(builder); foreach (var test in tests) { diff --git a/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj b/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj new file mode 100644 index 000000000..3dc3df2e2 --- /dev/null +++ b/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp3.0 + + + + + + + + + + + + + diff --git a/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.xproj b/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.xproj deleted file mode 100644 index cf42659a0..000000000 --- a/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.xproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 1790e052-646f-4529-b90e-6fea95520d69 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - - - - \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.CookiePolicy.Test/project.json b/test/Microsoft.AspNetCore.CookiePolicy.Test/project.json deleted file mode 100644 index bb388af16..000000000 --- a/test/Microsoft.AspNetCore.CookiePolicy.Test/project.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "compilationOptions": { - "warningsAsErrors": true - }, - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1-*", - "Microsoft.AspNetCore.CookiePolicy": "1.0.0-*", - "Microsoft.AspNetCore.TestHost": "1.0.0-*", - "Microsoft.Extensions.DependencyInjection": "1.0.0-*", - "xunit": "2.1.0" - }, - "frameworks": { - "netcoreapp1.0": { - "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.0.0-*", - "type": "platform" - }, - "dotnet-test-xunit": "1.0.0-*", - "System.Diagnostics.Process": "4.1.0-*" - }, - "imports": [ - "dnxcore50", - "portable-net451+win8" - ] - }, - "net451": { - "frameworkAssemblies": { - "System.Runtime": "", - "System.Threading.Tasks": "", - "System.Xml.Linq": "" - }, - "dependencies": { - "xunit.runner.console": "2.1.0" - } - } - }, - "testRunner": "xunit" -} \ No newline at end of file diff --git a/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs b/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs index 2abb115e4..24bf09722 100644 --- a/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs +++ b/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs @@ -1,6 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +/* See https://github.com/aspnet/AspNetCore/issues/4074. + +This test is was disabled as a part of changing frameworks. This test will need to be re-written using separate .NET Core and .NET Framework processes. + +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; @@ -12,8 +17,11 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Testing; using Owin; @@ -32,7 +40,7 @@ public async Task AspNetCoreWithInteropCookieContainsIdentity() var dataProtection = DataProtectionProvider.Create(new DirectoryInfo("..\\..\\artifacts")); var dataProtector = dataProtection.CreateProtector( "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", // full name of the ASP.NET Core type - CookieAuthenticationDefaults.AuthenticationType, "v2"); + Cookies.CookieAuthenticationDefaults.AuthenticationType, "v2"); var interopServer = TestServer.Create(app => { @@ -57,26 +65,81 @@ public async Task AspNetCoreWithInteropCookieContainsIdentity() var builder = new WebHostBuilder() .Configure(app => { - app.UseCookieAuthentication(new AspNetCore.Builder.CookieAuthenticationOptions - { - DataProtectionProvider = dataProtection - }); + app.UseAuthentication(); app.Run(async context => { - var result = await context.Authentication.AuthenticateAsync("Cookies"); - await context.Response.WriteAsync(result.Identity.Name); + var result = await context.AuthenticateAsync("Cookies"); + await context.Response.WriteAsync(result.Ticket.Principal.Identity.Name); }); }) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.DataProtectionProvider = dataProtection)); var newServer = new AspNetCore.TestHost.TestServer(builder); var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/login"); - request.Headers.Add("Cookie", transaction.SetCookie.Split(new[] { ';' }, 2).First()); + foreach (var cookie in SetCookieHeaderValue.ParseList(transaction.SetCookie)) + { + request.Headers.Add("Cookie", cookie.Name + "=" + cookie.Value); + } var response = await newServer.CreateClient().SendAsync(request); Assert.Equal("Alice", await response.Content.ReadAsStringAsync()); } + [Fact] + public async Task AspNetCoreWithLargeInteropCookieContainsIdentity() + { + var identity = new ClaimsIdentity("Cookies"); + identity.AddClaim(new Claim(ClaimTypes.Name, new string('a', 1024 * 5))); + + var dataProtection = DataProtectionProvider.Create(new DirectoryInfo("..\\..\\artifacts")); + var dataProtector = dataProtection.CreateProtector( + "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", // full name of the ASP.NET Core type + Cookies.CookieAuthenticationDefaults.AuthenticationType, "v2"); + + var interopServer = TestServer.Create(app => + { + app.Properties["host.AppName"] = "Microsoft.Owin.Security.Tests"; + + app.UseCookieAuthentication(new Cookies.CookieAuthenticationOptions + { + TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)), + CookieName = AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.CookiePrefix + + AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme, + CookieManager = new ChunkingCookieManager(), + }); + + app.Run(context => + { + context.Authentication.SignIn(identity); + return Task.FromResult(0); + }); + }); + + var transaction = await SendAsync(interopServer, "http://example.com"); + + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Run(async context => + { + var result = await context.AuthenticateAsync("Cookies"); + await context.Response.WriteAsync(result.Ticket.Principal.Identity.Name); + }); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.DataProtectionProvider = dataProtection)); + var newServer = new AspNetCore.TestHost.TestServer(builder); + + var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/login"); + foreach (var cookie in SetCookieHeaderValue.ParseList(transaction.SetCookie)) + { + request.Headers.Add("Cookie", cookie.Name + "=" + cookie.Value); + } + var response = await newServer.CreateClient().SendAsync(request); + + Assert.Equal(1024 * 5, (await response.Content.ReadAsStringAsync()).Length); + } + [Fact] public async Task InteropWithNewCookieContainsIdentity() { @@ -88,27 +151,24 @@ public async Task InteropWithNewCookieContainsIdentity() var dataProtection = DataProtectionProvider.Create(new DirectoryInfo("..\\..\\artifacts")); var dataProtector = dataProtection.CreateProtector( "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", // full name of the ASP.NET Core type - CookieAuthenticationDefaults.AuthenticationType, "v2"); + Cookies.CookieAuthenticationDefaults.AuthenticationType, "v2"); var builder = new WebHostBuilder() .Configure(app => { - app.UseCookieAuthentication(new AspNetCore.Builder.CookieAuthenticationOptions - { - DataProtectionProvider = dataProtection - }); - app.Run(context => context.Authentication.SignInAsync("Cookies", user)); + app.UseAuthentication(); + app.Run(context => context.SignInAsync("Cookies", user)); }) - .ConfigureServices(services => services.AddAuthentication()); + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.DataProtectionProvider = dataProtection)); var newServer = new AspNetCore.TestHost.TestServer(builder); - var cookie = await SendAndGetCookie(newServer, "http://example.com/login"); + var cookies = await SendAndGetCookies(newServer, "http://example.com/login"); var server = TestServer.Create(app => { app.Properties["host.AppName"] = "Microsoft.Owin.Security.Tests"; - app.UseCookieAuthentication(new Owin.Security.Cookies.CookieAuthenticationOptions + app.UseCookieAuthentication(new Cookies.CookieAuthenticationOptions { TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)), CookieName = AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.CookiePrefix @@ -122,18 +182,71 @@ public async Task InteropWithNewCookieContainsIdentity() }); }); - var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", cookie); + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", cookies); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); } - private static async Task SendAndGetCookie(AspNetCore.TestHost.TestServer server, string uri) + [Fact] + public async Task InteropWithLargeNewCookieContainsIdentity() + { + var user = new ClaimsPrincipal(); + var identity = new ClaimsIdentity("scheme"); + identity.AddClaim(new Claim(ClaimTypes.Name, new string('a', 1024 * 5))); + user.AddIdentity(identity); + + var dataProtection = DataProtectionProvider.Create(new DirectoryInfo("..\\..\\artifacts")); + var dataProtector = dataProtection.CreateProtector( + "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", // full name of the ASP.NET Core type + Cookies.CookieAuthenticationDefaults.AuthenticationType, "v2"); + + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Run(context => context.SignInAsync("Cookies", user)); + }) + .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.DataProtectionProvider = dataProtection)); + var newServer = new AspNetCore.TestHost.TestServer(builder); + + var cookies = await SendAndGetCookies(newServer, "http://example.com/login"); + + var server = TestServer.Create(app => + { + app.Properties["host.AppName"] = "Microsoft.Owin.Security.Tests"; + + app.UseCookieAuthentication(new Cookies.CookieAuthenticationOptions + { + TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)), + CookieName = AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.CookiePrefix + + AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme, + CookieManager = new ChunkingCookieManager(), + }); + + app.Run(async context => + { + var result = await context.Authentication.AuthenticateAsync("Cookies"); + Describe(context.Response, result); + }); + }); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", cookies); + + Assert.Equal(1024 * 5, FindClaimValue(transaction2, ClaimTypes.Name).Length); + } + + private static async Task> SendAndGetCookies(AspNetCore.TestHost.TestServer server, string uri) { var request = new HttpRequestMessage(HttpMethod.Get, uri); var response = await server.CreateClient().SendAsync(request); if (response.Headers.Contains("Set-Cookie")) { - return response.Headers.GetValues("Set-Cookie").ToList().First(); + IList cookieHeaders = new List(); + foreach (var cookie in SetCookieHeaderValue.ParseList(response.Headers.GetValues("Set-Cookie").ToList())) + { + cookieHeaders.Add(cookie.Name + "=" + cookie.Value); + } + return cookieHeaders; } return null; } @@ -148,7 +261,7 @@ private static string FindClaimValue(Transaction transaction, string claimType) return claim.Attribute("value").Value; } - private static void Describe(IOwinResponse res, Owin.Security.AuthenticateResult result) + private static void Describe(IOwinResponse res, AuthenticateResult result) { res.StatusCode = 200; res.ContentType = "text/xml"; @@ -171,12 +284,12 @@ private static void Describe(IOwinResponse res, Owin.Security.AuthenticateResult } } - private static async Task SendAsync(TestServer server, string uri, string cookieHeader = null, bool ajaxRequest = false) + private static async Task SendAsync(TestServer server, string uri, IList cookieHeaders = null, bool ajaxRequest = false) { var request = new HttpRequestMessage(HttpMethod.Get, uri); - if (!string.IsNullOrEmpty(cookieHeader)) + if (cookieHeaders != null) { - request.Headers.Add("Cookie", cookieHeader); + request.Headers.Add("Cookie", cookieHeaders); } if (ajaxRequest) { @@ -189,11 +302,11 @@ private static async Task SendAsync(TestServer server, string uri, }; if (transaction.Response.Headers.Contains("Set-Cookie")) { - transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").SingleOrDefault(); + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); } - if (!string.IsNullOrEmpty(transaction.SetCookie)) + if (transaction.SetCookie != null && transaction.SetCookie.Any()) { - transaction.CookieNameValue = transaction.SetCookie.Split(new[] { ';' }, 2).First(); + transaction.CookieNameValue = transaction.SetCookie.First().Split(new[] { ';' }, 2).First(); } transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); @@ -211,7 +324,7 @@ private class Transaction public HttpRequestMessage Request { get; set; } public HttpResponseMessage Response { get; set; } - public string SetCookie { get; set; } + public IList SetCookie { get; set; } public string CookieNameValue { get; set; } public string ResponseText { get; set; } @@ -221,3 +334,4 @@ private class Transaction } } +*/ diff --git a/test/Microsoft.Owin.Security.Interop.Test/Microsoft.Owin.Security.Interop.Test.csproj b/test/Microsoft.Owin.Security.Interop.Test/Microsoft.Owin.Security.Interop.Test.csproj new file mode 100644 index 000000000..57b224ef6 --- /dev/null +++ b/test/Microsoft.Owin.Security.Interop.Test/Microsoft.Owin.Security.Interop.Test.csproj @@ -0,0 +1,19 @@ + + + + net461 + + false + + + + + + + + + + + + + diff --git a/test/Microsoft.Owin.Security.Interop.Test/Microsoft.Owin.Security.Interop.Test.xproj b/test/Microsoft.Owin.Security.Interop.Test/Microsoft.Owin.Security.Interop.Test.xproj deleted file mode 100644 index 96d5b6c5a..000000000 --- a/test/Microsoft.Owin.Security.Interop.Test/Microsoft.Owin.Security.Interop.Test.xproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - a2b5dc39-68d5-4145-a8cc-6aeab7d33a24 - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\ - - - 2.0 - - - - - - \ No newline at end of file diff --git a/test/Microsoft.Owin.Security.Interop.Test/TicketInteropTests.cs b/test/Microsoft.Owin.Security.Interop.Test/TicketInteropTests.cs index 7b2d261bb..ccff1e936 100644 --- a/test/Microsoft.Owin.Security.Interop.Test/TicketInteropTests.cs +++ b/test/Microsoft.Owin.Security.Interop.Test/TicketInteropTests.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +/* + using System; using System.Linq; using System.Security.Claims; @@ -35,7 +37,7 @@ public void NewSerializerCanReadInteropTicket() var newTicket = newSerializer.Deserialize(bytes); Assert.NotNull(newTicket); - Assert.Equal(1, newTicket.Principal.Identities.Count()); + Assert.Single(newTicket.Principal.Identities); var newIdentity = newTicket.Principal.Identity as ClaimsIdentity; Assert.NotNull(newIdentity); Assert.Equal("scheme", newIdentity.AuthenticationType); @@ -58,7 +60,7 @@ public void InteropSerializerCanReadNewTicket() var expires = DateTime.Today; var issued = new DateTime(1979, 11, 11); - var properties = new AspNetCore.Http.Authentication.AuthenticationProperties(); + var properties = new AspNetCore.Authentication.AuthenticationProperties(); properties.IsPersistent = true; properties.RedirectUri = "/redirect"; properties.Items["key"] = "value"; @@ -87,5 +89,6 @@ public void InteropSerializerCanReadNewTicket() } } } +*/ diff --git a/test/Microsoft.Owin.Security.Interop.Test/project.json b/test/Microsoft.Owin.Security.Interop.Test/project.json deleted file mode 100644 index dac3f8185..000000000 --- a/test/Microsoft.Owin.Security.Interop.Test/project.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilationOptions": { - "warningsAsErrors": true - }, - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1-*", - "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-*", - "Microsoft.AspNetCore.TestHost": "1.0.0-*", - "Microsoft.Owin.Security.Cookies": "3.0.1", - "Microsoft.Owin.Security.Interop": "1.0.0-*", - "Microsoft.Owin.Testing": "3.0.1", - "xunit": "2.1.0", - "xunit.runner.console": "2.1.0" - }, - "frameworks": { - "net451": { - "frameworkAssemblies": { - "System.Runtime": "", - "System.Threading.Tasks": "" - } - } - }, - "testRunner": "xunit" -} \ No newline at end of file diff --git a/version.props b/version.props new file mode 100644 index 000000000..71a78cddd --- /dev/null +++ b/version.props @@ -0,0 +1,12 @@ + + + 3.0.0 + alpha1 + $(VersionPrefix) + $(VersionPrefix)-$(VersionSuffix)-final + t000 + a- + $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) + $(VersionSuffix)-$(BuildNumber) + +