diff --git a/.appveyor.yml b/.appveyor.yml index c7b6e3431..d45bd5a1f 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,19 +1,17 @@ init: - - git config --global core.autocrlf true +- git config --global core.autocrlf true branches: only: - - master - - release - - dev - - /^(.*\/)?ci-.*$/ - - /^rel\/.*/ + - master + - /^release\/.*$/ + - /^(.*\/)?ci-.*$/ build_script: - - ps: .\run.ps1 default-build +- 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 +test: 'off' +deploy: 'off' os: Visual Studio 2017 diff --git a/.gitignore b/.gitignore index d5717b3f3..3d7e16e84 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ project.lock.json /.vs/ .vscode/ global.json +BenchmarkDotNet.Artifacts/ diff --git a/.travis.yml b/.travis.yml index 6c59666f3..ab3980055 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,25 +3,25 @@ sudo: false dist: trusty env: global: - - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - - DOTNET_CLI_TELEMETRY_OPTOUT: 1 + - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + - DOTNET_CLI_TELEMETRY_OPTOUT: 1 mono: none os: - - linux - - osx +- linux +- osx osx_image: xcode8.2 addons: apt: packages: - - libunwind8 + - libunwind8 branches: only: - - master - - release - - dev - - /^(.*\/)?ci-.*$/ - - /^rel\/.*/ + - 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 +- 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 +- ./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 index adbda10ef..eb9d9535e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,7 +14,6 @@ $(MSBuildThisFileDirectory) $(MSBuildThisFileDirectory)build\Key.snk true - true true diff --git a/Directory.Build.targets b/Directory.Build.targets index 894b1d0cf..7e3f8df92 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,6 +1,6 @@ - + - $(MicrosoftNETCoreApp20PackageVersion) - $(MicrosoftNETCoreApp21PackageVersion) + $(MicrosoftNETCoreAppPackageVersion) + $(NETStandardLibrary20PackageVersion) diff --git a/LICENSE.txt b/LICENSE.txt index 7b2956ece..b3b180cd5 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,14 +1,201 @@ -Copyright (c) .NET Foundation and Contributors + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -All rights reserved. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -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 + 1. Definitions. - http://www.apache.org/licenses/LICENSE-2.0 + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -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. + "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/README.md b/README.md index e8e64c293..66c2aa45e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ -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. @@ -14,4 +12,4 @@ A list of community projects related to authentication and security for ASP.NET 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 f598f34eb..240100e8c 100644 --- a/Security.sln +++ b/Security.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27004.2002 +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 @@ -75,6 +75,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Author EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookiePolicySample", "samples\CookiePolicySample\CookiePolicySample.csproj", "{24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.WsFederation", "src\Microsoft.AspNetCore.Authentication.WsFederation\Microsoft.AspNetCore.Authentication.WsFederation.csproj", "{B1FC6AAF-9BF2-4CDA-84A2-AA8BF7603F29}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WsFedSample", "samples\WsFedSample\WsFedSample.csproj", "{5EC2E398-E46A-430D-8E4B-E91C8FC3E800}" +EndProject +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 @@ -481,6 +489,54 @@ Global {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 @@ -511,6 +567,9 @@ Global {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} 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/dependencies.props b/build/dependencies.props index bf30832e1..163a3d400 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,45 +3,54 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview1-15651 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 2.1.0-preview1-28009 - 3.14.2 - 5.2.0-preview2-41113220915 - 2.0.0 - 2.1.0-preview1-26102-01 - 15.3.0 + 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 - 10.0.1 - 0.8.0 + 4.10.0 + 2.0.3 + 11.0.2 + 5.3.0 + 0.10.0 2.3.1 - 2.3.1 + 2.4.0 + + diff --git a/build/repo.props b/build/repo.props index 541470c9f..4402da1d0 100644 --- a/build/repo.props +++ b/build/repo.props @@ -1,9 +1,6 @@ - + - - - Internal.AspNetCore.Universe.Lineup @@ -11,7 +8,6 @@ - - + diff --git a/build/sources.props b/build/sources.props index 9feff29d0..9215df975 100644 --- a/build/sources.props +++ b/build/sources.props @@ -1,10 +1,11 @@ - + $(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; diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 7c2e97aa7..73613543d 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview1-15651 -commithash:ebf2365121c2c6a6a0fbfa9b0f37bb5effc89323 +version:3.0.0-build-20181114.5 +commithash:880e9a204d4ee4a18dfd83c9fb05a192a28bca60 diff --git a/korebuild.json b/korebuild.json index bd5d51a51..8a276a7f3 100644 --- a/korebuild.json +++ b/korebuild.json @@ -1,4 +1,4 @@ { - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", - "channel": "dev" + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", + "channel": "master" } diff --git a/run.ps1 b/run.ps1 index 27dcf848f..34604c717 100644 --- a/run.ps1 +++ b/run.ps1 @@ -26,12 +26,18 @@ The base url where build tools can be downloaded. Overrides the value from the c .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 @@ -46,8 +52,8 @@ in the file are overridden by command line parameters. Example config file: ```json { - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", - "channel": "dev", + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", + "channel": "master", "toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools" } ``` @@ -65,8 +71,10 @@ param( [string]$ToolsSource, [Alias('u')] [switch]$Update, - [string]$ConfigFile, + [switch]$Reinstall, [string]$ToolsSourceSuffix, + [string]$ConfigFile = $null, + [switch]$CI, [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) @@ -93,6 +101,10 @@ function Get-KoreBuild { $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 @@ -101,9 +113,9 @@ function Get-KoreBuild { try { $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip" Get-RemoteFile $remotePath $tmpfile $ToolsSourceSuffix - if (Get-Command -Name 'Expand-Archive' -ErrorAction Ignore) { + if (Get-Command -Name 'Microsoft.PowerShell.Archive\Expand-Archive' -ErrorAction Ignore) { # Use built-in commands where possible as they are cross-plat compatible - Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath + Microsoft.PowerShell.Archive\Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath } else { # Fallback to old approach for old installations of PowerShell @@ -167,8 +179,9 @@ if (Test-Path $ConfigFile) { } } catch { - Write-Warning "$ConfigFile could not be read. Its settings will be ignored." - Write-Warning $Error[0] + Write-Host -ForegroundColor Red $Error[0] + Write-Error "$ConfigFile contains invalid JSON." + exit 1 } } @@ -179,7 +192,7 @@ if (!$DotNetHome) { else { Join-Path $PSScriptRoot '.dotnet'} } -if (!$Channel) { $Channel = 'dev' } +if (!$Channel) { $Channel = 'master' } if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' } # Execute @@ -188,7 +201,7 @@ $korebuildPath = Get-KoreBuild Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') try { - Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile + Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile -CI:$CI Invoke-KoreBuildCommand $Command @Arguments } finally { diff --git a/run.sh b/run.sh index 834961fc3..4c1fed564 100755 --- a/run.sh +++ b/run.sh @@ -14,10 +14,12 @@ 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 @@ -38,6 +40,8 @@ __usage() { 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." @@ -62,6 +66,10 @@ get_korebuild() { 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" @@ -175,6 +183,12 @@ while [[ $# -gt 0 ]]; do -u|--update|-Update) update=true ;; + --reinstall|-[Rr]einstall) + reinstall=true + ;; + --ci|-[Cc][Ii]) + ci=true + ;; --verbose|-Verbose) verbose=true ;; @@ -206,26 +220,37 @@ if [ -f "$config_file" ]; then config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")" config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")" else - __warn "$config_file is invalid JSON. Its settings will be ignored." + __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 - __warn "$config_file is invalid JSON. Its settings will be ignored." + __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 - __warn 'Missing required command: jq or pyton. Could not parse the JSON file. Its settings will be ignored.' + __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='dev' +[ -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" +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 index fb2e7d917..010edb018 100644 --- a/samples/CookiePolicySample/CookiePolicySample.csproj +++ b/samples/CookiePolicySample/CookiePolicySample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.1 + netcoreapp3.0 diff --git a/samples/CookiePolicySample/Program.cs b/samples/CookiePolicySample/Program.cs index 12fc8ff28..3fc09a3db 100644 --- a/samples/CookiePolicySample/Program.cs +++ b/samples/CookiePolicySample/Program.cs @@ -12,7 +12,7 @@ public static void Main(string[] args) .ConfigureLogging(factory => { factory.AddConsole(); - factory.AddFilter("Console", level => level >= LogLevel.Information); + factory.AddFilter("Microsoft", LogLevel.Trace); }) .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) diff --git a/samples/CookiePolicySample/Startup.cs b/samples/CookiePolicySample/Startup.cs index 7ce9c2d2d..4dcc6d4dc 100644 --- a/samples/CookiePolicySample/Startup.cs +++ b/samples/CookiePolicySample/Startup.cs @@ -58,6 +58,13 @@ private void NestedApp(IApplicationBuilder app) 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; @@ -84,6 +91,8 @@ private async Task HomePage(HttpContext context) 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"); diff --git a/samples/CookieSample/CookieSample.csproj b/samples/CookieSample/CookieSample.csproj index 193137b86..bc92d68a2 100644 --- a/samples/CookieSample/CookieSample.csproj +++ b/samples/CookieSample/CookieSample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.1 + netcoreapp3.0 diff --git a/samples/CookieSessionSample/CookieSessionSample.csproj b/samples/CookieSessionSample/CookieSessionSample.csproj index 6241edd66..6230c98a5 100644 --- a/samples/CookieSessionSample/CookieSessionSample.csproj +++ b/samples/CookieSessionSample/CookieSessionSample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.1 + netcoreapp3.0 diff --git a/samples/JwtBearerSample/JwtBearerSample.csproj b/samples/JwtBearerSample/JwtBearerSample.csproj index 84b436581..58bf5e2d9 100644 --- a/samples/JwtBearerSample/JwtBearerSample.csproj +++ b/samples/JwtBearerSample/JwtBearerSample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.1 + netcoreapp3.0 aspnet5-JwtBearerSample-20151210102827 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/OpenIdConnect.AzureAdSample.csproj b/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.csproj index b14b9590f..61754b60b 100644 --- a/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.csproj +++ b/samples/OpenIdConnect.AzureAdSample/OpenIdConnect.AzureAdSample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.1 + netcoreapp3.0 aspnet5-OpenIdConnectSample-20151210110318 diff --git a/samples/OpenIdConnectSample/OpenIdConnectSample.csproj b/samples/OpenIdConnectSample/OpenIdConnectSample.csproj index 23e87d4f2..137ee47ba 100644 --- a/samples/OpenIdConnectSample/OpenIdConnectSample.csproj +++ b/samples/OpenIdConnectSample/OpenIdConnectSample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.1 + netcoreapp3.0 aspnet5-OpenIdConnectSample-20151210110318 diff --git a/samples/OpenIdConnectSample/Startup.cs b/samples/OpenIdConnectSample/Startup.cs index c05bc8b52..a2b5a9a5c 100644 --- a/samples/OpenIdConnectSample/Startup.cs +++ b/samples/OpenIdConnectSample/Startup.cs @@ -1,6 +1,7 @@ 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; @@ -44,6 +45,8 @@ public Startup(IHostingEnvironment env) public void ConfigureServices(IServiceCollection services) { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + services.AddAuthentication(sharedOptions => { sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; @@ -56,9 +59,14 @@ public void ConfigureServices(IServiceCollection services) 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 => @@ -119,6 +127,16 @@ await WriteHtmlAsync(response, async res => return; } + 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; + } + if (context.Request.Path.Equals("/Account/AccessDenied")) { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); @@ -218,11 +236,28 @@ await WriteHtmlAsync(response, async res => return; } + if (context.Request.Path.Equals("/login-challenge")) + { + // Challenge the user authentication, and force a login prompt by overwriting the + // "prompt". This could be used for example to require the user to re-enter their + // credentials at the authentication provider, to add an extra confirmation layer. + await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new OpenIdConnectChallengeProperties() + { + Prompt = "login", + + // it is also possible to specify different scopes, e.g. + // Scope = new string[] { "openid", "profile", "other" } + }); + + return; + } + await WriteHtmlAsync(response, async res => { await res.WriteAsync($"

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"); diff --git a/samples/SocialSample/SocialSample.csproj b/samples/SocialSample/SocialSample.csproj index a423ae21a..e1e2262b0 100644 --- a/samples/SocialSample/SocialSample.csproj +++ b/samples/SocialSample/SocialSample.csproj @@ -1,7 +1,7 @@ - + - net461;netcoreapp2.1 + netcoreapp3.0 aspnet5-SocialSample-20151210111056 diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs index 35896e84b..b5032072b 100644 --- a/samples/SocialSample/Startup.cs +++ b/samples/SocialSample/Startup.cs @@ -59,6 +59,7 @@ public void ConfigureServices(IServiceCollection services) .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 => { o.AppId = Configuration["facebook:appid"]; @@ -74,6 +75,8 @@ public void ConfigureServices(IServiceCollection services) }) // 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 => { o.ClientId = Configuration["google:clientid"]; @@ -92,6 +95,8 @@ public void ConfigureServices(IServiceCollection services) }) // 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 => { o.ClientId = Configuration["google:clientid"]; @@ -108,6 +113,7 @@ public void ConfigureServices(IServiceCollection services) }) // 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 => { o.ConsumerKey = Configuration["twitter:consumerkey"]; 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/CookieAuthenticationHandler.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs index 5d0afba46..ff6bb3cbc 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationHandler.cs @@ -14,10 +14,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies { - public class CookieAuthenticationHandler : - AuthenticationHandler, - IAuthenticationSignInHandler, - IAuthenticationSignOutHandler + public class CookieAuthenticationHandler : SignInAuthenticationHandler { private const string HeaderValueNoCache = "no-cache"; private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT"; @@ -31,6 +28,7 @@ public class CookieAuthenticationHandler : 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) @@ -87,7 +85,7 @@ 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; @@ -99,9 +97,28 @@ private void RequestRefresh(AuthenticationTicket ticket) _refreshIssuedUtc = currentUtc; var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); _refreshExpiresUtc = currentUtc.Add(timeSpan); + _refreshTicket = CloneTicket(ticket, replacedPrincipal); } } + private AuthenticationTicket CloneTicket(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal) + { + var principal = replacedPrincipal ?? ticket.Principal; + var newPrincipal = new ClaimsPrincipal(); + foreach (var identity in principal.Identities) + { + newPrincipal.AddIdentity(identity.Clone()); + } + + var newProperties = new AuthenticationProperties(); + foreach (var item in ticket.Properties.Items) + { + newProperties.Items[item.Key] = item.Value; + } + + return new AuthenticationTicket(newPrincipal, newProperties, ticket.AuthenticationScheme); + } + private async Task ReadCookieTicket() { var cookie = Options.CookieManager.GetRequestCookie(Context, Options.Cookie.Name); @@ -167,7 +184,7 @@ 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, Scheme.Name)); @@ -190,7 +207,7 @@ protected virtual async Task FinishResponseAsync() return; } - var ticket = (await HandleAuthenticateOnceSafeAsync())?.Ticket; + var ticket = _refreshTicket; if (ticket != null) { var properties = ticket.Properties; @@ -233,7 +250,7 @@ protected virtual async Task FinishResponseAsync() } } - public async virtual Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) { if (user == null) { @@ -320,7 +337,7 @@ public async virtual Task SignInAsync(ClaimsPrincipal user, AuthenticationProper Logger.SignedIn(Scheme.Name); } - public async virtual Task SignOutAsync(AuthenticationProperties properties) + protected async override Task HandleSignOutAsync(AuthenticationProperties properties) { properties = properties ?? new AuthenticationProperties(); @@ -405,7 +422,7 @@ protected override async Task HandleForbiddenAsync(AuthenticationProperties prop 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 RedirectContext(Context, Scheme, Options, properties, BuildRedirectUri(accessDeniedUri)); @@ -417,7 +434,7 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop 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); diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs index 35017f9c4..78156caec 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs @@ -147,68 +147,5 @@ public CookieBuilder Cookie /// /// public TimeSpan ExpireTimeSpan { get; set; } - - #region Obsolete API - /// - /// - /// This property is obsolete and will be removed in a future version. The recommended alternative is on . - /// - /// - /// Determines the cookie name used to persist the identity. The default value is ".AspNetCore.Cookies". - /// This value should be changed if you change the name of the AuthenticationScheme, especially if your - /// system uses the cookie authentication handler multiple times. - /// - /// - [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Name) + ".")] - public string CookieName { get => Cookie.Name; set => Cookie.Name = value; } - - /// - /// - /// This property is obsolete and will be removed in a future version. The recommended alternative is on . - /// - /// - /// Determines the domain used to create the cookie. Is not provided by default. - /// - /// - [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Domain) + ".")] - public string CookieDomain { get => Cookie.Domain; set => Cookie.Domain = value; } - - /// - /// - /// This property is obsolete and will be removed in a future version. The recommended alternative is on . - /// - /// - /// Determines the path used to create the cookie. The default value is "/" for highest browser compatibility. - /// - /// - [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.Path) + ".")] - public string CookiePath { get => Cookie.Path; set => Cookie.Path = value; } - - /// - /// - /// This property is obsolete and will be removed in a future version. The recommended alternative is on . - /// - /// - /// 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. - /// - /// - [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.HttpOnly) + ".")] - public bool CookieHttpOnly { get => Cookie.HttpOnly; set => Cookie.HttpOnly = value; } - - /// - /// - /// This property is obsolete and will be removed in a future version. The recommended alternative is on . - /// - /// - /// 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. - /// - /// - [Obsolete("This property is obsolete and will be removed in a future version. The recommended alternative is " + nameof(Cookie) + "." + nameof(CookieBuilder.SecurePolicy) + ".")] - public CookieSecurePolicy CookieSecure { get => Cookie.SecurePolicy; set => Cookie.SecurePolicy = value; } - #endregion } } diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj b/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj index b188a58e0..093f60a3d 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/Microsoft.AspNetCore.Authentication.Cookies.csproj @@ -2,7 +2,7 @@ ASP.NET Core middleware that enables an application to use cookie based authentication. - netstandard2.0 + netcoreapp3.0 $(DefineConstants);SECURITY $(NoWarn);CS1591 true diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json index 1f1115460..b218669b7 100644 --- a/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Authentication.Cookies/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Cookies, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Cookies, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.Extensions.DependencyInjection.CookieExtensions", @@ -239,10 +239,8 @@ "Name": "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler", "Visibility": "Public", "Kind": "Class", - "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticationHandler", - "ImplementedInterfaces": [ - "Microsoft.AspNetCore.Authentication.IAuthenticationSignInHandler" - ], + "BaseType": "Microsoft.AspNetCore.Authentication.SignInAuthenticationHandler", + "ImplementedInterfaces": [], "Members": [ { "Kind": "Method", @@ -306,7 +304,7 @@ }, { "Kind": "Method", - "Name": "SignInAsync", + "Name": "HandleSignInAsync", "Parameters": [ { "Name": "user", @@ -319,13 +317,13 @@ ], "ReturnType": "System.Threading.Tasks.Task", "Virtual": true, - "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignInHandler", - "Visibility": "Public", + "Override": true, + "Visibility": "Protected", "GenericParameter": [] }, { "Kind": "Method", - "Name": "SignOutAsync", + "Name": "HandleSignOutAsync", "Parameters": [ { "Name": "properties", @@ -334,8 +332,8 @@ ], "ReturnType": "System.Threading.Tasks.Task", "Virtual": true, - "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignOutHandler", - "Visibility": "Public", + "Override": true, + "Visibility": "Protected", "GenericParameter": [] }, { 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.Facebook/FacebookDefaults.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs index 6143a4f23..ff9bb40bf 100644 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookDefaults.cs @@ -9,10 +9,11 @@ public static class FacebookDefaults public static readonly string DisplayName = "Facebook"; - public static readonly string AuthorizationEndpoint = "https://www.facebook.com/v2.6/dialog/oauth"; + // 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 TokenEndpoint = "https://graph.facebook.com/v2.6/oauth/access_token"; + public static readonly string TokenEndpoint = "https://graph.facebook.com/v3.1/oauth/access_token"; - public static readonly string UserInformationEndpoint = "https://graph.facebook.com/v2.6/me"; + public static readonly string UserInformationEndpoint = "https://graph.facebook.com/v3.1/me"; } } diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs index 0f83c1719..eb4251143 100644 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; using System.Globalization; using System.Net.Http; using System.Security.Claims; @@ -65,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(); } } diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs index 7010bb20a..c2078a017 100644 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs @@ -26,7 +26,6 @@ public FacebookOptions() AuthorizationEndpoint = FacebookDefaults.AuthorizationEndpoint; TokenEndpoint = FacebookDefaults.TokenEndpoint; UserInformationEndpoint = FacebookDefaults.UserInformationEndpoint; - Scope.Add("public_profile"); Scope.Add("email"); Fields.Add("name"); Fields.Add("email"); diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj b/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj index 62aee1367..f57fe26e0 100644 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/Microsoft.AspNetCore.Authentication.Facebook.csproj @@ -2,7 +2,7 @@ ASP.NET Core middleware that enables an application to support Facebook's OAuth 2.0 authentication workflow. - netstandard2.0 + netcoreapp3.0 $(NoWarn);CS1591 true aspnetcore;authentication;security diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json index 82a48a6bf..5d95efca6 100644 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Facebook, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Facebook, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.Extensions.DependencyInjection.FacebookAuthenticationOptionsExtensions", @@ -160,6 +160,88 @@ ], "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", 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 e4bd66615..26b3b8f01 100644 --- a/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.cs +++ b/src/Microsoft.AspNetCore.Authentication.Google/GoogleDefaults.cs @@ -12,10 +12,12 @@ public static class GoogleDefaults public static readonly string DisplayName = "Google"; - public static readonly string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/auth"; + // 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/GoogleHandler.cs b/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs index 091896f7c..88d48d446 100644 --- a/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs @@ -57,12 +57,12 @@ protected override string BuildChallengeUrl(AuthenticationProperties properties, queryStrings.Add("client_id", Options.ClientId); queryStrings.Add("redirect_uri", redirectUri); - AddQueryString(queryStrings, properties, "scope", FormatScope()); - AddQueryString(queryStrings, properties, "access_type", Options.AccessType); - AddQueryString(queryStrings, properties, "approval_prompt"); - AddQueryString(queryStrings, properties, "prompt"); - AddQueryString(queryStrings, properties, "login_hint"); - AddQueryString(queryStrings, properties, "include_granted_scopes"); + 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); @@ -71,29 +71,38 @@ protected override string BuildChallengeUrl(AuthenticationProperties properties, return authorizationEndpoint; } - private static void AddQueryString( + private void AddQueryString( IDictionary queryStrings, AuthenticationProperties properties, string name, - string defaultValue = null) + 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); } } diff --git a/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj b/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj index de8867f91..eba5b98ad 100644 --- a/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj +++ b/src/Microsoft.AspNetCore.Authentication.Google/Microsoft.AspNetCore.Authentication.Google.csproj @@ -2,7 +2,7 @@ ASP.NET Core contains middleware to support Google's OpenId and OAuth 2.0 authentication workflows. - netstandard2.0 + netcoreapp3.0 $(NoWarn);CS1591 true aspnetcore;authentication;security diff --git a/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json index 61aae649d..0a623b3b8 100644 --- a/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Authentication.Google/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Google, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Google, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.Extensions.DependencyInjection.GoogleExtensions", @@ -97,6 +97,206 @@ ], "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", @@ -160,6 +360,82 @@ ], "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", diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs index 6d5c7f5f5..4f03ccd69 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs @@ -22,7 +22,7 @@ public class JwtBearerHandler : AuthenticationHandler { private OpenIdConnectConfiguration _configuration; - public JwtBearerHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, IDataProtectionProvider dataProtection, ISystemClock clock) + public JwtBearerHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } @@ -32,8 +32,8 @@ public JwtBearerHandler(IOptionsMonitor options, ILoggerFactor /// protected new JwtBearerEvents Events { - get { return (JwtBearerEvents)base.Events; } - set { base.Events = value; } + get => (JwtBearerEvents)base.Events; + set => base.Events = value; } protected override Task CreateEventsAsync() => Task.FromResult(new JwtBearerEvents()); @@ -267,9 +267,8 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop private static string CreateErrorDescription(Exception authFailure) { IEnumerable exceptions; - if (authFailure is AggregateException) + if (authFailure is AggregateException agEx) { - var agEx = authFailure as AggregateException; exceptions = agEx.InnerExceptions; } else @@ -283,37 +282,32 @@ private static string CreateErrorDescription(Exception authFailure) { // Order sensitive, some of these exceptions derive from others // and we want to display the most specific message possible. - if (ex is SecurityTokenInvalidAudienceException) + switch (ex) { - messages.Add("The audience is invalid"); - } - else if (ex is SecurityTokenInvalidIssuerException) - { - messages.Add("The issuer is invalid"); - } - else if (ex is SecurityTokenNoExpirationException) - { - messages.Add("The token has no expiration"); - } - else if (ex is SecurityTokenInvalidLifetimeException) - { - messages.Add("The token lifetime is invalid"); - } - else if (ex is SecurityTokenNotYetValidException) - { - messages.Add("The token is not valid yet"); - } - else if (ex is SecurityTokenExpiredException) - { - messages.Add("The token is expired"); - } - else if (ex is SecurityTokenSignatureKeyNotFoundException) - { - messages.Add("The signature key was not found"); - } - else if (ex is SecurityTokenInvalidSignatureException) - { - messages.Add("The signature is invalid"); + 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; } } diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj index e5bae5a3d..8a26c58f3 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Microsoft.AspNetCore.Authentication.JwtBearer.csproj @@ -2,7 +2,7 @@ ASP.NET Core middleware that enables an application to receive an OpenID Connect bearer token. - netstandard2.0 + netcoreapp3.0 $(NoWarn);CS1591 true aspnetcore;authentication;security diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json index 997cf7bdf..d3839022b 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.JwtBearer, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.JwtBearer, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.Extensions.DependencyInjection.JwtBearerExtensions", @@ -609,6 +609,100 @@ ], "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", 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.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj index 0eddc6f76..1841157c9 100644 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj +++ b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj @@ -2,7 +2,7 @@ ASP.NET Core middleware that enables an application to support the Microsoft Account authentication workflow. - netstandard2.0 + netcoreapp3.0 $(NoWarn);CS1591 true aspnetcore;authentication;security diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs index 1b0859c5b..0421fa14b 100644 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs +++ b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountDefaults.cs @@ -9,6 +9,7 @@ public static class MicrosoftAccountDefaults 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/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/baseline.netcore.json index 5791297b2..877e9035a 100644 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.MicrosoftAccount, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.MicrosoftAccount, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.Extensions.DependencyInjection.MicrosoftAccountExtensions", @@ -160,6 +160,63 @@ ], "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", diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs index f3fee6a22..5a178957a 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs @@ -87,6 +87,27 @@ public static void MapCustomJson(this ClaimActionCollection collection, string c 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. /// @@ -96,5 +117,23 @@ public static void DeleteClaim(this ClaimActionCollection collection, string cla { 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/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/Microsoft.AspNetCore.Authentication.OAuth.csproj b/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.csproj index 5c8a5e3a9..97d4a9703 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.csproj +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Microsoft.AspNetCore.Authentication.OAuth.csproj @@ -2,7 +2,7 @@ ASP.NET Core middleware that enables an application to support any standard OAuth 2.0 authentication workflow. - netstandard2.0 + netcoreapp3.0 $(NoWarn);CS1591 true aspnetcore;authentication;security 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/OAuthHandler.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs index 80680a7cf..05ba4df68 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs @@ -63,6 +63,16 @@ protected override async Task HandleRemoteAuthenticateAsync 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"]; @@ -194,7 +204,7 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop { if (string.IsNullOrEmpty(properties.RedirectUri)) { - properties.RedirectUri = CurrentUri; + properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString; } // OAuth2 10.12 CSRF @@ -209,7 +219,8 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop 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 parameters = new Dictionary @@ -223,10 +234,20 @@ protected virtual string BuildChallengeUrl(AuthenticationProperties properties, 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/OAuthOptions.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs index 3c71f055f..4eacbd0b3 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs @@ -3,11 +3,9 @@ using System; using System.Collections.Generic; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.OAuth; -using Microsoft.AspNetCore.Authentication.OAuth.Claims; -using Microsoft.AspNetCore.Http.Authentication; using System.Globalization; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Authentication.OAuth { diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json index ffda3cbaf..9c2394704 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.OAuth, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.OAuth, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.Extensions.DependencyInjection.OAuthExtensions", @@ -417,6 +417,41 @@ "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", @@ -435,6 +470,26 @@ "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": [] @@ -697,6 +752,97 @@ ], "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", @@ -848,6 +994,20 @@ "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", @@ -1605,6 +1765,46 @@ } ], "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.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj index b7f4c1704..65ca6b842 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj @@ -2,7 +2,7 @@ ASP.NET Core middleware that enables an application to support the OpenID Connect authentication workflow. - netstandard2.0 + netcoreapp3.0 $(NoWarn);CS1591 true aspnetcore;authentication;security 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/OpenIdConnectHandler.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs index 4f722323d..8b0b72428 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs @@ -155,6 +155,13 @@ protected virtual async Task HandleRemoteSignOutAsync() /// 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); @@ -179,7 +186,7 @@ public async virtual Task SignOutAsync(AuthenticationProperties properties) properties.RedirectUri = BuildRedirectUriIfRelative(Options.SignedOutRedirectUri); if (string.IsNullOrWhiteSpace(properties.RedirectUri)) { - properties.RedirectUri = CurrentUri; + properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString; } } Logger.PostSignOutRedirect(properties.RedirectUri); @@ -305,7 +312,7 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop // 2. CurrentUri if RedirectUri is not set) if (string.IsNullOrEmpty(properties.RedirectUri)) { - properties.RedirectUri = CurrentUri; + properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString; } Logger.PostAuthenticationLocalRedirect(properties.RedirectUri); @@ -322,15 +329,16 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop RedirectUri = BuildRedirectUri(Options.CallbackPath), Resource = Options.Resource, ResponseType = Options.ResponseType, - Prompt = Options.Prompt, - 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 - if (Options.MaxAge.HasValue) + var maxAge = properties.GetParameter(OpenIdConnectParameterNames.MaxAge) ?? Options.MaxAge; + if (maxAge.HasValue) { - message.MaxAge = Convert.ToInt64(Math.Floor((Options.MaxAge.Value).TotalSeconds)) + message.MaxAge = Convert.ToInt64(Math.Floor((maxAge.Value).TotalSeconds)) .ToString(CultureInfo.InvariantCulture); } @@ -512,6 +520,16 @@ protected override async Task HandleRemoteAuthenticateAsync // if any of the error fields are set, throw error null if (!string.IsNullOrEmpty(authorizationResponse.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 (string.Equals(authorizationResponse.Error, "access_denied", StringComparison.Ordinal)) + { + return await HandleAccessDeniedErrorAsync(properties); + } + return HandleRequestResult.Fail(CreateOpenIdConnectProtocolException(authorizationResponse, response: null), properties); } @@ -776,7 +794,7 @@ protected virtual async Task RedeemAuthorizationCodeAsync( /// The authentication properties. /// which is used to determine if the remote authentication was successful. protected virtual async Task GetUserInformationAsync( - OpenIdConnectMessage message, JwtSecurityToken jwt, + OpenIdConnectMessage message, JwtSecurityToken jwt, ClaimsPrincipal principal, AuthenticationProperties properties) { var userInfoEndpoint = _configuration?.UserInfoEndpoint; diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json index 9623b214e..d5c10d18d 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.OpenIdConnect, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.OpenIdConnect, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.Extensions.DependencyInjection.OpenIdConnectExtensions", @@ -637,6 +637,27 @@ "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", @@ -791,6 +812,20 @@ "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", @@ -1265,6 +1300,113 @@ ], "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", @@ -1347,6 +1489,17 @@ "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", @@ -1394,17 +1547,6 @@ "Visibility": "Protected", "GenericParameter": [] }, - { - "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": "HandleRemoteSignOutAsync", @@ -1748,6 +1890,27 @@ "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", @@ -1916,6 +2079,27 @@ "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", diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj b/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj index f720d08f0..4f2aae616 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj @@ -2,7 +2,7 @@ ASP.NET Core middleware that enables an application to support Twitter's OAuth 1.0 authentication workflow. - netstandard2.0 + netcoreapp3.0 $(NoWarn);CS1591 true aspnetcore;authentication;security diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs index a39a3f036..bdab80e59 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterDefaults.cs @@ -8,5 +8,14 @@ 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/TwitterHandler.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs index 670e76f7e..cc8591ed3 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs @@ -22,9 +22,6 @@ namespace Microsoft.AspNetCore.Authentication.Twitter public class TwitterHandler : RemoteAuthenticationHandler { private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - private const string RequestTokenEndpoint = "https://api.twitter.com/oauth/request_token"; - private const string AuthenticationEndpoint = "https://api.twitter.com/oauth/authenticate?oauth_token="; - private const string AccessTokenEndpoint = "https://api.twitter.com/oauth/access_token"; private HttpClient Backchannel => Options.Backchannel; @@ -58,12 +55,14 @@ protected override async Task HandleRemoteAuthenticateAsync var properties = requestToken.Properties; - // REVIEW: see which of these are really errors - var denied = query["denied"]; if (!StringValues.IsNullOrEmpty(denied)) { - return HandleRequestResult.Fail("The user denied permissions.", properties); + // 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"]; @@ -133,12 +132,12 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop { 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(BuildRedirectUri(Options.CallbackPath), properties); - var twitterAuthenticationEndpoint = AuthenticationEndpoint + requestToken.Token; + var twitterAuthenticationEndpoint = TwitterDefaults.AuthenticationEndpoint + requestToken.Token; var cookieOptions = Options.StateCookie.Build(Context, Clock.UtcNow); @@ -148,53 +147,92 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop await Events.RedirectToAuthorizationEndpoint(redirectContext); } - private async Task ObtainRequestTokenAsync(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", Options.ConsumerKey }, - { "oauth_nonce", nonce }, + { "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(Options.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 Backchannel.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(); var responseText = await response.Content.ReadAsStringAsync(); @@ -213,58 +251,8 @@ private async Task ObtainAccessTokenAsync(RequestToken token, strin Logger.ObtainAccessToken(); - var nonce = Guid.NewGuid().ToString("N"); - - var authorizationParts = new SortedDictionary - { - { "oauth_consumer_key", Options.ConsumerKey }, - { "oauth_nonce", nonce }, - { "oauth_signature_method", "HMAC-SHA1" }, - { "oauth_token", token.Token }, - { "oauth_timestamp", GenerateTimeStamp() }, - { "oauth_verifier", verifier }, - { "oauth_version", "1.0" }, - }; - - var parameterBuilder = new StringBuilder(); - foreach (var authorizationKey in authorizationParts) - { - parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.Encode(authorizationKey.Key), UrlEncoder.Encode(authorizationKey.Value)); - } - parameterBuilder.Length--; - var parameterString = parameterBuilder.ToString(); - - var canonicalizedRequestBuilder = new StringBuilder(); - canonicalizedRequestBuilder.Append(HttpMethod.Post.Method); - canonicalizedRequestBuilder.Append("&"); - canonicalizedRequestBuilder.Append(UrlEncoder.Encode(AccessTokenEndpoint)); - canonicalizedRequestBuilder.Append("&"); - canonicalizedRequestBuilder.Append(UrlEncoder.Encode(parameterString)); - - var signature = ComputeSignature(Options.ConsumerSecret, token.TokenSecret, canonicalizedRequestBuilder.ToString()); - authorizationParts.Add("oauth_signature", signature); - authorizationParts.Remove("oauth_verifier"); - - var authorizationHeaderBuilder = new StringBuilder(); - authorizationHeaderBuilder.Append("OAuth "); - foreach (var authorizationPart in authorizationParts) - { - authorizationHeaderBuilder.AppendFormat( - "{0}=\"{1}\", ", authorizationPart.Key, UrlEncoder.Encode(authorizationPart.Value)); - } - authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2; - - var request = new HttpRequestMessage(HttpMethod.Post, AccessTokenEndpoint); - request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString()); - - var formPairs = new Dictionary() - { - { "oauth_verifier", verifier }, - }; - - request.Content = new FormUrlEncodedContent(formPairs); - - var response = await Backchannel.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) { @@ -289,53 +277,8 @@ private async Task RetrieveUserDetailsAsync(AccessToken accessToken, Cl { Logger.RetrieveUserDetails(); - var nonce = Guid.NewGuid().ToString("N"); - - var authorizationParts = new SortedDictionary - { - { "oauth_consumer_key", Options.ConsumerKey }, - { "oauth_nonce", nonce }, - { "oauth_signature_method", "HMAC-SHA1" }, - { "oauth_timestamp", GenerateTimeStamp() }, - { "oauth_token", accessToken.Token }, - { "oauth_version", "1.0" } - }; - - var parameterBuilder = new StringBuilder(); - foreach (var authorizationKey in authorizationParts) - { - parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.Encode(authorizationKey.Key), UrlEncoder.Encode(authorizationKey.Value)); - } - parameterBuilder.Length--; - var parameterString = parameterBuilder.ToString(); - - var resource_url = "https://api.twitter.com/1.1/account/verify_credentials.json"; - var resource_query = "include_email=true"; - var canonicalizedRequestBuilder = new StringBuilder(); - canonicalizedRequestBuilder.Append(HttpMethod.Get.Method); - canonicalizedRequestBuilder.Append("&"); - canonicalizedRequestBuilder.Append(UrlEncoder.Encode(resource_url)); - canonicalizedRequestBuilder.Append("&"); - canonicalizedRequestBuilder.Append(UrlEncoder.Encode(resource_query)); - canonicalizedRequestBuilder.Append("%26"); - canonicalizedRequestBuilder.Append(UrlEncoder.Encode(parameterString)); - - var signature = ComputeSignature(Options.ConsumerSecret, accessToken.TokenSecret, canonicalizedRequestBuilder.ToString()); - authorizationParts.Add("oauth_signature", signature); - - var authorizationHeaderBuilder = new StringBuilder(); - authorizationHeaderBuilder.Append("OAuth "); - foreach (var authorizationPart in authorizationParts) - { - authorizationHeaderBuilder.AppendFormat( - "{0}=\"{1}\", ", authorizationPart.Key, UrlEncoder.Encode(authorizationPart.Value)); - } - authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2; - - var request = new HttpRequestMessage(HttpMethod.Get, resource_url + "?include_email=true"); - request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString()); + var response = await ExecuteRequestAsync("https://api.twitter.com/1.1/account/verify_credentials.json", HttpMethod.Get, accessToken, queryParameters: new Dictionary() { { "include_email", "true" } }); - var response = await Backchannel.SendAsync(request, Context.RequestAborted); if (!response.IsSuccessStatusCode) { Logger.LogError("Email request failed with a status code of " + response.StatusCode); @@ -361,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/TwitterOptions.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs index 03396807e..269516ae1 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs @@ -2,8 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Security.Claims; using System.Globalization; +using System.Security.Claims; using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Http; diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json index b577f07eb..03ee64562 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Twitter, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication.Twitter, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.Extensions.DependencyInjection.TwitterExtensions", @@ -600,6 +600,122 @@ ], "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", @@ -741,6 +857,16 @@ "Visibility": "Public", "GenericParameter": [] }, + { + "Kind": "Method", + "Name": "Validate", + "Parameters": [], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, { "Kind": "Constructor", "Name": ".ctor", 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.WsFederation/Resources.resx b/src/Microsoft.AspNetCore.Authentication.WsFederation/Resources.resx new file mode 100644 index 000000000..e2edafb67 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.WsFederation/Resources.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The service descriptor is missing. + + + 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/AuthenticationBuilder.cs b/src/Microsoft.AspNetCore.Authentication/AuthenticationBuilder.cs index 7bf8fe96e..401b1f488 100644 --- a/src/Microsoft.AspNetCore.Authentication/AuthenticationBuilder.cs +++ b/src/Microsoft.AspNetCore.Authentication/AuthenticationBuilder.cs @@ -91,15 +91,15 @@ public virtual AuthenticationBuilder AddRemoteScheme(string } /// - /// Adds a based authentication handler which can be used to + /// 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 AddVirtualScheme(string authenticationScheme, string displayName, Action configureOptions) - => AddSchemeHelper(authenticationScheme, displayName, configureOptions); + 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 diff --git a/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs b/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs index ef4292100..5c9a6473f 100644 --- a/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication/AuthenticationHandler.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 System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -118,8 +117,24 @@ protected virtual async Task InitializeEventsAsync() protected string BuildRedirectUri(string targetPath) => Request.Scheme + "://" + Request.Host + OriginalPathBase + targetPath; + protected virtual string ResolveTarget(string scheme) + { + var target = scheme ?? Options.ForwardDefaultSelector?.Invoke(Context) ?? Options.ForwardDefault; + + // Prevent self targetting + return string.Equals(target, Scheme.Name, StringComparison.Ordinal) + ? null + : target; + } + public async Task AuthenticateAsync() { + var target = ResolveTarget(Options.ForwardAuthenticate); + if (target != null) + { + return await Context.AuthenticateAsync(target); + } + // Calling Authenticate more than once should always return the original value. var result = await HandleAuthenticateOnceAsync(); if (result?.Failure == null) @@ -200,6 +215,13 @@ protected virtual Task HandleChallengeAsync(AuthenticationProperties properties) public async Task ChallengeAsync(AuthenticationProperties properties) { + var target = ResolveTarget(Options.ForwardChallenge); + if (target != null) + { + await Context.ChallengeAsync(target, properties); + return; + } + properties = properties ?? new AuthenticationProperties(); await HandleChallengeAsync(properties); Logger.AuthenticationSchemeChallenged(Scheme.Name); @@ -207,6 +229,13 @@ public async Task ChallengeAsync(AuthenticationProperties properties) public async Task ForbidAsync(AuthenticationProperties properties) { + var target = ResolveTarget(Options.ForwardForbid); + if (target != null) + { + await Context.ForbidAsync(target, properties); + return; + } + properties = properties ?? new AuthenticationProperties(); await HandleForbiddenAsync(properties); Logger.AuthenticationSchemeForbidden(Scheme.Name); diff --git a/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs b/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs index 18d4c9788..a547d203b 100644 --- a/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication/AuthenticationSchemeOptions.cs @@ -2,6 +2,7 @@ // 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 { @@ -36,5 +37,57 @@ public virtual void Validate(string scheme) /// 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 b274eaace..6c381b84a 100644 --- a/src/Microsoft.AspNetCore.Authentication/AuthenticationServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication/AuthenticationServiceCollectionExtensions.cs @@ -46,48 +46,6 @@ public static AuthenticationBuilder AddAuthentication(this IServiceCollection se return builder; } - [Obsolete("AddScheme is obsolete. Use AddAuthentication().AddScheme instead.")] - public static IServiceCollection AddScheme(this IServiceCollection services, string authenticationScheme, string displayName, Action configureScheme, Action configureOptions) - where TOptions : AuthenticationSchemeOptions, new() - where THandler : AuthenticationHandler - { - services.AddAuthentication(o => - { - o.AddScheme(authenticationScheme, scheme => { - scheme.HandlerType = typeof(THandler); - scheme.DisplayName = displayName; - configureScheme?.Invoke(scheme); - }); - }); - if (configureOptions != null) - { - services.Configure(authenticationScheme, configureOptions); - } - services.AddTransient(); - return services; - } - - [Obsolete("AddScheme is obsolete. Use AddAuthentication().AddScheme instead.")] - public static IServiceCollection AddScheme(this IServiceCollection services, string authenticationScheme, Action configureOptions) - where TOptions : AuthenticationSchemeOptions, new() - where THandler : AuthenticationHandler - => services.AddScheme(authenticationScheme, displayName: null, configureScheme: null, configureOptions: configureOptions); - - [Obsolete("AddScheme is obsolete. Use AddAuthentication().AddScheme instead.")] - public static IServiceCollection AddScheme(this IServiceCollection services, string authenticationScheme, string displayName, Action configureOptions) - where TOptions : AuthenticationSchemeOptions, new() - where THandler : AuthenticationHandler - => services.AddScheme(authenticationScheme, displayName, configureScheme: null, configureOptions: configureOptions); - - [Obsolete("AddScheme is obsolete. Use AddAuthentication().AddScheme instead.")] - public static IServiceCollection AddRemoteScheme(this IServiceCollection services, string authenticationScheme, string displayName, Action configureOptions) - where TOptions : RemoteAuthenticationOptions, new() - where THandler : RemoteAuthenticationHandler - { - services.TryAddEnumerable(ServiceDescriptor.Singleton, EnsureSignInScheme>()); - return services.AddScheme(authenticationScheme, displayName, configureScheme: null, configureOptions: configureOptions); - } - // Used to ensure that there's always a sign in scheme private class EnsureSignInScheme : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions { 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/RemoteAuthenticationEvents.cs b/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.cs index ca0f4a5c0..be4c6ab49 100644 --- a/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.cs +++ b/src/Microsoft.AspNetCore.Authentication/Events/RemoteAuthenticationEvents.cs @@ -8,12 +8,18 @@ namespace Microsoft.AspNetCore.Authentication { public class RemoteAuthenticationEvents { + public Func OnAccessDenied { get; set; } = context => Task.CompletedTask; public Func OnRemoteFailure { get; set; } = context => Task.CompletedTask; 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 AccessDenied(AccessDeniedContext context) => OnAccessDenied(context); + + /// + /// Invoked when there is a remote failure. /// public virtual Task RemoteFailure(RemoteFailureContext context) => OnRemoteFailure(context); diff --git a/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs b/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs index 3f6c2d917..da9b6ea01 100644 --- a/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs +++ b/src/Microsoft.AspNetCore.Authentication/HandleRequestResult.cs @@ -33,7 +33,7 @@ public class HandleRequestResult : AuthenticateResult { throw new ArgumentNullException(nameof(ticket)); } - return new HandleRequestResult() { Ticket = ticket }; + return new HandleRequestResult() { Ticket = ticket, Properties = ticket.Properties }; } /// diff --git a/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs b/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs index 8cba6c0d5..042bfabca 100644 --- a/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs +++ b/src/Microsoft.AspNetCore.Authentication/LoggingExtensions.cs @@ -7,17 +7,20 @@ namespace Microsoft.Extensions.Logging { internal static class LoggingExtensions { - private static Action _authSchemeAuthenticated; - private static Action _authSchemeNotAuthenticated; - private static Action _authSchemeNotAuthenticatedWithFailure; - private static Action _authSchemeChallenged; - private static Action _authSchemeForbidden; - 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() { @@ -65,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) @@ -121,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 index 7e3ce4eb3..9ae0a7bcc 100644 --- a/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj +++ b/src/Microsoft.AspNetCore.Authentication/Microsoft.AspNetCore.Authentication.csproj @@ -2,7 +2,7 @@ ASP.NET Core common types used by the various authentication middleware components. - netstandard2.0 + netcoreapp3.0 $(NoWarn);CS1591 true aspnetcore;authentication;security 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/PolicySchemeOptions.cs b/src/Microsoft.AspNetCore.Authentication/PolicySchemeOptions.cs new file mode 100644 index 000000000..1921c77ec --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication/PolicySchemeOptions.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication +{ + /// + /// Contains the options used by the . + /// + public class PolicySchemeOptions : AuthenticationSchemeOptions + { } +} diff --git a/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs b/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs index bea4895d6..c72583b5c 100644 --- a/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationHandler.cs @@ -5,6 +5,7 @@ using System.Security.Cryptography; using System.Text.Encodings.Web; using System.Threading.Tasks; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -241,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 1bd3b210e..188b7a991 100644 --- a/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs @@ -89,6 +89,22 @@ public override void Validate() /// 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. 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/VirtualAuthenticationHandler.cs b/src/Microsoft.AspNetCore.Authentication/VirtualAuthenticationHandler.cs deleted file mode 100644 index 4a023bec2..000000000 --- a/src/Microsoft.AspNetCore.Authentication/VirtualAuthenticationHandler.cs +++ /dev/null @@ -1,71 +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.Http; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Authentication -{ - /// - /// Forwards calls to another authentication scheme. - /// - public class VirtualAuthenticationHandler : IAuthenticationHandler, IAuthenticationSignInHandler - { - protected IOptionsMonitor OptionsMonitor { get; } - public AuthenticationScheme Scheme { get; private set; } - public VirtualSchemeOptions Options { get; private set; } - protected HttpContext Context { get; private set; } - - public VirtualAuthenticationHandler(IOptionsMonitor options) - { - OptionsMonitor = options; - } - - /// - /// Initialize the handler, resolve the options and validate them. - /// - /// - /// - /// A Task. - public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) - { - if (scheme == null) - { - throw new ArgumentNullException(nameof(scheme)); - } - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - Scheme = scheme; - Context = context; - - Options = OptionsMonitor.Get(Scheme.Name) ?? new VirtualSchemeOptions(); - Options.Validate(); - - return Task.CompletedTask; - } - - protected virtual string ResolveTarget(string scheme) - => scheme ?? Options.DefaultSelector?.Invoke(Context) ?? Options.Default; - - public virtual Task AuthenticateAsync() - => Context.AuthenticateAsync(ResolveTarget(Options.Authenticate)); - - public virtual Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) - => Context.SignInAsync(ResolveTarget(Options.SignIn), user, properties); - - public virtual Task SignOutAsync(AuthenticationProperties properties) - => Context.SignOutAsync(ResolveTarget(Options.SignOut), properties); - - public virtual Task ChallengeAsync(AuthenticationProperties properties) - => Context.ChallengeAsync(ResolveTarget(Options.Challenge), properties); - - public virtual Task ForbidAsync(AuthenticationProperties properties) - => Context.ForbidAsync(ResolveTarget(Options.Forbid), properties); - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/VirtualSchemeOptions.cs b/src/Microsoft.AspNetCore.Authentication/VirtualSchemeOptions.cs deleted file mode 100644 index 38d819bf5..000000000 --- a/src/Microsoft.AspNetCore.Authentication/VirtualSchemeOptions.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; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Authentication -{ - /// - /// Used to redirect authentication methods to another scheme - /// - public class VirtualSchemeOptions - { - public string Default { get; set; } - - public string Authenticate { get; set; } - public string Challenge { get; set; } - public string Forbid { get; set; } - public string SignIn { get; set; } - public string SignOut { get; set; } - - /// - /// Used to select a default scheme to target based on the request. - /// - public Func DefaultSelector { get; set; } - - - /// - /// Check that the options are valid. Should throw an exception if things are not ok. - /// - public virtual void Validate() { } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json b/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json index 6db7d2c81..08eeb5e7b 100644 --- a/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Authentication/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Authentication, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Authentication, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.AspNetCore.Authentication.AuthenticationBuilder", @@ -126,6 +126,28 @@ } ] }, + { + "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", @@ -338,6 +360,20 @@ "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", @@ -545,6 +581,20 @@ "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", @@ -608,6 +658,153 @@ "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", @@ -1752,6 +1949,27 @@ "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", @@ -1966,59 +2184,313 @@ "GenericParameters": [] }, { - "Name": "Microsoft.AspNetCore.Authentication.ISystemClock", + "Name": "Microsoft.AspNetCore.Authentication.HandleRequestResult", "Visibility": "Public", - "Kind": "Interface", - "Abstract": true, + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", "ImplementedInterfaces": [], "Members": [ { "Kind": "Method", - "Name": "get_UtcNow", + "Name": "get_Handled", "Parameters": [], - "ReturnType": "System.DateTimeOffset", + "ReturnType": "System.Boolean", + "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", + "Name": "get_Skipped", "Parameters": [], - "ReturnType": "System.String", - "Visibility": "Protected", + "ReturnType": "System.Boolean", + "Visibility": "Public", "GenericParameter": [] }, { "Kind": "Method", - "Name": "get_Events", - "Parameters": [], - "ReturnType": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents", - "Visibility": "Protected", + "Name": "Success", + "Parameters": [ + { + "Name": "ticket", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + } + ], + "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Static": true, + "Visibility": "Public", "GenericParameter": [] }, { "Kind": "Method", - "Name": "set_Events", + "Name": "Fail", "Parameters": [ { - "Name": "value", - "Type": "Microsoft.AspNetCore.Authentication.RemoteAuthenticationEvents" + "Name": "failure", + "Type": "System.Exception" } ], - "ReturnType": "System.Void", - "Visibility": "Protected", + "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": [] }, { @@ -2156,6 +2628,21 @@ "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", @@ -2387,97 +2874,164 @@ "GenericParameters": [] }, { - "Name": "Microsoft.AspNetCore.Authentication.HandleRequestResult", + "Name": "Microsoft.AspNetCore.Authentication.SignInAuthenticationHandler", "Visibility": "Public", "Kind": "Class", - "BaseType": "Microsoft.AspNetCore.Authentication.AuthenticateResult", - "ImplementedInterfaces": [], + "Abstract": true, + "BaseType": "Microsoft.AspNetCore.Authentication.SignOutAuthenticationHandler", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Authentication.IAuthenticationSignInHandler" + ], "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", + "Name": "SignInAsync", "Parameters": [ { - "Name": "ticket", - "Type": "Microsoft.AspNetCore.Authentication.AuthenticationTicket" + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" } ], - "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", - "Static": true, + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Authentication.IAuthenticationSignInHandler", "Visibility": "Public", "GenericParameter": [] }, { "Kind": "Method", - "Name": "Fail", + "Name": "HandleSignInAsync", "Parameters": [ { - "Name": "failure", - "Type": "System.Exception" + "Name": "user", + "Type": "System.Security.Claims.ClaimsPrincipal" + }, + { + "Name": "properties", + "Type": "Microsoft.AspNetCore.Authentication.AuthenticationProperties" } ], - "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", - "Static": true, - "Visibility": "Public", + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Protected", "GenericParameter": [] }, { - "Kind": "Method", - "Name": "Fail", + "Kind": "Constructor", + "Name": ".ctor", "Parameters": [ { - "Name": "failureMessage", - "Type": "System.String" + "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" } ], - "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", - "Static": true, "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": "Handle", - "Parameters": [], - "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", - "Static": true, + "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": "SkipHandler", - "Parameters": [], - "ReturnType": "Microsoft.AspNetCore.Authentication.HandleRequestResult", - "Static": true, - "Visibility": "Public", + "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": [], + "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": [] + "GenericParameters": [ + { + "ParameterName": "TOptions", + "ParameterPosition": 0, + "New": true, + "BaseTypeOrInterfaces": [ + "Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions" + ] + } + ] }, { "Name": "Microsoft.AspNetCore.Authentication.SystemClock", @@ -2508,51 +3062,6 @@ ], "GenericParameters": [] }, - { - "Name": "Microsoft.AspNetCore.Authentication.Internal.RequestPathBaseCookieBuilder", - "Visibility": "Public", - "Kind": "Class", - "BaseType": "Microsoft.AspNetCore.Http.CookieBuilder", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_AdditionalPath", - "Parameters": [], - "ReturnType": "System.String", - "Virtual": true, - "Visibility": "Protected", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Build", - "Parameters": [ - { - "Name": "context", - "Type": "Microsoft.AspNetCore.Http.HttpContext" - }, - { - "Name": "expiresFrom", - "Type": "System.DateTimeOffset" - } - ], - "ReturnType": "Microsoft.AspNetCore.Http.CookieOptions", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, { "Name": "Microsoft.AspNetCore.Builder.AuthAppBuilderExtensions", "Visibility": "Public", 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.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/Microsoft.AspNetCore.Authorization.Policy.csproj b/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj index 16e4aa262..d3ad99a51 100644 --- a/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj +++ b/src/Microsoft.AspNetCore.Authorization.Policy/Microsoft.AspNetCore.Authorization.Policy.csproj @@ -2,8 +2,8 @@ ASP.NET Core authorization policy helper classes. - netstandard2.0 - $(NoWarn);CS1591 + netcoreapp3.0 + $(NoWarn);CS1591;NU1605 true aspnetcore;authorization @@ -14,6 +14,8 @@ + + diff --git a/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json b/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json index 0bad3ed62..e8708538d 100644 --- a/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Authorization.Policy/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Authorization.Policy, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Authorization.Policy, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.AspNetCore.Authorization.Policy.IPolicyEvaluator", diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs index 6899913af..f0c5527d2 100644 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationOptions.cs @@ -20,13 +20,25 @@ public class AuthorizationOptions public bool InvokeHandlersAfterFailure { get; set; } = true; /// - /// Gets or sets the default authorization policy. + /// Gets or sets the default authorization policy. Defaults to require authenticated users. /// /// - /// The default policy is to require any authenticated user. + /// 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 36e0ca7c3..b3b54f300 100644 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicy.cs +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicy.cs @@ -120,46 +120,74 @@ public static async Task CombineAsync(IAuthorizationPolicyP throw new ArgumentNullException(nameof(authorizeData)); } - var policyBuilder = new AuthorizationPolicyBuilder(); - var any = false; - foreach (var authorizeDatum in authorizeData) + // Avoid allocating enumerator if the data is known to be empty + var skipEnumeratingData = false; + if (authorizeData is IList dataList) { - any = true; - var useDefaultPolicy = true; - if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy)) + skipEnumeratingData = dataList.Count == 0; + } + + AuthorizationPolicyBuilder policyBuilder = null; + if (!skipEnumeratingData) + { + foreach (var authorizeDatum in authorizeData) { - var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy); - if (policy == null) + if (policyBuilder == null) { - throw new InvalidOperationException(Resources.FormatException_AuthorizationPolicyNotFound(authorizeDatum.Policy)); + policyBuilder = new AuthorizationPolicyBuilder(); } - policyBuilder.Combine(policy); - useDefaultPolicy = false; - } - var rolesSplit = authorizeDatum.Roles?.Split(','); - if (rolesSplit != null && rolesSplit.Any()) - { - var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()); - policyBuilder.RequireRole(trimmedRolesSplit); - useDefaultPolicy = false; - } - var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(','); - if (authTypesSplit != null && authTypesSplit.Any()) - { - foreach (var authType in authTypesSplit) + + 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(await policyProvider.GetDefaultPolicyAsync()); + 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 37335df8f..2f483e37c 100644 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.cs +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.cs @@ -96,16 +96,16 @@ public AuthorizationPolicyBuilder Combine(AuthorizationPolicy policy) /// to the current instance. /// /// The claim type required. - /// Values the claim must process one or more of for evaluation to succeed. + /// 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[] requiredValues) + 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); } /// @@ -113,16 +113,16 @@ public AuthorizationPolicyBuilder RequireClaim(string claimType, params string[] /// to the current instance. /// /// The claim type required. - /// Values the claim must process one or more of for evaluation to succeed. + /// 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 requiredValues) + 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; } @@ -147,7 +147,7 @@ public AuthorizationPolicyBuilder RequireClaim(string claimType) /// Adds a /// to the current instance. /// - /// The roles required. + /// The allowed roles. /// A reference to this instance after the operation has completed. public AuthorizationPolicyBuilder RequireRole(params string[] roles) { @@ -163,7 +163,7 @@ public AuthorizationPolicyBuilder RequireRole(params string[] roles) /// Adds a /// to the current instance. /// - /// The roles required. + /// The allowed roles. /// A reference to this instance after the operation has completed. public AuthorizationPolicyBuilder RequireRole(IEnumerable roles) { @@ -247,4 +247,4 @@ public AuthorizationPolicy Build() return new AuthorizationPolicy(Requirements, AuthenticationSchemes.Distinct()); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs b/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs index 63bfa30d4..b1f0f8bae 100644 --- a/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs +++ b/src/Microsoft.AspNetCore.Authorization/AuthorizeAttribute.cs @@ -39,15 +39,5 @@ public AuthorizeAttribute(string policy) /// Gets or sets a comma delimited list of schemes from which user information is constructed. /// public string AuthenticationSchemes { get; set; } - - /// - /// Gets or sets a comma delimited list of schemes from which user information is constructed. - /// - [Obsolete("Use AuthenticationSchemes instead.", error: false)] - public string ActiveAuthenticationSchemes - { - get => AuthenticationSchemes; - set => AuthenticationSchemes = value; - } } } diff --git a/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs index 0e4329dcc..f5e665273 100644 --- a/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs +++ b/src/Microsoft.AspNetCore.Authorization/DefaultAuthorizationPolicyProvider.cs @@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Authorization public class DefaultAuthorizationPolicyProvider : IAuthorizationPolicyProvider { private readonly AuthorizationOptions _options; + private Task _cachedDefaultPolicy; + private Task _cachedRequiredPolicy; /// /// Creates a new instance of . @@ -35,7 +37,26 @@ public DefaultAuthorizationPolicyProvider(IOptions options /// The default authorization policy. public Task GetDefaultPolicyAsync() { - return Task.FromResult(_options.DefaultPolicy); + 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; } /// diff --git a/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs b/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs index 9e9d0f468..4560d11e0 100644 --- a/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs +++ b/src/Microsoft.AspNetCore.Authorization/IAuthorizationPolicyProvider.cs @@ -22,5 +22,11 @@ public interface IAuthorizationPolicyProvider /// /// The default authorization policy. Task GetDefaultPolicyAsync(); + + /// + /// Gets the required authorization policy. + /// + /// The required authorization policy. + Task GetRequiredPolicyAsync(); } } diff --git a/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj b/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj index ac4aa6c32..9192ac8cf 100644 --- a/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj +++ b/src/Microsoft.AspNetCore.Authorization/Microsoft.AspNetCore.Authorization.csproj @@ -5,7 +5,7 @@ Commonly used types: Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute Microsoft.AspNetCore.Authorization.AuthorizeAttribute - netstandard2.0 + netcoreapp3.0 $(NoWarn);CS1591 true aspnetcore;authorization diff --git a/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json b/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json index 6108db6d2..9910c93f6 100644 --- a/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.Authorization/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.Authorization, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.Authorization, Version=2.0.3.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.Extensions.DependencyInjection.AuthorizationServiceCollectionExtensions", diff --git a/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs b/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs index b99fed2c3..1a810b7d5 100644 --- a/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs +++ b/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs @@ -1,10 +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. +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.CookiePolicy @@ -12,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; } @@ -26,7 +36,7 @@ public CookiePolicyMiddleware( public Task Invoke(HttpContext context) { var feature = context.Features.Get() ?? new ResponseCookiesFeature(context.Features); - var wrapper = new ResponseCookiesWrapper(context, Options, feature); + var wrapper = new ResponseCookiesWrapper(context, Options, feature, _logger); context.Features.Set(new CookiesWrapperFeature(wrapper)); context.Features.Set(wrapper); 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 index 1a42b04dd..81760d50a 100644 --- a/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.csproj +++ b/src/Microsoft.AspNetCore.CookiePolicy/Microsoft.AspNetCore.CookiePolicy.csproj @@ -2,7 +2,7 @@ ASP.NET Core cookie policy classes to control the behavior of cookies. - netstandard2.0 + netcoreapp3.0 $(NoWarn);CS1591 true aspnetcore @@ -10,6 +10,7 @@ + diff --git a/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs b/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs index e05cc9466..126c4d7bd 100644 --- a/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs +++ b/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs @@ -5,21 +5,23 @@ 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) + public ResponseCookiesWrapper(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature, ILogger logger) { Context = context; Feature = feature; Options = options; + _logger = logger; } private HttpContext Context { get; } @@ -38,6 +40,7 @@ public bool IsConsentNeeded { _isConsentNeeded = Options.CheckConsentNeeded == null ? false : Options.CheckConsentNeeded(Context); + _logger.NeedsConsent(_isConsentNeeded.Value); } return _isConsentNeeded.Value; @@ -52,6 +55,7 @@ public bool HasConsent { var cookie = Context.Request.Cookies[Options.ConsentCookie.Name]; _hasConsent = string.Equals(cookie, ConsentValue, StringComparison.Ordinal); + _logger.HasConsent(_hasConsent.Value); } return _hasConsent.Value; @@ -67,6 +71,7 @@ public void GrantConsent() 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; } @@ -78,6 +83,7 @@ public void WithdrawConsent() 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; } @@ -137,12 +143,16 @@ public void Append(string key, string value, CookieOptions 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(options); + ApplyPolicy(key, options); if (Options.OnAppendCookie != null) { var context = new AppendCookieContext(Context, options, key, value) @@ -182,7 +192,7 @@ public void Delete(string key, CookieOptions options) // Assume you can always delete cookies unless directly overridden in the user event. var issueCookie = true; - ApplyPolicy(options); + ApplyPolicy(key, options); if (Options.OnDeleteCookie != null) { var context = new DeleteCookieContext(Context, options, key) @@ -201,17 +211,30 @@ public void Delete(string key, CookieOptions options) { Cookies.Delete(key, options); } + else + { + _logger.DeleteCookieSuppressed(key); + } } - private void ApplyPolicy(CookieOptions options) + private void ApplyPolicy(string key, CookieOptions options) { switch (Options.Secure) { case CookieSecurePolicy.Always: - options.Secure = true; + if (!options.Secure) + { + options.Secure = true; + _logger.CookieUpgradedToSecure(key); + } break; case CookieSecurePolicy.SameAsRequest: - options.Secure = Context.Request.IsHttps; + // Never downgrade a cookie + if (Context.Request.IsHttps && !options.Secure) + { + options.Secure = true; + _logger.CookieUpgradedToSecure(key); + } break; case CookieSecurePolicy.None: break; @@ -226,10 +249,15 @@ private void ApplyPolicy(CookieOptions options) if (options.SameSite == SameSiteMode.None) { options.SameSite = SameSiteMode.Lax; + _logger.CookieSameSiteUpgraded(key, "lax"); } break; case SameSiteMode.Strict: - options.SameSite = 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()}"); @@ -237,7 +265,11 @@ private void ApplyPolicy(CookieOptions options) switch (Options.HttpOnly) { case HttpOnlyPolicy.Always: - options.HttpOnly = true; + if (!options.HttpOnly) + { + options.HttpOnly = true; + _logger.CookieUpgradedToHttpOnly(key); + } break; case HttpOnlyPolicy.None: break; diff --git a/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json b/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json index 97da7ea5c..01a16c57a 100644 --- a/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.CookiePolicy/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.CookiePolicy, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.CookiePolicy, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.AspNetCore.Builder.CookiePolicyAppBuilderExtensions", @@ -116,6 +116,48 @@ "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", @@ -232,6 +274,43 @@ "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", @@ -299,6 +378,26 @@ "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", @@ -361,6 +460,43 @@ "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", diff --git a/src/Microsoft.Owin.Security.Interop/baseline.netframework.json b/src/Microsoft.Owin.Security.Interop/baseline.netframework.json index a55bf8f33..bfc0c0076 100644 --- a/src/Microsoft.Owin.Security.Interop/baseline.netframework.json +++ b/src/Microsoft.Owin.Security.Interop/baseline.netframework.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.Owin.Security.Interop, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.Owin.Security.Interop, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.Owin.Security.Interop.AspNetTicketDataFormat", diff --git a/test/Directory.Build.props b/test/Directory.Build.props index b842a4831..c42d54465 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,13 +1,6 @@ - + - - netcoreapp2.1 - $(DeveloperBuildTestTfms) - $(StandardTestTfms);netcoreapp2.0 - $(StandardTestTfms);net461 - - diff --git a/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs index 541e1edf2..b083e9d76 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/ClaimActionTests.cs @@ -51,5 +51,62 @@ public void CanMapArrayValueUserDataToClaims() 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/CookieTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/CookieTests.cs index 789f5ede9..107fc5db1 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/CookieTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/CookieTests.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 System.Diagnostics; using System.Linq; using System.Net; using System.Net.Http; @@ -22,21 +21,16 @@ namespace Microsoft.AspNetCore.Authentication.Cookies { - public class CookieTests + public class CookieTests : SharedAuthenticationTests { private TestClock _clock = new TestClock(); - [Fact] - public async Task VerifySchemeDefaults() + protected override string DefaultScheme => CookieAuthenticationDefaults.AuthenticationScheme; + protected override Type HandlerType => typeof(CookieAuthenticationHandler); + + protected override void RegisterAuth(AuthenticationBuilder services, Action configure) { - var services = new ServiceCollection(); - services.AddAuthentication().AddCookie(); - var sp = services.BuildServiceProvider(); - var schemeProvider = sp.GetRequiredService(); - var scheme = await schemeProvider.GetSchemeAsync(CookieAuthenticationDefaults.AuthenticationScheme); - Assert.NotNull(scheme); - Assert.Equal("CookieAuthenticationHandler", scheme.HandlerType.Name); - Assert.Null(scheme.DisplayName); + services.AddCookie(configure); } [Fact] @@ -105,8 +99,10 @@ public async Task ProtectedCustomRequestShouldRedirectToCustomRedirectUri() private Task SignInAsAlice(HttpContext context) { + var user = new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")); + user.AddClaim(new Claim("marker", "true")); return context.SignInAsync("Cookies", - new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), + new ClaimsPrincipal(user), new AuthenticationProperties()); } @@ -488,6 +484,80 @@ public async Task CookieCanBeRenewedByValidator() 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() { @@ -532,6 +602,61 @@ public async Task CookieCanBeRenewedByValidatorWithSlidingExpiry() 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() { @@ -704,6 +829,51 @@ public async Task CookieIsRenewedWithSlidingExpiration() Assert.Equal("Alice", FindClaimValue(transaction5, ClaimTypes.Name)); } + [Fact] + public async Task CookieIsRenewedWithSlidingExpirationWithoutTransformations() + { + var server = CreateServer(o => + { + o.ExpireTimeSpan = TimeSpan.FromMinutes(10); + o.SlidingExpiration = true; + o.Events.OnValidatePrincipal = c => + { + // https://github.com/aspnet/Security/issues/1607 + // On sliding refresh the transformed principal should not be serialized into the cookie, only the original principal. + Assert.Single(c.Principal.Identities); + Assert.True(c.Principal.Identities.First().HasClaim("marker", "true")); + return Task.CompletedTask; + }; + }, + SignInAsAlice, + claimsTransform: true); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction2.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(4)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.Null(transaction3.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(4)); + + // transaction4 should arrive with a new SetCookie value + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + Assert.NotNull(transaction4.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); + + _clock.Add(TimeSpan.FromMinutes(4)); + + var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); + Assert.Null(transaction5.SetCookie); + Assert.Equal("Alice", FindClaimValue(transaction5, ClaimTypes.Name)); + } + [Fact] public async Task CookieUsesPathBaseByDefault() { @@ -1218,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); @@ -1233,6 +1413,13 @@ 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"); @@ -1248,7 +1435,10 @@ private TestServer CreateServer(Action configureOpt { s.AddSingleton(_clock); s.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(configureOptions); - s.AddSingleton(); + if (claimsTransform) + { + s.AddSingleton(); + } }, testpath, baseAddress); private static TestServer CreateServerWithServices(Action configureServices, Func testpath = null, Uri baseAddress = null) diff --git a/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs index 2314b6b3c..964af2ce7 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/FacebookTests.cs @@ -1,13 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -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; @@ -18,81 +11,38 @@ 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 + public class FacebookTests : RemoteAuthenticationTests { - [Fact] - public async Task VerifySignInSchemeCannotBeSetToSelf() - { - var server = CreateServer( - app => { }, - services => services.AddAuthentication().AddFacebook(o => { - o.AppId = "whatever"; - o.AppSecret = "whatever"; - o.SignInScheme = FacebookDefaults.AuthenticationScheme; - }), - context => - { - // Gross - context.ChallengeAsync("Facebook").GetAwaiter().GetResult(); - return true; - }); - var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); - Assert.Contains("cannot be set to itself", error.Message); - } + protected override string DefaultScheme => FacebookDefaults.AuthenticationScheme; + protected override Type HandlerType => typeof(FacebookHandler); + protected override bool SupportsSignIn { get => false; } + protected override bool SupportsSignOut { get => false; } - [Fact] - public async Task VerifySignInSchemeCannotBeSetToSelfUsingDefaultScheme() + protected override void RegisterAuth(AuthenticationBuilder services, Action configure) { - var server = CreateServer( - app => { }, - services => services.AddAuthentication(o => o.DefaultScheme = FacebookDefaults.AuthenticationScheme).AddFacebook(o => { - o.AppId = "whatever"; - o.AppSecret = "whatever"; - }), - context => - { - // Gross - context.ChallengeAsync("Facebook").GetAwaiter().GetResult(); - return 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 = CreateServer( - app => { }, - services => services.AddAuthentication(o => o.DefaultSignInScheme = FacebookDefaults.AuthenticationScheme).AddFacebook(o => { - o.AppId = "whatever"; - o.AppSecret = "whatever"; - }), - context => - { - // Gross - context.ChallengeAsync("Facebook").GetAwaiter().GetResult(); - return true; - }); - var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); - Assert.Contains("cannot be set to itself", error.Message); + services.AddFacebook(o => + { + ConfigureDefaults(o); + configure.Invoke(o); + }); } - - [Fact] - public async Task VerifySchemeDefaults() + + protected override void ConfigureDefaults(FacebookOptions o) { - var services = new ServiceCollection(); - services.AddAuthentication().AddFacebook(); - var sp = services.BuildServiceProvider(); - var schemeProvider = sp.GetRequiredService(); - var scheme = await schemeProvider.GetSchemeAsync(FacebookDefaults.AuthenticationScheme); - Assert.NotNull(scheme); - Assert.Equal("FacebookHandler", scheme.HandlerType.Name); - Assert.Equal(FacebookDefaults.AuthenticationScheme, scheme.DisplayName); + o.AppId = "whatever"; + o.AppSecret = "whatever"; + o.SignInScheme = "auth1"; } [Fact] @@ -101,10 +51,9 @@ public async Task ThrowsIfAppIdMissing() var server = CreateServer( app => { }, services => services.AddAuthentication().AddFacebook(o => o.SignInScheme = "Whatever"), - context => + async context => { - // REVIEW: Gross. - Assert.Throws("AppId", () => context.ChallengeAsync("Facebook").GetAwaiter().GetResult()); + await Assert.ThrowsAsync("AppId", () => context.ChallengeAsync("Facebook")); return true; }); var transaction = await server.SendAsync("http://example.com/challenge"); @@ -117,10 +66,9 @@ public async Task ThrowsIfAppSecretMissing() var server = CreateServer( app => { }, services => services.AddAuthentication().AddFacebook(o => o.AppId = "Whatever"), - context => + async context => { - // REVIEW: Gross. - Assert.Throws("AppSecret", () => context.ChallengeAsync("Facebook").GetAwaiter().GetResult()); + await Assert.ThrowsAsync("AppSecret", () => context.ChallengeAsync("Facebook")); return true; }); var transaction = await server.SendAsync("http://example.com/challenge"); @@ -153,10 +101,9 @@ public async Task ChallengeWillTriggerApplyRedirectEvent() }; }); }, - context => + async context => { - // REVIEW: Gross. - context.ChallengeAsync("Facebook").GetAwaiter().GetResult(); + await context.ChallengeAsync("Facebook"); return true; }); var transaction = await server.SendAsync("http://example.com/challenge"); @@ -165,6 +112,97 @@ public async Task ChallengeWillTriggerApplyRedirectEvent() 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() { @@ -188,7 +226,7 @@ public async Task NestedMapWillNotAffectRedirect() 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.6/dialog/oauth", location); + 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); @@ -220,10 +258,10 @@ public async Task MapWillNotAffectRedirect() 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.6/dialog/oauth", location); + 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("redirect_uri=" + UrlEncoder.Default.Encode("http://example.com/signin-facebook"), location); Assert.Contains("scope=", location); Assert.Contains("state=", location); } @@ -246,16 +284,15 @@ public async Task ChallengeWillTriggerRedirection() o.AppSecret = "Test App Secret"; }); }, - context => + async context => { - // REVIEW: gross - context.ChallengeAsync("Facebook").GetAwaiter().GetResult(); + await context.ChallengeAsync("Facebook"); return true; }); var transaction = await server.SendAsync("http://example.com/challenge"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var location = transaction.Response.Headers.Location.AbsoluteUri; - Assert.Contains("https://www.facebook.com/v2.6/dialog/oauth", location); + 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); @@ -275,7 +312,7 @@ public async Task CustomUserInfoEndpointHasValidGraphQuery() { services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie() - .AddFacebook(o => + .AddFacebook(o => { o.AppId = "Test App Id"; o.AppSecret = "Test App Secret"; @@ -331,7 +368,7 @@ public async Task CustomUserInfoEndpointHasValidGraphQuery() Assert.Contains("&access_token=", finalUserInfoEndpoint); } - private static TestServer CreateServer(Action configure, Action configureServices, Func handler) + private static TestServer CreateServer(Action configure, Action configureServices, Func> handler) { var builder = new WebHostBuilder() .Configure(app => @@ -339,7 +376,7 @@ private static TestServer CreateServer(Action configure, Ac configure?.Invoke(app); app.Use(async (context, next) => { - if (handler == null || !handler(context)) + if (handler == null || !await handler(context)) { await next(); } diff --git a/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs index 944a4827c..ce158eaf2 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/GoogleTests.cs @@ -1,53 +1,49 @@ // 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.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 + public class GoogleTests : RemoteAuthenticationTests { - [Fact] - public async Task VerifySignInSchemeCannotBeSetToSelf() + 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) { - var server = CreateServer(o => + services.AddGoogle(o => { - o.ClientId = "Test Id"; - o.ClientSecret = "Test Secret"; - o.SignInScheme = GoogleDefaults.AuthenticationScheme; + ConfigureDefaults(o); + configure.Invoke(o); }); - var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); - Assert.Contains("cannot be set to itself", error.Message); } - [Fact] - public async Task VerifySchemeDefaults() + protected override void ConfigureDefaults(GoogleOptions o) { - var services = new ServiceCollection(); - services.AddAuthentication().AddGoogle(); - var sp = services.BuildServiceProvider(); - var schemeProvider = sp.GetRequiredService(); - var scheme = await schemeProvider.GetSchemeAsync(GoogleDefaults.AuthenticationScheme); - Assert.NotNull(scheme); - Assert.Equal("GoogleHandler", scheme.HandlerType.Name); - Assert.Equal(GoogleDefaults.AuthenticationScheme, scheme.DisplayName); + o.ClientId = "whatever"; + o.ClientSecret = "whatever"; + o.SignInScheme = "auth1"; } [Fact] @@ -61,7 +57,7 @@ public async Task ChallengeWillTriggerRedirection() 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("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); @@ -149,43 +145,160 @@ public async Task ChallengeWillSetDefaultScope() } [Fact] - public async Task ChallengeWillUseAuthenticationPropertiesAsParameters() + 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"; - //AutomaticChallenge = true + o.StateDataFormat = stateFormat; }, context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge2")) { - var req = context.Request; - var res = context.Response; - if (req.Path == new PathString("/challenge2")) + return context.ChallengeAsync("Google", new GoogleChallengeProperties { - 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" } - })); - } + 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); - }); + 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("prompt=consent", query); - Assert.Contains("include_granted_scopes=false", query); - Assert.Contains("login_hint=" + UrlEncoder.Default.Encode("test@example.com"), query); + + // 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] @@ -244,6 +357,70 @@ public async Task ReplyPathWithoutStateQueryStringWillBeRejected() 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)] @@ -265,7 +442,7 @@ public async Task ReplyPathWithErrorFails(bool redirect) } : new OAuthEvents(); }); var sendTask = server.SendAsync("https://example.com/signin-google?error=OMG&error_description=SoBad&error_uri=foobar&state=protected_state", - ".AspNetCore.Correlation.Google.corrilationId=N"); + ".AspNetCore.Correlation.Google.correlationId=N"); if (redirect) { var transaction = await sendTask; @@ -1061,18 +1238,13 @@ private static TestServer CreateServer(Action configureOptions, F .ConfigureServices(services => { services.AddTransient(); - services.AddAuthentication("Auth") - .AddVirtualScheme("Auth", "Auth", o => - { - o.Default = TestExtensions.CookieAuthenticationScheme; - o.Challenge = GoogleDefaults.AuthenticationScheme; - }) - .AddCookie(TestExtensions.CookieAuthenticationScheme) + services.AddAuthentication(TestExtensions.CookieAuthenticationScheme) + .AddCookie(TestExtensions.CookieAuthenticationScheme, o => o.ForwardChallenge = GoogleDefaults.AuthenticationScheme) .AddGoogle(configureOptions) .AddFacebook(o => { - o.AppId = "Test AppId"; - o.AppSecret = "Test AppSecrent"; + o.ClientId = "Test ClientId"; + o.ClientSecret = "Test AppSecrent"; }); }); return new TestServer(builder); @@ -1097,7 +1269,7 @@ public AuthenticationProperties Unprotect(string protectedText) Assert.Equal("protected_state", protectedText); var properties = new AuthenticationProperties(new Dictionary() { - { ".xsrf", "corrilationId" }, + { ".xsrf", "correlationId" }, { "testkey", "testvalue" } }); properties.RedirectUri = "http://testhost/redirect"; diff --git a/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs index 97adb2105..ad77803cd 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/JwtBearerTests.cs @@ -1,8 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using 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.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Net; using System.Net.Http; @@ -11,51 +17,102 @@ using System.Text; using System.Threading.Tasks; using System.Xml.Linq; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.TestHost; -using Microsoft.AspNetCore.Testing.xunit; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; using Xunit; namespace Microsoft.AspNetCore.Authentication.JwtBearer { - public class JwtBearerTests + public class JwtBearerTests : SharedAuthenticationTests { - [Fact] - public async Task VerifySchemeDefaults() + 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) { - var services = new ServiceCollection(); - services.AddAuthentication().AddJwtBearer(); - var sp = services.BuildServiceProvider(); - var schemeProvider = sp.GetRequiredService(); - var scheme = await schemeProvider.GetSchemeAsync(JwtBearerDefaults.AuthenticationScheme); - Assert.NotNull(scheme); - Assert.Equal("JwtBearerHandler", scheme.HandlerType.Name); - Assert.Null(scheme.DisplayName); } - [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 + [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.Authority = "https://login.windows.net/tushartest.onmicrosoft.com"; - o.Audience = "https://TusharTest.onmicrosoft.com/TodoListService-ManualJwt"; - o.TokenValidationParameters.ValidateLifetime = false; + o.TokenValidationParameters = new TokenValidationParameters() + { + ValidIssuer = "issuer.contoso.com", + ValidAudience = "audience.contoso.com", + IssuerSigningKey = key, + }; }); - var newBearerToken = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImtyaU1QZG1Cdng2OHNrVDgtbVBBQjNCc2VlQSJ9.eyJhdWQiOiJodHRwczovL1R1c2hhclRlc3Qub25taWNyb3NvZnQuY29tL1RvZG9MaXN0U2VydmljZS1NYW51YWxKd3QiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9hZmJlY2UwMy1hZWFhLTRmM2YtODVlNy1jZTA4ZGQyMGNlNTAvIiwiaWF0IjoxNDE4MzMwNjE0LCJuYmYiOjE0MTgzMzA2MTQsImV4cCI6MTQxODMzNDUxNCwidmVyIjoiMS4wIiwidGlkIjoiYWZiZWNlMDMtYWVhYS00ZjNmLTg1ZTctY2UwOGRkMjBjZTUwIiwiYW1yIjpbInB3ZCJdLCJvaWQiOiI1Mzk3OTdjMi00MDE5LTQ2NTktOWRiNS03MmM0Yzc3NzhhMzMiLCJ1cG4iOiJWaWN0b3JAVHVzaGFyVGVzdC5vbm1pY3Jvc29mdC5jb20iLCJ1bmlxdWVfbmFtZSI6IlZpY3RvckBUdXNoYXJUZXN0Lm9ubWljcm9zb2Z0LmNvbSIsInN1YiI6IkQyMm9aMW9VTzEzTUFiQXZrdnFyd2REVE80WXZJdjlzMV9GNWlVOVUwYnciLCJmYW1pbHlfbmFtZSI6Ikd1cHRhIiwiZ2l2ZW5fbmFtZSI6IlZpY3RvciIsImFwcGlkIjoiNjEzYjVhZjgtZjJjMy00MWI2LWExZGMtNDE2Yzk3ODAzMGI3IiwiYXBwaWRhY3IiOiIwIiwic2NwIjoidXNlcl9pbXBlcnNvbmF0aW9uIiwiYWNyIjoiMSJ9.N_Kw1EhoVGrHbE6hOcm7ERdZ7paBQiNdObvp2c6T6n5CE8p0fZqmUd-ya_EqwElcD6SiKSiP7gj0gpNUnOJcBl_H2X8GseaeeMxBrZdsnDL8qecc6_ygHruwlPltnLTdka67s1Ow4fDSHaqhVTEk6lzGmNEcbNAyb0CxQxU6o7Fh0yHRiWoLsT8yqYk8nKzsHXfZBNby4aRo3_hXaa4i0SZLYfDGGYPdttG4vT_u54QGGd4Wzbonv2gjDlllOVGOwoJS6kfl1h8mk0qxdiIaT_ChbDWgkWvTB7bTvBE-EgHgV0XmAo0WtJeSxgjsG3KhhEPsONmqrSjhIUV4IVnF2w"; + 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() { @@ -728,6 +785,11 @@ private static TestServer CreateServer(Action options = null, 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 diff --git a/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj b/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj index 57fed96c0..c7a380868 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj +++ b/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj @@ -1,9 +1,34 @@  - $(StandardTestTfms) + netcoreapp3.0 + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + @@ -12,6 +37,7 @@ + diff --git a/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs index b2854e344..26a5484c8 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/MicrosoftAccountTests.cs @@ -1,14 +1,5 @@ // Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Security.Claims; -using System.Text; -using System.Text.Encodings.Web; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.MicrosoftAccount; using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Builder; @@ -16,41 +7,42 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; using Newtonsoft.Json; +using 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 + public class MicrosoftAccountTests : RemoteAuthenticationTests { - [Fact] - public async Task VerifySignInSchemeCannotBeSetToSelf() + 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) { - var server = CreateServer(o => + services.AddMicrosoftAccount(o => { - o.ClientId = "Test Id"; - o.ClientSecret = "Test Secret"; - o.SignInScheme = MicrosoftAccountDefaults.AuthenticationScheme; + ConfigureDefaults(o); + configure.Invoke(o); }); - var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); - Assert.Contains("cannot be set to itself", error.Message); } - [Fact] - public async Task VerifySchemeDefaults() + protected override void ConfigureDefaults(MicrosoftAccountOptions o) { - var services = new ServiceCollection(); - services.AddAuthentication().AddMicrosoftAccount(); - var sp = services.BuildServiceProvider(); - var schemeProvider = sp.GetRequiredService(); - var scheme = await schemeProvider.GetSchemeAsync(MicrosoftAccountDefaults.AuthenticationScheme); - Assert.NotNull(scheme); - Assert.Equal("MicrosoftAccountHandler", scheme.HandlerType.Name); - Assert.Equal(MicrosoftAccountDefaults.AuthenticationScheme, scheme.DisplayName); + o.ClientId = "whatever"; + o.ClientSecret = "whatever"; + o.SignInScheme = "auth1"; } [Fact] @@ -130,6 +122,57 @@ public async Task ChallengeWillTriggerRedirection() 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() { @@ -213,6 +256,18 @@ private static TestServer CreateServer(Action configure { 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); 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 index 65d865b94..838798cea 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/OAuthTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/OAuthTests.cs @@ -1,51 +1,34 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; 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 Microsoft.Net.Http.Headers; +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; using Xunit; namespace Microsoft.AspNetCore.Authentication.OAuth { - public class OAuthTests + public class OAuthTests : RemoteAuthenticationTests { - [Fact] - public async Task VerifySignInSchemeCannotBeSetToSelf() - { - var server = CreateServer( - services => services.AddAuthentication().AddOAuth("weeblie", o => - { - o.SignInScheme = "weeblie"; - o.ClientId = "whatever"; - o.ClientSecret = "whatever"; - o.CallbackPath = "/whatever"; - o.AuthorizationEndpoint = "/whatever"; - o.TokenEndpoint = "/whatever"; - })); - var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/")); - Assert.Contains("cannot be set to itself", error.Message); - } + protected override string DefaultScheme => OAuthDefaults.DisplayName; + protected override Type HandlerType => typeof(OAuthHandler); + protected override bool SupportsSignIn { get => false; } + protected override bool SupportsSignOut { get => false; } - [Fact] - public async Task VerifySchemeDefaults() + protected override void RegisterAuth(AuthenticationBuilder services, Action configure) { - var services = new ServiceCollection(); - services.AddAuthentication().AddOAuth("oauth", o => { }); - var sp = services.BuildServiceProvider(); - var schemeProvider = sp.GetRequiredService(); - var scheme = await schemeProvider.GetSchemeAsync("oauth"); - Assert.NotNull(scheme); - Assert.Equal("OAuthHandler`1", scheme.HandlerType.Name); - Assert.Equal(OAuthDefaults.DisplayName, scheme.DisplayName); + services.AddOAuth(DefaultScheme, o => + { + ConfigureDefaults(o); + configure.Invoke(o); + }); } [Fact] @@ -131,12 +114,7 @@ public async Task RedirectToIdentityProvider_SetsCorrelationIdCookiePath_ToCallB "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"; + ConfigureDefaults(opt); }), async ctx => { @@ -162,12 +140,7 @@ public async Task RedirectToAuthorizeEndpoint_CorrelationIdCookieOptions_CanBeOv "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"; + ConfigureDefaults(opt); opt.CorrelationCookie.Path = "/"; }), async ctx => @@ -186,6 +159,193 @@ public async Task RedirectToAuthorizeEndpoint_CorrelationIdCookieOptions_CanBeOv 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() { @@ -205,7 +365,7 @@ public async Task RemoteAuthenticationFailed_OAuthError_IncludesProperties() { OnRemoteFailure = context => { - Assert.Contains("declined", context.Failure.Message); + Assert.Contains("custom_error", context.Failure.Message); Assert.Equal("testvalue", context.Properties.Items["testkey"]); context.Response.StatusCode = StatusCodes.Status406NotAcceptable; context.HandleResponse(); @@ -214,8 +374,8 @@ public async Task RemoteAuthenticationFailed_OAuthError_IncludesProperties() }; })); - var transaction = await server.SendAsync("https://www.example.com/oauth-callback?error=declined&state=protected_state", - ".AspNetCore.Correlation.Weblie.corrilationId=N"); + 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); @@ -258,7 +418,7 @@ public AuthenticationProperties Unprotect(string protectedText) Assert.Equal("protected_state", protectedText); var properties = new AuthenticationProperties(new Dictionary() { - { ".xsrf", "corrilationId" }, + { ".xsrf", "correlationId" }, { "testkey", "testvalue" } }); properties.RedirectUri = "http://testhost/redirect"; diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs index 7ab81c9dd..cbafc4622 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs @@ -51,14 +51,14 @@ public async Task ChallengeRedirectIsIssuedCorrectly() [Fact] public async Task AuthorizationRequestDoesNotIncludeTelemetryParametersWhenDisabled() { - var setting = new TestSettings(opt => + var settings = new TestSettings(opt => { opt.ClientId = "Test Id"; opt.Authority = TestServerBuilder.DefaultAuthority; opt.DisableTelemetry = true; }); - var server = setting.CreateTestServer(); + var server = settings.CreateTestServer(); var transaction = await server.SendAsync(ChallengeEndpoint); var res = transaction.Response; @@ -425,6 +425,7 @@ public async Task Challenge_WithDefaultMaxAge_HasExpectedMaxAgeParam() var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); settings.ValidateChallengeRedirect( res.Headers.Location, OpenIdConnectParameterNames.MaxAge); @@ -446,9 +447,170 @@ public async Task Challenge_WithSpecificMaxAge_HasExpectedMaxAgeParam() 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 index 69ba75829..ed368c1ef 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs @@ -3,10 +3,13 @@ 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; @@ -15,6 +18,421 @@ 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() { diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs index 7530b00c3..090bf3dec 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectEventTests.cs @@ -783,6 +783,52 @@ public async Task OnAuthenticationFailed_HandledWithTicket_SkipToTicketReceived( 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() { @@ -1099,6 +1145,9 @@ private class ExpectedOidcEvents : OpenIdConnectEvents 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; } @@ -1168,6 +1217,12 @@ public override Task TicketReceived(TicketReceivedContext context) return base.TicketReceived(context); } + public override Task AccessDenied(AccessDeniedContext context) + { + InvokedAccessDenied = true; + return base.AccessDenied(context); + } + public override Task RemoteFailure(RemoteFailureContext context) { InvokedRemoteFailure = true; @@ -1201,6 +1256,7 @@ public void ValidateExpectations() 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); @@ -1248,7 +1304,7 @@ private TestServer CreateServer(OpenIdConnectEvents events, RequestDelegate appC private Task PostAsync(TestServer server, string path, string form) { var client = server.CreateClient(); - var cookie = ".AspNetCore.Correlation." + OpenIdConnectDefaults.AuthenticationScheme + ".corrilationId=N"; + 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")); @@ -1273,7 +1329,7 @@ public AuthenticationProperties Unprotect(string protectedText) Assert.Equal("protected_state", protectedText); var properties = new AuthenticationProperties(new Dictionary() { - { ".xsrf", "corrilationId" }, + { ".xsrf", "correlationId" }, { OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, "redirect_uri" }, { "testkey", "testvalue" } }); diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs index 509b85e64..eb045b438 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs @@ -4,10 +4,14 @@ 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; @@ -22,6 +26,7 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect internal class TestSettings { private readonly Action _configureOptions; + private OpenIdConnectOptions _options; public TestSettings() : this(configure: null) { @@ -33,6 +38,7 @@ public TestSettings(Action configure) { configure?.Invoke(o); _options = o; + _options.BackchannelHttpHandler = new MockBackchannel(); }; } @@ -189,7 +195,7 @@ private void ValidateParameters( ValidateState(actualValues, errors, htmlEncoded); break; case OpenIdConnectParameterNames.SkuTelemetry: - ValidateSkuTelemetry(actualValues, errors, htmlEncoded); + ValidateSkuTelemetry(actualValues, errors); break; case OpenIdConnectParameterNames.VersionTelemetry: ValidateVersionTelemetry(actualValues, errors, htmlEncoded); @@ -200,14 +206,15 @@ private void ValidateParameters( case OpenIdConnectParameterNames.MaxAge: ValidateMaxAge(actualValues, errors, htmlEncoded); break; + case OpenIdConnectParameterNames.Prompt: + ValidatePrompt(actualValues, errors, htmlEncoded); + break; default: throw new InvalidOperationException($"Unknown parameter \"{paramToValidate}\"."); } } } - OpenIdConnectOptions _options = null; - private void ValidateExpectedAuthority(string absoluteUri, ICollection errors, OpenIdConnectRequestType requestType) { string expectedAuthority; @@ -251,14 +258,13 @@ private void ValidateResource(IDictionary actualParams, ICollect private void ValidateState(IDictionary actualParams, ICollection errors, bool htmlEncoded) => ValidateParameter(OpenIdConnectParameterNames.State, ExpectedState, actualParams, errors, htmlEncoded); - private void ValidateSkuTelemetry(IDictionary actualParams, ICollection errors, bool htmlEncoded) => -#if NETCOREAPP2_0 || NETCOREAPP2_1 - ValidateParameter(OpenIdConnectParameterNames.SkuTelemetry, "ID_NETSTANDARD1_4", actualParams, errors, htmlEncoded); -#elif NET461 - ValidateParameter(OpenIdConnectParameterNames.SkuTelemetry, "ID_NET451", actualParams, errors, htmlEncoded); -#else -#error Invalid target framework. -#endif + private 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); @@ -280,6 +286,9 @@ private void ValidateMaxAge(IDictionary actualQuery, ICollection } } + private void ValidatePrompt(IDictionary actualParams, ICollection errors, bool htmlEncoded) => + ValidateParameter(OpenIdConnectParameterNames.Prompt, _options.Prompt, actualParams, errors, htmlEncoded); + private void ValidateParameter( string parameterName, string expectedValue, @@ -305,5 +314,37 @@ private void ValidateParameter( 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/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/VirtualHandlerTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/PolicyTests.cs similarity index 85% rename from test/Microsoft.AspNetCore.Authentication.Test/VirtualHandlerTests.cs rename to test/Microsoft.AspNetCore.Authentication.Test/PolicyTests.cs index a43478c94..368026beb 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/VirtualHandlerTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/PolicyTests.cs @@ -12,26 +12,26 @@ namespace Microsoft.AspNetCore.Authentication { - public class VirtualHandlerTests + public class PolicyTests { [Fact] public async Task CanDispatch() { var server = CreateServer(services => { - services.AddAuthentication(o => + services.AddLogging().AddAuthentication(o => { o.AddScheme("auth1", "auth1"); o.AddScheme("auth2", "auth2"); o.AddScheme("auth3", "auth3"); }) - .AddVirtualScheme("policy1", "policy1", p => + .AddPolicyScheme("policy1", "policy1", p => { - p.Default = "auth1"; + p.ForwardDefault = "auth1"; }) - .AddVirtualScheme("policy2", "policy2", p => + .AddPolicyScheme("policy2", "policy2", p => { - p.Authenticate = "auth2"; + p.ForwardAuthenticate = "auth2"; }); }); @@ -54,16 +54,15 @@ public async Task CanDispatch() [Fact] public async Task DefaultTargetSelectorWinsOverDefaultTarget() { - var services = new ServiceCollection().AddOptions(); - + var services = new ServiceCollection().AddOptions().AddLogging(); services.AddAuthentication(o => { o.AddScheme("auth1", "auth1"); o.AddScheme("auth2", "auth2"); }) - .AddVirtualScheme("forward", "forward", p => { - p.Default = "auth2"; - p.DefaultSelector = ctx => "auth1"; + .AddPolicyScheme("forward", "forward", p => { + p.ForwardDefault= "auth2"; + p.ForwardDefaultSelector = ctx => "auth1"; }); var handler1 = new TestHandler(); @@ -110,16 +109,15 @@ public async Task DefaultTargetSelectorWinsOverDefaultTarget() [Fact] public async Task NullDefaultTargetSelectorFallsBacktoDefaultTarget() { - var services = new ServiceCollection().AddOptions(); - + var services = new ServiceCollection().AddOptions().AddLogging(); services.AddAuthentication(o => { o.AddScheme("auth1", "auth1"); o.AddScheme("auth2", "auth2"); }) - .AddVirtualScheme("forward", "forward", p => { - p.Default = "auth1"; - p.DefaultSelector = ctx => null; + .AddPolicyScheme("forward", "forward", p => { + p.ForwardDefault= "auth1"; + p.ForwardDefaultSelector = ctx => null; }); var handler1 = new TestHandler(); @@ -166,21 +164,20 @@ public async Task NullDefaultTargetSelectorFallsBacktoDefaultTarget() [Fact] public async Task SpecificTargetAlwaysWinsOverDefaultTarget() { - var services = new ServiceCollection().AddOptions(); - + var services = new ServiceCollection().AddOptions().AddLogging(); services.AddAuthentication(o => { o.AddScheme("auth1", "auth1"); o.AddScheme("auth2", "auth2"); }) - .AddVirtualScheme("forward", "forward", p => { - p.Default = "auth2"; - p.DefaultSelector = ctx => "auth2"; - p.Authenticate = "auth1"; - p.SignIn = "auth1"; - p.SignOut = "auth1"; - p.Forbid = "auth1"; - p.Challenge = "auth1"; + .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(); @@ -227,14 +224,13 @@ public async Task SpecificTargetAlwaysWinsOverDefaultTarget() [Fact] public async Task VirtualSchemeTargetsForwardWithDefaultTarget() { - var services = new ServiceCollection().AddOptions(); - + var services = new ServiceCollection().AddOptions().AddLogging(); services.AddAuthentication(o => { o.AddScheme("auth1", "auth1"); o.AddScheme("auth2", "auth2"); }) - .AddVirtualScheme("forward", "forward", p => p.Default = "auth1"); + .AddPolicyScheme("forward", "forward", p => p.ForwardDefault= "auth1"); var handler1 = new TestHandler(); services.AddSingleton(handler1); @@ -280,18 +276,17 @@ public async Task VirtualSchemeTargetsForwardWithDefaultTarget() [Fact] public async Task VirtualSchemeTargetsOverrideDefaultTarget() { - var services = new ServiceCollection().AddOptions(); - + var services = new ServiceCollection().AddOptions().AddLogging(); services.AddAuthentication(o => { o.AddScheme("auth1", "auth1"); o.AddScheme("auth2", "auth2"); }) - .AddVirtualScheme("forward", "forward", p => + .AddPolicyScheme("forward", "forward", p => { - p.Default = "auth1"; - p.Challenge = "auth2"; - p.SignIn = "auth2"; + p.ForwardDefault= "auth1"; + p.ForwardChallenge = "auth2"; + p.ForwardSignIn = "auth2"; }); var handler1 = new TestHandler(); @@ -346,9 +341,9 @@ public async Task CanDynamicTargetBasedOnQueryString() o.AddScheme("auth2", "auth2"); o.AddScheme("auth3", "auth3"); }) - .AddVirtualScheme("dynamic", "dynamic", p => + .AddPolicyScheme("dynamic", "dynamic", p => { - p.DefaultSelector = c => c.Request.QueryString.Value.Substring(1); + p.ForwardDefaultSelector = c => c.Request.QueryString.Value.Substring(1); }); }); @@ -360,39 +355,6 @@ public async Task CanDynamicTargetBasedOnQueryString() Assert.Equal("auth3", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "auth3")); } - [Fact] - public async Task TargetsDefaultSchemeByDefault() - { - var server = CreateServer(services => - { - services.AddAuthentication(o => - { - o.DefaultScheme = "default"; - o.AddScheme("default", "default"); - }) - .AddVirtualScheme("virtual", "virtual", p => { }); - }); - - var transaction = await server.SendAsync("http://example.com/auth/virtual"); - Assert.Equal("default", transaction.FindClaimValue(ClaimTypes.NameIdentifier, "default")); - } - - [Fact] - public async Task TargetsDefaultSchemeThrowsWithNoDefault() - { - var server = CreateServer(services => - { - services.AddAuthentication(o => - { - o.AddScheme("default", "default"); - }) - .AddVirtualScheme("virtual", "virtual", p => { }); - }); - - var error = await Assert.ThrowsAsync(() => server.SendAsync("http://example.com/auth/virtual")); - Assert.Contains("No authenticationScheme was specified", error.Message); - } - private class TestHandler : IAuthenticationSignInHandler { public AuthenticationScheme Scheme { get; set; } 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/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/TwitterTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/TwitterTests.cs index 2a63757b9..8eb7a5cd7 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/TwitterTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/TwitterTests.cs @@ -1,5 +1,11 @@ // 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; @@ -7,42 +13,31 @@ 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 Microsoft.Net.Http.Headers; using Xunit; namespace Microsoft.AspNetCore.Authentication.Twitter { - public class TwitterTests + public class TwitterTests : RemoteAuthenticationTests { - [Fact] - public async Task VerifySignInSchemeCannotBeSetToSelf() + 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) { - var server = CreateServer(o => + services.AddTwitter(o => { - o.ConsumerKey = "Test Consumer Key"; - o.ConsumerSecret = "Test Consumer Secret"; - o.SignInScheme = TwitterDefaults.AuthenticationScheme; + ConfigureDefaults(o); + configure.Invoke(o); }); - var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); - Assert.Contains("cannot be set to itself", error.Message); } - [Fact] - public async Task VerifySchemeDefaults() + protected override void ConfigureDefaults(TwitterOptions o) { - var services = new ServiceCollection(); - services.AddAuthentication().AddTwitter(); - var sp = services.BuildServiceProvider(); - var schemeProvider = sp.GetRequiredService(); - var scheme = await schemeProvider.GetSchemeAsync(TwitterDefaults.AuthenticationScheme); - Assert.NotNull(scheme); - Assert.Equal("TwitterHandler", scheme.HandlerType.Name); - Assert.Equal(TwitterDefaults.AuthenticationScheme, scheme.DisplayName); + o.ConsumerKey = "whatever"; + o.ConsumerSecret = "whatever"; + o.SignInScheme = "auth1"; } [Fact] @@ -179,6 +174,94 @@ public async Task ChallengeWillTriggerRedirection() 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() { @@ -195,7 +278,7 @@ public async Task BadCallbackCallsRemoteAuthFailedWithState() OnRemoteFailure = context => { Assert.NotNull(context.Failure); - Assert.Equal("The user denied permissions.", context.Failure.Message); + 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; 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.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 3eefb7af7..143be1b9b 100644 --- a/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationPolicyFacts.cs +++ b/test/Microsoft.AspNetCore.Authorization.Test/AuthorizationPolicyFacts.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.AspNetCore.Authroization.Test +namespace Microsoft.AspNetCore.Authorization.Test { public class AuthorizationPolicyFacts { diff --git a/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs b/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs index ef17b9462..230e95c4c 100644 --- a/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs +++ b/test/Microsoft.AspNetCore.Authorization.Test/DefaultAuthorizationServiceTests.cs @@ -1025,6 +1025,11 @@ 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()); @@ -1059,6 +1064,11 @@ 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()); diff --git a/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj b/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj index d4379c3aa..9ff42a070 100644 --- a/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj +++ b/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj @@ -1,7 +1,8 @@  - $(StandardTestTfms) + netcoreapp3.0 + $(NoWarn);NU1605 @@ -11,8 +12,10 @@ + + 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.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj b/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj index 20cd400ce..2f38abff9 100644 --- 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 @@ -1,7 +1,7 @@  - $(StandardTestTfms) + netcoreapp3.0 diff --git a/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs b/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs index 4e35c165f..fffb8cc88 100644 --- a/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs +++ b/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs @@ -579,7 +579,13 @@ public async Task CreateConsentCookieMatchesGrantConsentCookie() Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite); Assert.NotNull(consentCookie.Expires); - Assert.Equal(httpContext.Response.Headers[HeaderNames.SetCookie], httpContext.Response.Headers["ManualCookie"]); + 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] @@ -626,7 +632,13 @@ public async Task CreateConsentCookieAppliesPolicy() Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite); Assert.NotNull(consentCookie.Expires); - Assert.Equal(httpContext.Response.Headers[HeaderNames.SetCookie], httpContext.Response.Headers["ManualCookie"]); + 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) diff --git a/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs b/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs index 7c34f950a..a2592e557 100644 --- a/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs +++ b/test/Microsoft.AspNetCore.CookiePolicy.Test/CookiePolicyTests.cs @@ -102,7 +102,7 @@ await RunTest("/secureSame", 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("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]); }), new RequestTest("https://example.com/secureSame", transaction => diff --git a/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj b/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj index d7a42f3ef..3dc3df2e2 100644 --- a/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj +++ b/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj @@ -1,7 +1,7 @@  - $(StandardTestTfms) + netcoreapp3.0 diff --git a/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs b/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs index e2e4fd7d0..24bf09722 100644 --- a/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs +++ b/test/Microsoft.Owin.Security.Interop.Test/CookieInteropTests.cs @@ -1,6 +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. +/* 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; @@ -330,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 index f369f1f01..57b224ef6 100644 --- 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 @@ -2,10 +2,11 @@ net461 + + false - diff --git a/test/Microsoft.Owin.Security.Interop.Test/TicketInteropTests.cs b/test/Microsoft.Owin.Security.Interop.Test/TicketInteropTests.cs index 769adc015..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; @@ -87,5 +89,6 @@ public void InteropSerializerCanReadNewTicket() } } } +*/ diff --git a/version.props b/version.props index 370d5abab..71a78cddd 100644 --- a/version.props +++ b/version.props @@ -1,10 +1,12 @@ - + - 2.1.0 - preview2 + 3.0.0 + alpha1 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 + a- + $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) $(VersionSuffix)-$(BuildNumber)