diff --git a/.gitignore b/.gitignore index aba9c594d..ac82da756 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,6 @@ nuget.exe *DS_Store *.ncrunchsolution *.*sdf -*.ipch \ No newline at end of file +*.ipch +*.sln.ide +project.lock.json diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..947bf868e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: csharp +sudo: false +script: + - ./build.sh --quiet verify \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..64ff041d5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +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. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..0bdc1962b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/NuGet.Config b/NuGet.Config index a059188b0..da57d4726 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -2,12 +2,6 @@ - + - - - - - - \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..a676d4a87 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +ASP.NET Security +======== + +ASP.NET Security contains the security and authorization middlewares for ASP.NET 5. + +This project is part of ASP.NET 5. You can find samples, documentation and getting started instructions for ASP.NET 5 at the [Home](https://github.com/aspnet/home) repo. diff --git a/Security.sln b/Security.sln index 752ce9ab0..8f65fe16b 100644 --- a/Security.sln +++ b/Security.sln @@ -1,21 +1,50 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.30411.0 +# Visual Studio 14 +VisualStudioVersion = 14.0.22605.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4D2B6A51-2F9F-44F5-8131-EA5CAC053652}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security", "src\Microsoft.AspNet.Security\Microsoft.AspNet.Security.kproj", "{0F174C63-1898-4024-9A3C-3FDF5CAE5C68}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CookieSample", "samples\CookieSample\CookieSample.xproj", "{558C2C2A-AED8-49DE-BB60-D5F8AE06C714}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.Cookies", "src\Microsoft.AspNet.Security.Cookies\Microsoft.AspNet.Security.Cookies.kproj", "{15F1211B-B695-4A1C-B730-1AC58FC91090}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7BF11F3A-60B6-4796-B504-579C67FFBA34}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CookieSample", "samples\CookieSample\CookieSample.kproj", "{558C2C2A-AED8-49DE-BB60-D5F8AE06C714}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C40A5A3B-ABA3-4819-9C44-D821E6DA1BA1}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7BF11F3A-60B6-4796-B504-579C67FFBA34}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SocialSample", "samples\SocialSample\SocialSample.xproj", "{8C73D216-332D-41D8-BFD0-45BC4BC36552}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CookieSessionSample", "samples\CookieSessionSample\CookieSessionSample.xproj", "{19711880-46DA-4A26-9E0F-9B2E41D27651}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "OpenIdConnectSample", "samples\OpenIdConnectSample\OpenIdConnectSample.xproj", "{BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authentication.Cookies", "src\Microsoft.AspNet.Authentication.Cookies\Microsoft.AspNet.Authentication.Cookies.xproj", "{FC152CC4-054B-457E-8D91-389C5DE3C561}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authentication", "src\Microsoft.AspNet.Authentication\Microsoft.AspNet.Authentication.xproj", "{2286250A-52C8-4126-9F93-B1E45F0AD078}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authentication.Facebook", "src\Microsoft.AspNet.Authentication.Facebook\Microsoft.AspNet.Authentication.Facebook.xproj", "{EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authentication.Google", "src\Microsoft.AspNet.Authentication.Google\Microsoft.AspNet.Authentication.Google.xproj", "{76579C39-B829-490D-B8BE-1BD35FE8412E}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authentication.OpenIdConnect", "src\Microsoft.AspNet.Authentication.OpenIdConnect\Microsoft.AspNet.Authentication.OpenIdConnect.xproj", "{35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authentication.MicrosoftAccount", "src\Microsoft.AspNet.Authentication.MicrosoftAccount\Microsoft.AspNet.Authentication.MicrosoftAccount.xproj", "{ACB45E19-F520-4D0C-8916-B0CEB9C017FE}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authentication.Twitter", "src\Microsoft.AspNet.Authentication.Twitter\Microsoft.AspNet.Authentication.Twitter.xproj", "{0330FFF6-B4B5-42DD-8C99-26A789569000}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authentication.OAuth", "src\Microsoft.AspNet.Authentication.OAuth\Microsoft.AspNet.Authentication.OAuth.xproj", "{1657C79E-7755-4AEE-9D61-571295B69A30}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authentication.Test", "test\Microsoft.AspNet.Authentication.Test\Microsoft.AspNet.Authentication.Test.xproj", "{8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authentication.OAuthBearer", "src\Microsoft.AspNet.Authentication.OAuthBearer\Microsoft.AspNet.Authentication.OAuthBearer.xproj", "{2755BFE5-7421-4A31-A644-F817DF5CAA98}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authorization.Test", "test\Microsoft.AspNet.Authorization.Test\Microsoft.AspNet.Authorization.Test.xproj", "{7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security", "test\Microsoft.AspNet.Security.Test\Microsoft.AspNet.Security.kproj", "{8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authorization", "src\Microsoft.AspNet.Authorization\Microsoft.AspNet.Authorization.xproj", "{6AB3E514-5894-4131-9399-DC7D5284ADDB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,54 +56,212 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0F174C63-1898-4024-9A3C-3FDF5CAE5C68}.Debug|Any CPU.ActiveCfg = Debug|x86 - {0F174C63-1898-4024-9A3C-3FDF5CAE5C68}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {0F174C63-1898-4024-9A3C-3FDF5CAE5C68}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {0F174C63-1898-4024-9A3C-3FDF5CAE5C68}.Debug|x86.ActiveCfg = Debug|x86 - {0F174C63-1898-4024-9A3C-3FDF5CAE5C68}.Debug|x86.Build.0 = Debug|x86 - {0F174C63-1898-4024-9A3C-3FDF5CAE5C68}.Release|Any CPU.ActiveCfg = Release|x86 - {0F174C63-1898-4024-9A3C-3FDF5CAE5C68}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {0F174C63-1898-4024-9A3C-3FDF5CAE5C68}.Release|Mixed Platforms.Build.0 = Release|x86 - {0F174C63-1898-4024-9A3C-3FDF5CAE5C68}.Release|x86.ActiveCfg = Release|x86 - {0F174C63-1898-4024-9A3C-3FDF5CAE5C68}.Release|x86.Build.0 = Release|x86 - {15F1211B-B695-4A1C-B730-1AC58FC91090}.Debug|Any CPU.ActiveCfg = Debug|x86 - {15F1211B-B695-4A1C-B730-1AC58FC91090}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {15F1211B-B695-4A1C-B730-1AC58FC91090}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {15F1211B-B695-4A1C-B730-1AC58FC91090}.Debug|x86.ActiveCfg = Debug|x86 - {15F1211B-B695-4A1C-B730-1AC58FC91090}.Debug|x86.Build.0 = Debug|x86 - {15F1211B-B695-4A1C-B730-1AC58FC91090}.Release|Any CPU.ActiveCfg = Release|x86 - {15F1211B-B695-4A1C-B730-1AC58FC91090}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {15F1211B-B695-4A1C-B730-1AC58FC91090}.Release|Mixed Platforms.Build.0 = Release|x86 - {15F1211B-B695-4A1C-B730-1AC58FC91090}.Release|x86.ActiveCfg = Release|x86 - {15F1211B-B695-4A1C-B730-1AC58FC91090}.Release|x86.Build.0 = Release|x86 - {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Any CPU.ActiveCfg = Debug|x86 - {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|x86.ActiveCfg = Debug|x86 - {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|x86.Build.0 = Debug|x86 - {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Any CPU.ActiveCfg = Release|x86 - {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Mixed Platforms.Build.0 = Release|x86 - {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|x86.ActiveCfg = Release|x86 - {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|x86.Build.0 = Release|x86 - {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Any CPU.ActiveCfg = Debug|x86 - {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x86.ActiveCfg = Debug|x86 - {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x86.Build.0 = Debug|x86 - {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Any CPU.ActiveCfg = Release|x86 - {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.Build.0 = Release|x86 - {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x86.ActiveCfg = Release|x86 - {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x86.Build.0 = Release|x86 + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Any CPU.Build.0 = Debug|Any CPU + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Debug|x86.ActiveCfg = Debug|Any CPU + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Any CPU.ActiveCfg = Release|Any CPU + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Any CPU.Build.0 = Release|Any CPU + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|x86.ActiveCfg = Release|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|x86.ActiveCfg = Debug|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Any CPU.Build.0 = Release|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|x86.ActiveCfg = Release|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|x86.ActiveCfg = Debug|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Any CPU.Build.0 = Release|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|x86.ActiveCfg = Release|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|x86.ActiveCfg = Debug|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Debug|x86.Build.0 = Debug|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|Any CPU.Build.0 = Release|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|x86.ActiveCfg = Release|Any CPU + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}.Release|x86.Build.0 = Release|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|x86.ActiveCfg = Debug|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Debug|x86.Build.0 = Debug|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|Any CPU.Build.0 = Release|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|x86.ActiveCfg = Release|Any CPU + {FC152CC4-054B-457E-8D91-389C5DE3C561}.Release|x86.Build.0 = Release|Any CPU + {2286250A-52C8-4126-9F93-B1E45F0AD078}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2286250A-52C8-4126-9F93-B1E45F0AD078}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2286250A-52C8-4126-9F93-B1E45F0AD078}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2286250A-52C8-4126-9F93-B1E45F0AD078}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2286250A-52C8-4126-9F93-B1E45F0AD078}.Debug|x86.ActiveCfg = Debug|Any CPU + {2286250A-52C8-4126-9F93-B1E45F0AD078}.Debug|x86.Build.0 = Debug|Any CPU + {2286250A-52C8-4126-9F93-B1E45F0AD078}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2286250A-52C8-4126-9F93-B1E45F0AD078}.Release|Any CPU.Build.0 = Release|Any CPU + {2286250A-52C8-4126-9F93-B1E45F0AD078}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2286250A-52C8-4126-9F93-B1E45F0AD078}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2286250A-52C8-4126-9F93-B1E45F0AD078}.Release|x86.ActiveCfg = Release|Any CPU + {2286250A-52C8-4126-9F93-B1E45F0AD078}.Release|x86.Build.0 = Release|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|x86.ActiveCfg = Debug|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Debug|x86.Build.0 = Debug|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|Any CPU.Build.0 = Release|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|x86.ActiveCfg = Release|Any CPU + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A}.Release|x86.Build.0 = Release|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|x86.ActiveCfg = Debug|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Debug|x86.Build.0 = Debug|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|Any CPU.Build.0 = Release|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|x86.ActiveCfg = Release|Any CPU + {76579C39-B829-490D-B8BE-1BD35FE8412E}.Release|x86.Build.0 = Release|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|x86.ActiveCfg = Debug|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Debug|x86.Build.0 = Debug|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|Any CPU.Build.0 = Release|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|x86.ActiveCfg = Release|Any CPU + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A}.Release|x86.Build.0 = Release|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Debug|x86.Build.0 = Debug|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|Any CPU.Build.0 = Release|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|x86.ActiveCfg = Release|Any CPU + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE}.Release|x86.Build.0 = Release|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|x86.ActiveCfg = Debug|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Debug|x86.Build.0 = Debug|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|Any CPU.Build.0 = Release|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|x86.ActiveCfg = Release|Any CPU + {0330FFF6-B4B5-42DD-8C99-26A789569000}.Release|x86.Build.0 = Release|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|x86.ActiveCfg = Debug|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Debug|x86.Build.0 = Debug|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|Any CPU.Build.0 = Release|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|x86.ActiveCfg = Release|Any CPU + {1657C79E-7755-4AEE-9D61-571295B69A30}.Release|x86.Build.0 = Release|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x86.ActiveCfg = Debug|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x86.Build.0 = Debug|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Any CPU.Build.0 = Release|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x86.ActiveCfg = Release|Any CPU + {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x86.Build.0 = Release|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|x86.ActiveCfg = Debug|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Debug|x86.Build.0 = Debug|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|Any CPU.Build.0 = Release|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|x86.ActiveCfg = Release|Any CPU + {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|x86.Build.0 = Release|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|x86.ActiveCfg = Debug|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Debug|x86.Build.0 = Debug|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|Any CPU.Build.0 = Release|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|x86.ActiveCfg = Release|Any CPU + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2}.Release|x86.Build.0 = Release|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|x86.ActiveCfg = Debug|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Debug|x86.Build.0 = Debug|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|Any CPU.Build.0 = Release|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|x86.ActiveCfg = Release|Any CPU + {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {0F174C63-1898-4024-9A3C-3FDF5CAE5C68} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} - {15F1211B-B695-4A1C-B730-1AC58FC91090} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {558C2C2A-AED8-49DE-BB60-D5F8AE06C714} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} + {8C73D216-332D-41D8-BFD0-45BC4BC36552} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} + {19711880-46DA-4A26-9E0F-9B2E41D27651} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} + {FC152CC4-054B-457E-8D91-389C5DE3C561} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {2286250A-52C8-4126-9F93-B1E45F0AD078} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {EEAAEE68-607B-4E33-AF3E-45C66B4DBA5A} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {76579C39-B829-490D-B8BE-1BD35FE8412E} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {35115D55-B69E-46D4-BB33-C9E9E6EC5E7A} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {ACB45E19-F520-4D0C-8916-B0CEB9C017FE} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {0330FFF6-B4B5-42DD-8C99-26A789569000} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {1657C79E-7755-4AEE-9D61-571295B69A30} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B} = {7BF11F3A-60B6-4796-B504-579C67FFBA34} + {2755BFE5-7421-4A31-A644-F817DF5CAA98} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2} = {7BF11F3A-60B6-4796-B504-579C67FFBA34} + {6AB3E514-5894-4131-9399-DC7D5284ADDB} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} EndGlobalSection EndGlobal diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..636a7618d --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,7 @@ +init: + - git config --global core.autocrlf true +build_script: + - build.cmd --quiet verify +clone_depth: 1 +test: off +deploy: off \ No newline at end of file diff --git a/build.cmd b/build.cmd index 903d532df..41025afb2 100644 --- a/build.cmd +++ b/build.cmd @@ -18,9 +18,11 @@ copy %CACHED_NUGET% .nuget\nuget.exe > nul IF EXIST packages\KoreBuild goto run .nuget\NuGet.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre .nuget\NuGet.exe install Sake -version 0.2 -o packages -ExcludeVersion -CALL packages\KoreBuild\build\kvm upgrade -svr50 -x86 -CALL packages\KoreBuild\build\kvm install default -svrc50 -x86 + +IF "%SKIP_DNX_INSTALL%"=="1" goto run +CALL packages\KoreBuild\build\dnvm upgrade -runtime CLR -arch x86 +CALL packages\KoreBuild\build\dnvm install default -runtime CoreCLR -arch x86 :run -CALL packages\KoreBuild\build\kvm use default -svr50 -x86 +CALL packages\KoreBuild\build\dnvm use default -runtime CLR -arch x86 packages\Sake\tools\Sake.exe -I packages\KoreBuild\build -f makefile.shade %* diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index db1e0c3dd..d81164353 --- a/build.sh +++ b/build.sh @@ -1,12 +1,12 @@ -#!/bin/sh +#!/bin/bash if test `uname` = Darwin; then cachedir=~/Library/Caches/KBuild else - if x$XDG_DATA_HOME = x; then - cachedir=$HOME/.local/share + if [ -z $XDG_DATA_HOME ]; then + cachedir=$HOME/.local/share else - cachedir=$XDG_DATA_HOME; + cachedir=$XDG_DATA_HOME; fi fi mkdir -p $cachedir @@ -14,12 +14,12 @@ mkdir -p $cachedir url=https://www.nuget.org/nuget.exe if test ! -f $cachedir/nuget.exe; then - wget -o $cachedir/nuget.exe $url 2>/dev/null || curl -o $cachedir/nuget.exe --location $url /dev/null + wget -O $cachedir/nuget.exe $url 2>/dev/null || curl -o $cachedir/nuget.exe --location $url /dev/null fi if test ! -e .nuget; then mkdir .nuget - cp $cachedir/nuget.exe .nuget + cp $cachedir/nuget.exe .nuget/nuget.exe fi if test ! -d packages/KoreBuild; then @@ -27,4 +27,13 @@ if test ! -d packages/KoreBuild; then mono .nuget/nuget.exe install Sake -version 0.2 -o packages -ExcludeVersion fi -mono packages/Sake/tools/Sake.exe -I packages/KoreBuild/build -f makefile.shade "$@" \ No newline at end of file +if ! type dnvm > /dev/null 2>&1; then + source packages/KoreBuild/build/dnvm.sh +fi + +if ! type dnx > /dev/null 2>&1; then + dnvm upgrade +fi + +mono packages/Sake/tools/Sake.exe -I packages/KoreBuild/build -f makefile.shade "$@" + diff --git a/global.json b/global.json index 840c36f6a..983ba0401 100644 --- a/global.json +++ b/global.json @@ -1,3 +1,3 @@ { - "sources": ["src"] -} \ No newline at end of file + "projects": ["src"] +} diff --git a/makefile.shade b/makefile.shade index 6357ea284..562494d14 100644 --- a/makefile.shade +++ b/makefile.shade @@ -1,7 +1,7 @@ var VERSION='0.1' var FULL_VERSION='0.1' -var AUTHORS='Microsoft' +var AUTHORS='Microsoft Open Technologies, Inc.' use-standard-lifecycle k-standard-goals diff --git a/samples/CookieSample/CookieSample.kproj b/samples/CookieSample/CookieSample.kproj deleted file mode 100644 index 7d88de92e..000000000 --- a/samples/CookieSample/CookieSample.kproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - 12.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 558c2c2a-aed8-49de-bb60-d5f8ae06c714 - Library - - - - - - - 2.0 - - - - - - - - - \ No newline at end of file diff --git a/samples/CookieSample/CookieSample.xproj b/samples/CookieSample/CookieSample.xproj new file mode 100644 index 000000000..50d06f00e --- /dev/null +++ b/samples/CookieSample/CookieSample.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 558c2c2a-aed8-49de-bb60-d5f8ae06c714 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 22569 + + + \ No newline at end of file diff --git a/samples/CookieSample/Properties/launchSettings.json b/samples/CookieSample/Properties/launchSettings.json new file mode 100644 index 000000000..381ef9b50 --- /dev/null +++ b/samples/CookieSample/Properties/launchSettings.json @@ -0,0 +1,21 @@ +{ + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNET_ENV": "Development" + } + }, + "web": { + "commandName": "web", + "launchBrowser": true, + "launchUrl": "http://localhost:12345" + }, + "kestrel": { + "commandName": "kestrel", + "launchBrowser": true, + "launchUrl": "http://localhost:5004" + } + } +} \ No newline at end of file diff --git a/samples/CookieSample/Startup.cs b/samples/CookieSample/Startup.cs index 71386c068..6547450e5 100644 --- a/samples/CookieSample/Startup.cs +++ b/samples/CookieSample/Startup.cs @@ -1,37 +1,34 @@ -using System; using System.Security.Claims; -using Microsoft.AspNet; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Security.Cookies; -using Microsoft.AspNet.RequestContainer; +using Microsoft.AspNet.Authentication.Cookies; using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.DependencyInjection.Fallback; using Microsoft.Framework.Logging; namespace CookieSample { public class Startup { - public void Configuration(IBuilder app) + public void ConfigureServices(IServiceCollection services) { - app.UseServices(services => - { - // TODO: Move to host. - services.AddInstance(new NullLoggerFactory()); - }); + services.AddAuthentication(); + } - app.UseCookieAuthentication(new CookieAuthenticationOptions() - { + public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) + { + loggerfactory.AddConsole(LogLevel.Information); + app.UseCookieAuthentication(options => + { + options.AutomaticAuthentication = true; }); app.Run(async context => { - if (context.User == null || !context.User.Identity.IsAuthenticated) + if (string.IsNullOrEmpty(context.User.Identity.Name)) { - context.Response.SignIn(new ClaimsIdentity(new[] { new Claim("name", "bob") }, CookieAuthenticationDefaults.AuthenticationType)); - + var user = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "bob") })); + context.Authentication.SignIn(CookieAuthenticationDefaults.AuthenticationScheme, user); context.Response.ContentType = "text/plain"; await context.Response.WriteAsync("Hello First timer"); return; @@ -41,23 +38,5 @@ public void Configuration(IBuilder app) await context.Response.WriteAsync("Hello old timer"); }); } - - // TODO: Temp workaround until the host reliably provides logging. - // If ILoggerFactory is never guaranteed, move this fallback into Microsoft.Framework.Logging. - private class NullLoggerFactory : ILoggerFactory - { - public ILogger Create(string name) - { - return new NullLongger(); - } - } - - private class NullLongger : ILogger - { - public bool WriteCore(TraceType eventType, int eventId, object state, Exception exception, Func formatter) - { - return false; - } - } } } \ No newline at end of file diff --git a/samples/CookieSample/project.json b/samples/CookieSample/project.json index 22f33b30b..e69eed0e2 100644 --- a/samples/CookieSample/project.json +++ b/samples/CookieSample/project.json @@ -1,38 +1,21 @@ { - "version": "0.1-alpha-*", - "dependencies": { - "Microsoft.AspNet.Http": "0.1-alpha-*", - "Microsoft.AspNet.Security": "", - "Microsoft.AspNet.Security.Cookies": "", - "Microsoft.AspNet.Hosting": "0.1-alpha-*", - "Microsoft.AspNet.RequestContainer": "0.1-alpha-*", - "Microsoft.AspNet.PipelineCore": "0.1-alpha-*", - "Microsoft.AspNet.FeatureModel": "0.1-alpha-*", - "Microsoft.AspNet.HttpFeature": "0.1-alpha-*", - "Microsoft.AspNet.Server.WebListener": "0.1-alpha-*", - "Microsoft.Framework.DependencyInjection": "0.1-alpha-*", - "Microsoft.Framework.Logging": "0.1-alpha-*" - }, - "commands": { "web": "Microsoft.AspNet.Hosting server.name=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:12345" }, - "configurations": { - "net45": { + "dependencies": { + "Microsoft.AspNet.Authentication.Cookies": "1.0.0-*", + "Microsoft.AspNet.DataProtection": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.Framework.Logging.Console": "1.0.0-*", + "Kestrel": "1.0.0-*" }, - "k10": { - "dependencies": { - "System.Console": "4.0.0.0", - "System.Collections": "4.0.0.0", - "System.Diagnostics.Debug": "4.0.10.0", - "System.Diagnostics.Tools": "4.0.0.0", - "System.Globalization": "4.0.10.0", - "System.IO": "4.0.0.0", - "System.Linq": "4.0.0.0", - "System.Reflection": "4.0.10.0", - "System.Resources.ResourceManager": "4.0.0.0", - "System.Runtime": "4.0.20.0", - "System.Runtime.Extensions": "4.0.10.0", - "System.Runtime.InteropServices": "4.0.20.0", - "System.Threading.Tasks": "4.0.10.0" - } - } - } -} \ No newline at end of file + "commands": { + "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:12345", + "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5004" + }, + "frameworks": { + "dnx451": { + }, + "dnxcore50": { + } + }, + "webroot":"wwwroot" +} diff --git a/samples/CookieSample/wwwroot/.gitkeep b/samples/CookieSample/wwwroot/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/samples/CookieSessionSample/CookieSessionSample.xproj b/samples/CookieSessionSample/CookieSessionSample.xproj new file mode 100644 index 000000000..ec3d4dd5e --- /dev/null +++ b/samples/CookieSessionSample/CookieSessionSample.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 19711880-46da-4a26-9e0f-9b2e41d27651 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 36505 + + + \ No newline at end of file diff --git a/samples/CookieSessionSample/MemoryCacheSessionStore.cs b/samples/CookieSessionSample/MemoryCacheSessionStore.cs new file mode 100644 index 000000000..0ed51dfb2 --- /dev/null +++ b/samples/CookieSessionSample/MemoryCacheSessionStore.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication; +using Microsoft.AspNet.Authentication.Cookies.Infrastructure; +using Microsoft.Framework.Caching.Memory; + +namespace CookieSessionSample +{ + public class MemoryCacheSessionStore : IAuthenticationSessionStore + { + private const string KeyPrefix = "AuthSessionStore-"; + private IMemoryCache _cache; + + public MemoryCacheSessionStore() + { + _cache = new MemoryCache(new MemoryCacheOptions()); + } + + public async Task StoreAsync(AuthenticationTicket ticket) + { + var guid = Guid.NewGuid(); + var key = KeyPrefix + guid.ToString(); + await RenewAsync(key, ticket); + return key; + } + + public Task RenewAsync(string key, AuthenticationTicket ticket) + { + _cache.Set(key, ticket, context => + { + var expiresUtc = ticket.Properties.ExpiresUtc; + if (expiresUtc.HasValue) + { + context.SetAbsoluteExpiration(expiresUtc.Value); + } + context.SetSlidingExpiration(TimeSpan.FromHours(1)); // TODO: configurable. + + return (AuthenticationTicket)context.State; + }); + return Task.FromResult(0); + } + + public Task RetrieveAsync(string key) + { + AuthenticationTicket ticket; + _cache.TryGetValue(key, out ticket); + return Task.FromResult(ticket); + } + + public Task RemoveAsync(string key) + { + _cache.Remove(key); + return Task.FromResult(0); + } + } +} diff --git a/samples/CookieSessionSample/Properties/launchSettings.json b/samples/CookieSessionSample/Properties/launchSettings.json new file mode 100644 index 000000000..381ef9b50 --- /dev/null +++ b/samples/CookieSessionSample/Properties/launchSettings.json @@ -0,0 +1,21 @@ +{ + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNET_ENV": "Development" + } + }, + "web": { + "commandName": "web", + "launchBrowser": true, + "launchUrl": "http://localhost:12345" + }, + "kestrel": { + "commandName": "kestrel", + "launchBrowser": true, + "launchUrl": "http://localhost:5004" + } + } +} \ No newline at end of file diff --git a/samples/CookieSessionSample/Startup.cs b/samples/CookieSessionSample/Startup.cs new file mode 100644 index 000000000..f0b1b5219 --- /dev/null +++ b/samples/CookieSessionSample/Startup.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.Cookies; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; + +namespace CookieSessionSample +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication(); + } + + public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) + { + loggerfactory.AddConsole(LogLevel.Information); + + app.UseCookieAuthentication(options => + { + options.AutomaticAuthentication = true; + options.SessionStore = new MemoryCacheSessionStore(); + }); + + app.Run(async context => + { + if (string.IsNullOrEmpty(context.User.Identity.Name)) + { + // Make a large identity + var claims = new List(1001); + claims.Add(new Claim(ClaimTypes.Name, "bob")); + for (int i = 0; i < 1000; i++) + { + claims.Add(new Claim(ClaimTypes.Role, "SomeRandomGroup" + i, ClaimValueTypes.String, "IssuedByBob", "OriginalIssuerJoe")); + } + context.Authentication.SignIn(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(claims))); + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync("Hello First timer"); + return; + } + + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync("Hello old timer"); + }); + } + } +} \ No newline at end of file diff --git a/samples/CookieSessionSample/project.json b/samples/CookieSessionSample/project.json new file mode 100644 index 000000000..0f481e611 --- /dev/null +++ b/samples/CookieSessionSample/project.json @@ -0,0 +1,20 @@ +{ + "dependencies": { + "Microsoft.AspNet.Authentication.Cookies": "1.0.0-*", + "Microsoft.AspNet.DataProtection": "1.0.0-*", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.Framework.Caching.Memory": "1.0.0-*", + "Microsoft.Framework.Logging.Console": "1.0.0-*", + "Kestrel": "1.0.0-*" + }, + "commands": { + "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:12345", + "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5004" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + }, + "webroot": "wwwroot" +} diff --git a/samples/CookieSessionSample/wwwroot/.gitkeep b/samples/CookieSessionSample/wwwroot/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/samples/OpenIdConnectSample/OpenIdConnectSample.xproj b/samples/OpenIdConnectSample/OpenIdConnectSample.xproj new file mode 100644 index 000000000..1f0879ea4 --- /dev/null +++ b/samples/OpenIdConnectSample/OpenIdConnectSample.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + bef0f5c3-ef4e-4649-9c49-d5e279a3ca2b + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 42023 + + + \ No newline at end of file diff --git a/samples/OpenIdConnectSample/Properties/launchSettings.json b/samples/OpenIdConnectSample/Properties/launchSettings.json new file mode 100644 index 000000000..d8622657c --- /dev/null +++ b/samples/OpenIdConnectSample/Properties/launchSettings.json @@ -0,0 +1,21 @@ +{ + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNET_ENV": "Development" + } + }, + "kestrel": { + "commandName": "kestrel", + "launchBrowser": true, + "launchUrl": "http://localhost:5004" + }, + "web": { + "commandName": "web", + "launchBrowser": true, + "launchUrl": "http://localhost:12345" + } + } +} \ No newline at end of file diff --git a/samples/OpenIdConnectSample/Startup.cs b/samples/OpenIdConnectSample/Startup.cs new file mode 100644 index 000000000..2077d802b --- /dev/null +++ b/samples/OpenIdConnectSample/Startup.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Authentication; +using Microsoft.AspNet.Authentication.Cookies; +using Microsoft.AspNet.Authentication.OpenIdConnect; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; + +namespace OpenIdConnectSample +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication(); + services.Configure(options => + { + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }); + } + + public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) + { + loggerfactory.AddConsole(LogLevel.Information); + + app.UseCookieAuthentication(options => + { + options.AutomaticAuthentication = true; + }); + + app.UseOpenIdConnectAuthentication(options => + { + options.ClientId = "fe78e0b4-6fe7-47e6-812c-fb75cee266a4"; + options.Authority = "https://login.windows.net/cyrano.onmicrosoft.com"; + options.RedirectUri = "http://localhost:42023"; + }); + + app.Run(async context => + { + if (string.IsNullOrEmpty(context.User.Identity.Name)) + { + context.Authentication.Challenge(OpenIdConnectAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" }); + + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync("Hello First timer"); + return; + } + + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync("Hello Authenticated User"); + }); + + + } + } +} diff --git a/samples/OpenIdConnectSample/project.json b/samples/OpenIdConnectSample/project.json new file mode 100644 index 000000000..815ad8a33 --- /dev/null +++ b/samples/OpenIdConnectSample/project.json @@ -0,0 +1,20 @@ +{ + "dependencies": { + "Microsoft.AspNet.Authentication.Cookies": "1.0.0-*", + "Microsoft.AspNet.Authentication.OpenIdConnect": "1.0.0-*", + "Microsoft.AspNet.DataProtection": "1.0.0-*", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.Framework.Logging.Console": "1.0.0-*", + "Kestrel": "1.0.0-*" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + }, + "commands": { + "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:12345", + "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5004" + }, + "webroot": "wwwroot" +} diff --git a/samples/OpenIdConnectSample/wwwroot/placeholder.html b/samples/OpenIdConnectSample/wwwroot/placeholder.html new file mode 100644 index 000000000..125a5a8cf --- /dev/null +++ b/samples/OpenIdConnectSample/wwwroot/placeholder.html @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/samples/SocialSample/Properties/launchSettings.json b/samples/SocialSample/Properties/launchSettings.json new file mode 100644 index 000000000..88e42ba2b --- /dev/null +++ b/samples/SocialSample/Properties/launchSettings.json @@ -0,0 +1,21 @@ +{ + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNET_ENV": "Development" + } + }, + "kestrel": { + "commandName": "kestrel", + "launchBrowser": true, + "launchUrl": "http://localhost:54540/" + }, + "web": { + "commandName": "web", + "launchBrowser": true, + "launchUrl": "http://localhost:54540/" + } + } +} \ No newline at end of file diff --git a/samples/SocialSample/SocialSample.xproj b/samples/SocialSample/SocialSample.xproj new file mode 100644 index 000000000..3d2aa528d --- /dev/null +++ b/samples/SocialSample/SocialSample.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 8c73d216-332d-41d8-bfd0-45bc4bc36552 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 54540 + + + \ No newline at end of file diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs new file mode 100644 index 000000000..eb6c74563 --- /dev/null +++ b/samples/SocialSample/Startup.cs @@ -0,0 +1,246 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using Microsoft.AspNet.Authentication; +using Microsoft.AspNet.Authentication.Cookies; +using Microsoft.AspNet.Authentication.Google; +using Microsoft.AspNet.Authentication.MicrosoftAccount; +using Microsoft.AspNet.Authentication.OAuth; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; +using Newtonsoft.Json.Linq; + +namespace CookieSample +{ + /* Note all servers must use the same address and port because these are pre-registered with the various providers. */ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication(); + services.Configure(options => + { + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }); + services.ConfigureClaimsTransformation(p => + { + var id = new ClaimsIdentity("xform"); + id.AddClaim(new Claim("ClaimsTransformation", "TransformAddedClaim")); + p.AddIdentity(id); + return p; + }); + } + + public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) + { + loggerfactory.AddConsole(LogLevel.Information); + + app.UseCookieAuthentication(options => + { + options.AutomaticAuthentication = true; + options.LoginPath = new PathString("/login"); + }); + + // https://developers.facebook.com/apps/ + app.UseFacebookAuthentication(options => + { + options.AppId = "569522623154478"; + options.AppSecret = "a124463c4719c94b4228d9a240e5dc1a"; + }); + + app.UseOAuthAuthentication("Google-AccessToken", options => + { + options.ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com"; + options.ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f"; + options.CallbackPath = new PathString("/signin-google-token"); + options.AuthorizationEndpoint = GoogleAuthenticationDefaults.AuthorizationEndpoint; + options.TokenEndpoint = GoogleAuthenticationDefaults.TokenEndpoint; + options.Scope.Add("openid"); + options.Scope.Add("profile"); + options.Scope.Add("email"); + }); + + // https://console.developers.google.com/project + app.UseGoogleAuthentication(options => + { + options.ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com"; + options.ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f"; + }); + + // https://apps.twitter.com/ + app.UseTwitterAuthentication(options => + { + options.ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g"; + options.ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI"; + }); + + /* https://account.live.com/developers/applications + The MicrosoftAccount service has restrictions that prevent the use of http://localhost:54540/ for test applications. + As such, here is how to change this sample to uses http://mssecsample.localhost.this:54540/ instead. + + Edit the Project.json file and replace http://localhost:54540/ with http://mssecsample.localhost.this:54540/. + + From an admin command console first enter: + notepad C:\Windows\System32\drivers\etc\hosts + and add this to the file, save, and exit (and reboot?): + 127.0.0.1 MsSecSample.localhost.this + + Then you can choose to run the app as admin (see below) or add the following ACL as admin: + netsh http add urlacl url=http://mssecsample.localhost.this:54540/ user=[domain\user] + + The sample app can then be run via: + dnx . web + */ + app.UseOAuthAuthentication("Microsoft-AccessToken", options => + { + options.Caption = "MicrosoftAccount-AccessToken - Requires project changes"; + options.ClientId = "00000000480FF62E"; + options.ClientSecret = "bLw2JIvf8Y1TaToipPEqxTVlOeJwCUsr"; + options.CallbackPath = new PathString("/signin-microsoft-token"); + options.AuthorizationEndpoint = MicrosoftAccountAuthenticationDefaults.AuthorizationEndpoint; + options.TokenEndpoint = MicrosoftAccountAuthenticationDefaults.TokenEndpoint; + options.Scope.Add("wl.basic"); + }); + + app.UseMicrosoftAccountAuthentication(options => + { + options.Caption = "MicrosoftAccount - Requires project changes"; + options.ClientId = "00000000480FF62E"; + options.ClientSecret = "bLw2JIvf8Y1TaToipPEqxTVlOeJwCUsr"; + }); + + // https://github.com/settings/applications/ + app.UseOAuthAuthentication("GitHub-AccessToken", options => + { + options.ClientId = "8c0c5a572abe8fe89588"; + options.ClientSecret = "e1d95eaf03461d27acd6f49d4fc7bf19d6ac8cda"; + options.CallbackPath = new PathString("/signin-github-token"); + options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize"; + options.TokenEndpoint = "https://github.com/login/oauth/access_token"; + }); + + app.UseOAuthAuthentication("GitHub", options => + { + options.ClientId = "49e302895d8b09ea5656"; + options.ClientSecret = "98f1bf028608901e9df91d64ee61536fe562064b"; + options.CallbackPath = new PathString("/signin-github"); + options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize"; + options.TokenEndpoint = "https://github.com/login/oauth/access_token"; + options.UserInformationEndpoint = "https://api.github.com/user"; + options.ClaimsIssuer = "OAuth2-Github"; + // Retrieving user information is unique to each provider. + options.Notifications = new OAuthAuthenticationNotifications() + { + OnGetUserInformationAsync = async (context) => + { + // Get the GitHub user + var userRequest = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); + userRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); + userRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var userResponse = await context.Backchannel.SendAsync(userRequest, context.HttpContext.RequestAborted); + userResponse.EnsureSuccessStatusCode(); + var text = await userResponse.Content.ReadAsStringAsync(); + var user = JObject.Parse(text); + + var identity = new ClaimsIdentity( + context.Options.AuthenticationScheme, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType); + + JToken value; + var id = user.TryGetValue("id", out value) ? value.ToString() : null; + if (!string.IsNullOrEmpty(id)) + { + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id, ClaimValueTypes.String, context.Options.ClaimsIssuer)); + } + var userName = user.TryGetValue("login", out value) ? value.ToString() : null; + if (!string.IsNullOrEmpty(userName)) + { + identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, userName, ClaimValueTypes.String, context.Options.ClaimsIssuer)); + } + var name = user.TryGetValue("name", out value) ? value.ToString() : null; + if (!string.IsNullOrEmpty(name)) + { + identity.AddClaim(new Claim("urn:github:name", name, ClaimValueTypes.String, context.Options.ClaimsIssuer)); + } + var link = user.TryGetValue("url", out value) ? value.ToString() : null; + if (!string.IsNullOrEmpty(link)) + { + identity.AddClaim(new Claim("urn:github:url", link, ClaimValueTypes.String, context.Options.ClaimsIssuer)); + } + + context.Principal = new ClaimsPrincipal(identity); + }, + }; + }); + + // Choose an authentication type + app.Map("/login", signoutApp => + { + signoutApp.Run(async context => + { + var authType = context.Request.Query["authscheme"]; + if (!string.IsNullOrEmpty(authType)) + { + // By default the client will be redirect back to the URL that issued the challenge (/login?authtype=foo), + // send them to the home page instead (/). + context.Authentication.Challenge(authType, new AuthenticationProperties() { RedirectUri = "/" }); + return; + } + + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync(""); + await context.Response.WriteAsync("Choose an authentication scheme:
"); + foreach (var type in context.Authentication.GetAuthenticationSchemes()) + { + await context.Response.WriteAsync("" + (type.Caption ?? "(suppressed)") + "
"); + } + await context.Response.WriteAsync(""); + }); + }); + + // Sign-out to remove the user cookie. + app.Map("/logout", signoutApp => + { + signoutApp.Run(async context => + { + context.Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationScheme); + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync(""); + await context.Response.WriteAsync("You have been logged out. Goodbye " + context.User.Identity.Name + "
"); + await context.Response.WriteAsync("Home"); + await context.Response.WriteAsync(""); + }); + }); + + // Deny anonymous request beyond this point. + app.Use(async (context, next) => + { + if (string.IsNullOrEmpty(context.User.Identity.Name)) + { + // The cookie middleware will intercept this 401 and redirect to /login + context.Authentication.Challenge(); + return; + } + await next(); + }); + + // Display user information + app.Run(async context => + { + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync(""); + await context.Response.WriteAsync("Hello " + context.User.Identity.Name + "
"); + foreach (var claim in context.User.Claims) + { + await context.Response.WriteAsync(claim.Type + ": " + claim.Value + "
"); + } + await context.Response.WriteAsync("Logout"); + await context.Response.WriteAsync(""); + }); + } + } +} diff --git a/samples/SocialSample/project.json b/samples/SocialSample/project.json new file mode 100644 index 000000000..024014140 --- /dev/null +++ b/samples/SocialSample/project.json @@ -0,0 +1,25 @@ +{ + "dependencies": { + "Microsoft.AspNet.Authentication.Cookies": "1.0.0-*", + "Microsoft.AspNet.Authentication.Facebook": "1.0.0-*", + "Microsoft.AspNet.Authentication.Google": "1.0.0-*", + "Microsoft.AspNet.Authentication.MicrosoftAccount": "1.0.0-*", + "Microsoft.AspNet.Authentication.Twitter": "1.0.0-*", + "Microsoft.AspNet.DataProtection": "1.0.0-*", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.Framework.Logging.Console": "1.0.0-*", + "Kestrel": "1.0.0-*" + }, + "commands": { + "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:54540", + "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:54540" + }, + "frameworks": { + "dnx451": { + }, + "dnxcore50": { + } + }, + "webroot": "wwwroot" +} diff --git a/samples/SocialSample/wwwroot/.gitkeep b/samples/SocialSample/wwwroot/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAppBuilderExtensions.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAppBuilderExtensions.cs new file mode 100644 index 000000000..d865dc4ce --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAppBuilderExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Authentication.Cookies; +using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods provided by the cookies authentication middleware + /// + public static class CookieAppBuilderExtensions + { + /// + /// Adds a cookie-based authentication middleware to your web application pipeline. + /// + /// The IApplicationBuilder passed to your configuration method + /// Used to configure the options for the middleware + /// The name of the options class that controls the middleware behavior, null will use the default options + /// The original app parameter + public static IApplicationBuilder UseCookieAuthentication([NotNull] this IApplicationBuilder app, Action configureOptions = null, string optionsName = "") + { + return app.UseMiddleware( + new ConfigureOptions(configureOptions ?? (o => { })) + { + Name = optionsName + }); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationDefaults.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationDefaults.cs similarity index 83% rename from src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationDefaults.cs rename to src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationDefaults.cs index 3ea10afb9..95981a0f0 100644 --- a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationDefaults.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationDefaults.cs @@ -1,9 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics.CodeAnalysis; using Microsoft.AspNet.Http; -namespace Microsoft.AspNet.Security.Cookies +namespace Microsoft.AspNet.Authentication.Cookies { /// /// Default values related to cookie-based authentication middleware @@ -11,9 +12,9 @@ namespace Microsoft.AspNet.Security.Cookies public static class CookieAuthenticationDefaults { /// - /// The default value used for CookieAuthenticationOptions.AuthenticationType + /// The default value used for CookieAuthenticationOptions.AuthenticationScheme /// - public const string AuthenticationType = "Cookies"; + public const string AuthenticationScheme = "Cookies"; /// /// The prefix used to provide a default CookieAuthenticationOptions.CookieName diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs new file mode 100644 index 000000000..04852e390 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs @@ -0,0 +1,404 @@ +// Copyright (c) .NET Foundation. All 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.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Authentication.Cookies +{ + internal class CookieAuthenticationHandler : AuthenticationHandler + { + private const string HeaderNameCacheControl = "Cache-Control"; + private const string HeaderNamePragma = "Pragma"; + private const string HeaderNameExpires = "Expires"; + private const string HeaderValueNoCache = "no-cache"; + private const string HeaderValueMinusOne = "-1"; + private const string SessionIdClaim = "Microsoft.AspNet.Authentication.Cookies-SessionId"; + + private bool _shouldRenew; + private DateTimeOffset _renewIssuedUtc; + private DateTimeOffset _renewExpiresUtc; + private string _sessionKey; + + protected override AuthenticationTicket AuthenticateCore() + { + return AuthenticateCoreAsync().GetAwaiter().GetResult(); + } + + protected override async Task AuthenticateCoreAsync() + { + AuthenticationTicket ticket = null; + try + { + var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName); + if (string.IsNullOrWhiteSpace(cookie)) + { + return null; + } + + ticket = Options.TicketDataFormat.Unprotect(cookie); + + if (ticket == null) + { + Logger.LogWarning(@"Unprotect ticket failed"); + return null; + } + + if (Options.SessionStore != null) + { + var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim)); + if (claim == null) + { + Logger.LogWarning(@"SessionId missing"); + return null; + } + _sessionKey = claim.Value; + ticket = await Options.SessionStore.RetrieveAsync(_sessionKey); + if (ticket == null) + { + Logger.LogWarning(@"Identity missing in session store"); + return null; + } + } + + var currentUtc = Options.SystemClock.UtcNow; + var issuedUtc = ticket.Properties.IssuedUtc; + var expiresUtc = ticket.Properties.ExpiresUtc; + + if (expiresUtc != null && expiresUtc.Value < currentUtc) + { + if (Options.SessionStore != null) + { + await Options.SessionStore.RemoveAsync(_sessionKey); + } + return null; + } + + var allowRefresh = ticket.Properties.AllowRefresh ?? true; + if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration && allowRefresh) + { + var timeElapsed = currentUtc.Subtract(issuedUtc.Value); + var timeRemaining = expiresUtc.Value.Subtract(currentUtc); + + if (timeRemaining < timeElapsed) + { + _shouldRenew = true; + _renewIssuedUtc = currentUtc; + var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); + _renewExpiresUtc = currentUtc.Add(timeSpan); + } + } + + var context = new CookieValidatePrincipalContext(Context, ticket, Options); + + await Options.Notifications.ValidatePrincipal(context); + + return new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme); + } + catch (Exception exception) + { + var exceptionContext = new CookieExceptionContext(Context, Options, + CookieExceptionContext.ExceptionLocation.Authenticate, exception, ticket); + Options.Notifications.Exception(exceptionContext); + if (exceptionContext.Rethrow) + { + throw; + } + return exceptionContext.Ticket; + } + } + + protected override void ApplyResponseGrant() + { + ApplyResponseGrantAsync().GetAwaiter().GetResult(); + } + + protected override async Task ApplyResponseGrantAsync() + { + var signin = SignInContext; + var shouldSignin = signin != null; + var signout = SignOutContext; + var shouldSignout = signout != null; + + if (!(shouldSignin || shouldSignout || _shouldRenew)) + { + return; + } + + var model = await AuthenticateAsync(); + try + { + var cookieOptions = new CookieOptions + { + Domain = Options.CookieDomain, + HttpOnly = Options.CookieHttpOnly, + Path = Options.CookiePath ?? (RequestPathBase.HasValue ? RequestPathBase.ToString() : "/"), + }; + if (Options.CookieSecure == CookieSecureOption.SameAsRequest) + { + cookieOptions.Secure = Request.IsHttps; + } + else + { + cookieOptions.Secure = Options.CookieSecure == CookieSecureOption.Always; + } + + if (shouldSignin) + { + var signInContext = new CookieResponseSignInContext( + Context, + Options, + Options.AuthenticationScheme, + signin.Principal, + new AuthenticationProperties(signin.Properties), + cookieOptions); + + DateTimeOffset issuedUtc; + if (signInContext.Properties.IssuedUtc.HasValue) + { + issuedUtc = signInContext.Properties.IssuedUtc.Value; + } + else + { + issuedUtc = Options.SystemClock.UtcNow; + signInContext.Properties.IssuedUtc = issuedUtc; + } + + if (!signInContext.Properties.ExpiresUtc.HasValue) + { + signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan); + } + + Options.Notifications.ResponseSignIn(signInContext); + + if (signInContext.Properties.IsPersistent) + { + var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan); + signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; + } + + model = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.AuthenticationScheme); + if (Options.SessionStore != null) + { + if (_sessionKey != null) + { + await Options.SessionStore.RemoveAsync(_sessionKey); + } + _sessionKey = await Options.SessionStore.StoreAsync(model); + var principal = new ClaimsPrincipal( + new ClaimsIdentity( + new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, + Options.ClaimsIssuer)); + model = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); + } + var cookieValue = Options.TicketDataFormat.Protect(model); + + Options.CookieManager.AppendResponseCookie( + Context, + Options.CookieName, + cookieValue, + signInContext.CookieOptions); + + var signedInContext = new CookieResponseSignedInContext( + Context, + Options, + Options.AuthenticationScheme, + signInContext.Principal, + signInContext.Properties); + + Options.Notifications.ResponseSignedIn(signedInContext); + } + else if (shouldSignout) + { + if (Options.SessionStore != null && _sessionKey != null) + { + await Options.SessionStore.RemoveAsync(_sessionKey); + } + + var context = new CookieResponseSignOutContext( + Context, + Options, + cookieOptions); + + Options.Notifications.ResponseSignOut(context); + + Options.CookieManager.DeleteCookie( + Context, + Options.CookieName, + context.CookieOptions); + } + else if (_shouldRenew) + { + model.Properties.IssuedUtc = _renewIssuedUtc; + model.Properties.ExpiresUtc = _renewExpiresUtc; + + if (Options.SessionStore != null && _sessionKey != null) + { + await Options.SessionStore.RenewAsync(_sessionKey, model); + var principal = new ClaimsPrincipal( + new ClaimsIdentity( + new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, + Options.AuthenticationScheme)); + model = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); + } + + var cookieValue = Options.TicketDataFormat.Protect(model); + + if (model.Properties.IsPersistent) + { + cookieOptions.Expires = _renewExpiresUtc.ToUniversalTime().DateTime; + } + + Options.CookieManager.AppendResponseCookie( + Context, + Options.CookieName, + cookieValue, + cookieOptions); + } + + Response.Headers.Set( + HeaderNameCacheControl, + HeaderValueNoCache); + + Response.Headers.Set( + HeaderNamePragma, + HeaderValueNoCache); + + Response.Headers.Set( + HeaderNameExpires, + HeaderValueMinusOne); + + var shouldLoginRedirect = shouldSignin && Options.LoginPath.HasValue && Request.Path == Options.LoginPath; + var shouldLogoutRedirect = shouldSignout && Options.LogoutPath.HasValue && Request.Path == Options.LogoutPath; + + if ((shouldLoginRedirect || shouldLogoutRedirect) && Response.StatusCode == 200) + { + var query = Request.Query; + var redirectUri = query.Get(Options.ReturnUrlParameter); + if (!string.IsNullOrWhiteSpace(redirectUri) + && IsHostRelative(redirectUri)) + { + var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri); + Options.Notifications.ApplyRedirect(redirectContext); + } + } + } + catch (Exception exception) + { + var exceptionContext = new CookieExceptionContext(Context, Options, + CookieExceptionContext.ExceptionLocation.ApplyResponseGrant, exception, model); + Options.Notifications.Exception(exceptionContext); + if (exceptionContext.Rethrow) + { + throw; + } + } + } + + private static bool IsHostRelative(string path) + { + if (string.IsNullOrEmpty(path)) + { + return false; + } + if (path.Length == 1) + { + return path[0] == '/'; + } + return path[0] == '/' && path[1] != '/' && path[1] != '\\'; + } + + protected override void ApplyResponseChallenge() + { + if (ShouldConvertChallengeToForbidden()) + { + // Handle 403 by redirecting to AccessDeniedPath if set + if (Options.AccessDeniedPath.HasValue) + { + try + { + var accessDeniedUri = + Request.Scheme + + "://" + + Request.Host + + Request.PathBase + + Options.AccessDeniedPath; + + var redirectContext = new CookieApplyRedirectContext(Context, Options, accessDeniedUri); + Options.Notifications.ApplyRedirect(redirectContext); + } + catch (Exception exception) + { + var exceptionContext = new CookieExceptionContext(Context, Options, + CookieExceptionContext.ExceptionLocation.ApplyResponseChallenge, exception, ticket: null); + Options.Notifications.Exception(exceptionContext); + if (exceptionContext.Rethrow) + { + throw; + } + } + } + else + { + Response.StatusCode = 403; + } + return; + } + + if (Response.StatusCode != 401 || !Options.LoginPath.HasValue ) + { + return; + } + + // Automatic middleware should redirect on 401 even if there wasn't an explicit challenge. + if (ChallengeContext == null && !Options.AutomaticAuthentication) + { + return; + } + + var loginUri = string.Empty; + if (ChallengeContext != null) + { + loginUri = new AuthenticationProperties(ChallengeContext.Properties).RedirectUri; + } + + try + { + if (string.IsNullOrWhiteSpace(loginUri)) + { + var currentUri = + Request.PathBase + + Request.Path + + Request.QueryString; + + loginUri = + Request.Scheme + + "://" + + Request.Host + + Request.PathBase + + Options.LoginPath + + QueryString.Create(Options.ReturnUrlParameter, currentUri); + } + + var redirectContext = new CookieApplyRedirectContext(Context, Options, loginUri); + Options.Notifications.ApplyRedirect(redirectContext); + } + catch (Exception exception) + { + var exceptionContext = new CookieExceptionContext(Context, Options, + CookieExceptionContext.ExceptionLocation.ApplyResponseChallenge, exception, ticket: null); + Options.Notifications.Exception(exceptionContext); + if (exceptionContext.Rethrow) + { + throw; + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs new file mode 100644 index 000000000..70cafe854 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Authentication.Cookies.Infrastructure; +using Microsoft.AspNet.Authentication.DataHandler; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.DataProtection; +using Microsoft.Framework.Internal; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Authentication.Cookies +{ + public class CookieAuthenticationMiddleware : AuthenticationMiddleware + { + public CookieAuthenticationMiddleware( + [NotNull] RequestDelegate next, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IUrlEncoder urlEncoder, + [NotNull] IOptions options, + ConfigureOptions configureOptions) + : base(next, options, loggerFactory, urlEncoder, configureOptions) + { + if (Options.Notifications == null) + { + Options.Notifications = new CookieAuthenticationNotifications(); + } + if (String.IsNullOrEmpty(Options.CookieName)) + { + Options.CookieName = CookieAuthenticationDefaults.CookiePrefix + Options.AuthenticationScheme; + } + if (Options.TicketDataFormat == null) + { + var dataProtector = dataProtectionProvider.CreateProtector( + typeof(CookieAuthenticationMiddleware).FullName, Options.AuthenticationScheme, "v2"); + Options.TicketDataFormat = new TicketDataFormat(dataProtector); + } + if (Options.CookieManager == null) + { + Options.CookieManager = new ChunkingCookieManager(urlEncoder); + } + } + + protected override AuthenticationHandler CreateHandler() + { + return new CookieAuthenticationHandler(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs similarity index 79% rename from src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs rename to src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs index 3fa5cb5e8..52660955c 100644 --- a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs @@ -1,11 +1,13 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNet.Http; -using Microsoft.AspNet.Security.Infrastructure; +using Microsoft.AspNet.Authentication.Cookies.Infrastructure; +using Microsoft.Framework.Internal; -namespace Microsoft.AspNet.Security.Cookies +namespace Microsoft.AspNet.Authentication.Cookies { /// /// Contains the options used by the CookiesAuthenticationMiddleware @@ -18,10 +20,9 @@ public class CookieAuthenticationOptions : AuthenticationOptions /// Create an instance of the options initialized with the default values /// public CookieAuthenticationOptions() - : base(CookieAuthenticationDefaults.AuthenticationType) { + AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme; ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter; - CookiePath = "/"; ExpireTimeSpan = TimeSpan.FromDays(14); SlidingExpiration = true; CookieHttpOnly = true; @@ -32,18 +33,15 @@ public CookieAuthenticationOptions() /// /// Determines the cookie name used to persist the identity. The default value is ".AspNet.Cookies". - /// This value should be changed if you change the name of the AuthenticationType, especially if your + /// This value should be changed if you change the name of the AuthenticationScheme, especially if your /// system uses the cookie authentication middleware multiple times. /// public string CookieName { get { return _cookieName; } + [param: NotNull] set { - if (value == null) - { - throw new ArgumentNullException("value"); - } _cookieName = value; } } @@ -104,6 +102,15 @@ public string CookieName [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Logout", Justification = "By design")] public PathString LogoutPath { get; set; } + /// + /// The AccessDeniedPath property informs the middleware that it should change an outgoing 403 Forbidden status + /// code into a 302 redirection onto the given path. + /// + /// If the AccessDeniedPath is null or empty, the middleware will not look for 403 Forbidden status codes, and it will + /// not redirect + /// + public PathString AccessDeniedPath { get; set; } + /// /// The ReturnUrlParameter determines the name of the query string parameter which is appended by the middleware /// when a 401 Unauthorized status code is changed to a 302 redirect onto the login path. This is also the query @@ -123,7 +130,7 @@ public string CookieName /// /// The TicketDataFormat is used to protect and unprotect the identity and other properties which are stored in the /// cookie value. If it is not provided a default data handler is created using the data protection service contained - /// in the IAppBuilder.Properties. The default data protection service is based on machine key when running on ASP.NET, + /// in the IApplicationBuilder.Properties. The default data protection service is based on machine key when running on ASP.NET, /// and on DPAPI when running in a different process. /// public ISecureDataFormat TicketDataFormat { get; set; } @@ -133,5 +140,18 @@ public string CookieName /// used which calls DateTimeOffset.UtcNow. This is typically not replaced except for unit testing. /// public ISystemClock SystemClock { get; set; } + + /// + /// The component used to get cookies from the request or set them on the response. + /// + /// ChunkingCookieManager will be used by default. + /// + public ICookieManager CookieManager { get; set; } + + /// + /// An optional container in which to store the identity across requests. When used, only a session identifier is sent + /// to the client. This can be used to mitigate potential problems with very large identities. + /// + public IAuthenticationSessionStore SessionStore { get; set; } } } diff --git a/src/Microsoft.AspNet.Security.Cookies/CookieSecureOption.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieSecureOption.cs similarity index 87% rename from src/Microsoft.AspNet.Security.Cookies/CookieSecureOption.cs rename to src/Microsoft.AspNet.Authentication.Cookies/CookieSecureOption.cs index 19efae749..83d34d0ae 100644 --- a/src/Microsoft.AspNet.Security.Cookies/CookieSecureOption.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieSecureOption.cs @@ -1,6 +1,8 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (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.AspNet.Security.Cookies + +namespace Microsoft.AspNet.Authentication.Cookies { /// /// Determines how the identity cookie's security property is set. diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieServiceCollectionExtensions.cs new file mode 100644 index 000000000..c55cb9d5f --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieServiceCollectionExtensions.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 Microsoft.AspNet.Authentication.Cookies; +using Microsoft.Framework.Configuration; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.DependencyInjection +{ + /// + /// Extension methods provided by the cookies authentication middleware + /// + public static class CookieServiceCollectionExtensions + { + public static IServiceCollection ConfigureCookieAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure) + { + return services.ConfigureCookieAuthentication(configure, optionsName: ""); + } + + public static IServiceCollection ConfigureCookieAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure, string optionsName) + { + return services.Configure(configure, optionsName); + } + + public static IServiceCollection ConfigureCookieAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config) + { + return services.ConfigureCookieAuthentication(config, optionsName: ""); + } + + public static IServiceCollection ConfigureCookieAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config, string optionsName) + { + return services.Configure(config, optionsName); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Infrastructure/ChunkingCookieManager.cs b/src/Microsoft.AspNet.Authentication.Cookies/Infrastructure/ChunkingCookieManager.cs new file mode 100644 index 000000000..62ab42dd2 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/Infrastructure/ChunkingCookieManager.cs @@ -0,0 +1,283 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.AspNet.Http; +using Microsoft.Framework.Internal; +using Microsoft.Framework.WebEncoders; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure +{ + /// + /// This handles cookies that are limited by per cookie length. It breaks down long cookies for responses, and reassembles them + /// from requests. + /// + public class ChunkingCookieManager : ICookieManager + { + public ChunkingCookieManager(IUrlEncoder urlEncoder) + { + // Lowest common denominator. Safari has the lowest known limit (4093), and we leave little extra just in case. + // See http://browsercookielimits.x64.me/. + ChunkSize = 4090; + ThrowForPartialCookies = true; + Encoder = urlEncoder ?? UrlEncoder.Default; + } + + /// + /// The maximum size of cookie to send back to the client. If a cookie exceeds this size it will be broken down into multiple + /// cookies. Set this value to null to disable this behavior. The default is 4090 characters, which is supported by all + /// common browsers. + /// + /// Note that browsers may also have limits on the total size of all cookies per domain, and on the number of cookies per domain. + /// + public int? ChunkSize { get; set; } + + /// + /// Throw if not all chunks of a cookie are available on a request for re-assembly. + /// + public bool ThrowForPartialCookies { get; set; } + + private IUrlEncoder Encoder { get; set; } + + // Parse the "chunks:XX" to determine how many chunks there should be. + private static int ParseChunksCount(string value) + { + if (value != null && value.StartsWith("chunks:", StringComparison.Ordinal)) + { + var chunksCountString = value.Substring("chunks:".Length); + int chunksCount; + if (int.TryParse(chunksCountString, NumberStyles.None, CultureInfo.InvariantCulture, out chunksCount)) + { + return chunksCount; + } + } + return 0; + } + + /// + /// Get the reassembled cookie. Non chunked cookies are returned normally. + /// Cookies with missing chunks just have their "chunks:XX" header returned. + /// + /// + /// + /// The reassembled cookie, if any, or null. + public string GetRequestCookie([NotNull] HttpContext context, [NotNull] string key) + { + var requestCookies = context.Request.Cookies; + var value = requestCookies[key]; + var chunksCount = ParseChunksCount(value); + if (chunksCount > 0) + { + var quoted = false; + var chunks = new string[chunksCount]; + for (var chunkId = 1; chunkId <= chunksCount; chunkId++) + { + var chunk = requestCookies[key + "C" + chunkId.ToString(CultureInfo.InvariantCulture)]; + if (chunk == null) + { + if (ThrowForPartialCookies) + { + var totalSize = 0; + for (int i = 0; i < chunkId - 1; i++) + { + totalSize += chunks[i].Length; + } + throw new FormatException( + string.Format(CultureInfo.CurrentCulture, Resources.Exception_ImcompleteChunkedCookie, chunkId - 1, chunksCount, totalSize)); + } + // Missing chunk, abort by returning the original cookie value. It may have been a false positive? + return value; + } + if (IsQuoted(chunk)) + { + // Note: Since we assume these cookies were generated by our code, then we can assume that if one cookie has quotes then they all do. + quoted = true; + chunk = RemoveQuotes(chunk); + } + chunks[chunkId - 1] = chunk; + } + var merged = string.Join(string.Empty, chunks); + if (quoted) + { + merged = Quote(merged); + } + return merged; + } + return value; + } + + /// + /// Appends a new response cookie to the Set-Cookie header. If the cookie is larger than the given size limit + /// then it will be broken down into multiple cookies as follows: + /// Set-Cookie: CookieName=chunks:3; path=/ + /// Set-Cookie: CookieNameC1=Segment1; path=/ + /// Set-Cookie: CookieNameC2=Segment2; path=/ + /// Set-Cookie: CookieNameC3=Segment3; path=/ + /// + /// + /// + /// + /// + public void AppendResponseCookie([NotNull] HttpContext context, [NotNull] string key, string value, [NotNull] CookieOptions options) + { + var escapedKey = Encoder.UrlEncode(key); + + var template = new SetCookieHeaderValue(escapedKey) + { + Domain = options.Domain, + Expires = options.Expires, + HttpOnly = options.HttpOnly, + Path = options.Path, + Secure = options.Secure, + }; + + var templateLength = template.ToString().Length; + + value = value ?? string.Empty; + var quoted = false; + if (IsQuoted(value)) + { + quoted = true; + value = RemoveQuotes(value); + } + var escapedValue = Encoder.UrlEncode(value); + + // Normal cookie + var responseHeaders = context.Response.Headers; + if (!ChunkSize.HasValue || ChunkSize.Value > templateLength + escapedValue.Length + (quoted ? 2 : 0)) + { + template.Value = quoted ? Quote(escapedValue) : escapedValue; + responseHeaders.AppendValues(Constants.Headers.SetCookie, template.ToString()); + } + else if (ChunkSize.Value < templateLength + (quoted ? 2 : 0) + 10) + { + // 10 is the minimum data we want to put in an individual cookie, including the cookie chunk identifier "CXX". + // No room for data, we can't chunk the options and name + throw new InvalidOperationException(Resources.Exception_CookieLimitTooSmall); + } + else + { + // Break the cookie down into multiple cookies. + // Key = CookieName, value = "Segment1Segment2Segment2" + // Set-Cookie: CookieName=chunks:3; path=/ + // Set-Cookie: CookieNameC1="Segment1"; path=/ + // Set-Cookie: CookieNameC2="Segment2"; path=/ + // Set-Cookie: CookieNameC3="Segment3"; path=/ + var dataSizePerCookie = ChunkSize.Value - templateLength - (quoted ? 2 : 0) - 3; // Budget 3 chars for the chunkid. + var cookieChunkCount = (int)Math.Ceiling(escapedValue.Length * 1.0 / dataSizePerCookie); + + template.Value = "chunks:" + cookieChunkCount.ToString(CultureInfo.InvariantCulture); + responseHeaders.AppendValues(Constants.Headers.SetCookie, template.ToString()); + + var chunks = new string[cookieChunkCount]; + var offset = 0; + for (var chunkId = 1; chunkId <= cookieChunkCount; chunkId++) + { + var remainingLength = escapedValue.Length - offset; + var length = Math.Min(dataSizePerCookie, remainingLength); + var segment = escapedValue.Substring(offset, length); + offset += length; + + template.Name = escapedKey + "C" + chunkId.ToString(CultureInfo.InvariantCulture); + template.Value = quoted ? Quote(segment) : segment; + chunks[chunkId - 1] = template.ToString(); + } + responseHeaders.AppendValues(Constants.Headers.SetCookie, chunks); + } + } + + /// + /// Deletes the cookie with the given key by setting an expired state. If a matching chunked cookie exists on + /// the request, delete each chunk. + /// + /// + /// + /// + public void DeleteCookie([NotNull] HttpContext context, [NotNull] string key, [NotNull] CookieOptions options) + { + var escapedKey = Encoder.UrlEncode(key); + var keys = new List(); + keys.Add(escapedKey + "="); + + var requestCookie = context.Request.Cookies[key]; + var chunks = ParseChunksCount(requestCookie); + if (chunks > 0) + { + for (int i = 1; i <= chunks + 1; i++) + { + var subkey = escapedKey + "C" + i.ToString(CultureInfo.InvariantCulture); + keys.Add(subkey + "="); + } + } + + var domainHasValue = !string.IsNullOrEmpty(options.Domain); + var pathHasValue = !string.IsNullOrEmpty(options.Path); + + Func rejectPredicate; + Func predicate = value => keys.Any(k => value.StartsWith(k, StringComparison.OrdinalIgnoreCase)); + if (domainHasValue) + { + rejectPredicate = value => predicate(value) && value.IndexOf("domain=" + options.Domain, StringComparison.OrdinalIgnoreCase) != -1; + } + else if (pathHasValue) + { + rejectPredicate = value => predicate(value) && value.IndexOf("path=" + options.Path, StringComparison.OrdinalIgnoreCase) != -1; + } + else + { + rejectPredicate = value => predicate(value); + } + + var responseHeaders = context.Response.Headers; + var existingValues = responseHeaders.GetValues(Constants.Headers.SetCookie); + if (existingValues != null) + { + responseHeaders.SetValues(Constants.Headers.SetCookie, existingValues.Where(value => !rejectPredicate(value)).ToArray()); + } + + AppendResponseCookie( + context, + key, + string.Empty, + new CookieOptions() + { + Path = options.Path, + Domain = options.Domain, + Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + }); + + for (int i = 1; i <= chunks; i++) + { + AppendResponseCookie( + context, + key + "C" + i.ToString(CultureInfo.InvariantCulture), + string.Empty, + new CookieOptions() + { + Path = options.Path, + Domain = options.Domain, + Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + }); + } + } + + private static bool IsQuoted([NotNull] string value) + { + return value.Length >= 2 && value[0] == '"' && value[value.Length - 1] == '"'; + } + + private static string RemoveQuotes([NotNull] string value) + { + return value.Substring(1, value.Length - 2); + } + + private static string Quote([NotNull] string value) + { + return '"' + value + '"'; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Infrastructure/Constants.cs b/src/Microsoft.AspNet.Authentication.Cookies/Infrastructure/Constants.cs new file mode 100644 index 000000000..6865cea0f --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/Infrastructure/Constants.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure +{ + internal static class Constants + { + internal static class Headers + { + internal const string SetCookie = "Set-Cookie"; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Infrastructure/IAuthenticationSessionStore.cs b/src/Microsoft.AspNet.Authentication.Cookies/Infrastructure/IAuthenticationSessionStore.cs new file mode 100644 index 000000000..6b2886d0a --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/Infrastructure/IAuthenticationSessionStore.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure +{ + /// + /// This provides an abstract storage mechanic to preserve identity information on the server + /// while only sending a simple identifier key to the client. This is most commonly used to mitigate + /// issues with serializing large identities into cookies. + /// + public interface IAuthenticationSessionStore + { + /// + /// Store the identity ticket and return the associated key. + /// + /// The identity information to store. + /// The key that can be used to retrieve the identity later. + Task StoreAsync(AuthenticationTicket ticket); + + /// + /// Tells the store that the given identity should be updated. + /// + /// + /// + /// + Task RenewAsync(string key, AuthenticationTicket ticket); + + /// + /// Retrieves an identity from the store for the given key. + /// + /// The key associated with the identity. + /// The identity associated with the given key, or if not found. + Task RetrieveAsync(string key); + + /// + /// Remove the identity associated with the given key. + /// + /// The key associated with the identity. + /// + Task RemoveAsync(string key); + } +} diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Infrastructure/ICookieManager.cs b/src/Microsoft.AspNet.Authentication.Cookies/Infrastructure/ICookieManager.cs new file mode 100644 index 000000000..cffde3869 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/Infrastructure/ICookieManager.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure +{ + /// + /// This is used by the CookieAuthenticationMiddleware to process request and response cookies. + /// It is abstracted from the normal cookie APIs to allow for complex operations like chunking. + /// + public interface ICookieManager + { + /// + /// Retrieve a cookie of the given name from the request. + /// + /// + /// + /// + string GetRequestCookie(HttpContext context, string key); + + /// + /// Append the given cookie to the response. + /// + /// + /// + /// + /// + void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options); + + /// + /// Append a delete cookie to the response. + /// + /// + /// + /// + void DeleteCookie(HttpContext context, string key, CookieOptions options); + } +} diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Microsoft.AspNet.Authentication.Cookies.xproj b/src/Microsoft.AspNet.Authentication.Cookies/Microsoft.AspNet.Authentication.Cookies.xproj new file mode 100644 index 000000000..bc93d6322 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/Microsoft.AspNet.Authentication.Cookies.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + fc152cc4-054b-457e-8d91-389c5de3c561 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieApplyRedirectContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieApplyRedirectContext.cs similarity index 78% rename from src/Microsoft.AspNet.Security.Cookies/Notifications/CookieApplyRedirectContext.cs rename to src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieApplyRedirectContext.cs index 9d0aca22c..9083ed817 100644 --- a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieApplyRedirectContext.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieApplyRedirectContext.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics.CodeAnalysis; using Microsoft.AspNet.Http; -using Microsoft.AspNet.Security.Notifications; +using Microsoft.AspNet.Authentication.Notifications; -namespace Microsoft.AspNet.Security.Cookies +namespace Microsoft.AspNet.Authentication.Cookies { /// /// Context passed when a Challenge, SignIn, or SignOut causes a redirect in the cookie middleware @@ -14,7 +15,7 @@ public class CookieApplyRedirectContext : BaseContext /// Creates a new context object. /// - /// The OWIN request context + /// The HTTP request context /// The cookie middleware options /// The initial redirect URI [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "Represents header value")] diff --git a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieAuthenticationNotifications.cs similarity index 58% rename from src/Microsoft.AspNet.Security.Cookies/Notifications/CookieAuthenticationNotifications.cs rename to src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieAuthenticationNotifications.cs index f3496bbc7..0c875b208 100644 --- a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieAuthenticationNotifications.cs @@ -1,9 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All 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.AspNet.Security.Cookies +namespace Microsoft.AspNet.Authentication.Cookies { /// /// This default implementation of the ICookieAuthenticationNotifications may be used if the @@ -17,22 +18,29 @@ public class CookieAuthenticationNotifications : ICookieAuthenticationNotificati /// public CookieAuthenticationNotifications() { - OnValidateIdentity = context => Task.FromResult(0); + OnValidatePrincipal = context => Task.FromResult(0); OnResponseSignIn = context => { }; + OnResponseSignedIn = context => { }; OnResponseSignOut = context => { }; OnApplyRedirect = DefaultBehavior.ApplyRedirect; + OnException = context => { }; } /// /// A delegate assigned to this property will be invoked when the related method is called /// - public Func OnValidateIdentity { get; set; } + public Func OnValidatePrincipal { get; set; } /// /// A delegate assigned to this property will be invoked when the related method is called /// public Action OnResponseSignIn { get; set; } + /// + /// A delegate assigned to this property will be invoked when the related method is called + /// + public Action OnResponseSignedIn { get; set; } + /// /// A delegate assigned to this property will be invoked when the related method is called /// @@ -43,14 +51,19 @@ public CookieAuthenticationNotifications() /// public Action OnApplyRedirect { get; set; } + /// + /// A delegate assigned to this property will be invoked when the related method is called + /// + public Action OnException { get; set; } + /// /// Implements the interface method by invoking the related delegate method /// /// /// - public virtual Task ValidateIdentity(CookieValidateIdentityContext context) + public virtual Task ValidatePrincipal(CookieValidatePrincipalContext context) { - return OnValidateIdentity.Invoke(context); + return OnValidatePrincipal.Invoke(context); } /// @@ -62,6 +75,15 @@ public virtual void ResponseSignIn(CookieResponseSignInContext context) OnResponseSignIn.Invoke(context); } + /// + /// Implements the interface method by invoking the related delegate method + /// + /// + public virtual void ResponseSignedIn(CookieResponseSignedInContext context) + { + OnResponseSignedIn.Invoke(context); + } + /// /// Implements the interface method by invoking the related delegate method /// @@ -72,12 +94,21 @@ public virtual void ResponseSignOut(CookieResponseSignOutContext context) } /// - /// Called when a Challenge, SignIn, or SignOut causes a redirect in the cookie middleware + /// Implements the interface method by invoking the related delegate method /// /// Contains information about the event - public void ApplyRedirect(CookieApplyRedirectContext context) + public virtual void ApplyRedirect(CookieApplyRedirectContext context) { OnApplyRedirect.Invoke(context); } + + /// + /// Implements the interface method by invoking the related delegate method + /// + /// Contains information about the event + public virtual void Exception(CookieExceptionContext context) + { + OnException.Invoke(context); + } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieExceptionContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieExceptionContext.cs new file mode 100644 index 000000000..0d51f95d4 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieExceptionContext.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.Notifications; + +namespace Microsoft.AspNet.Authentication.Cookies +{ + /// + /// Context object passed to the ICookieAuthenticationProvider method Exception. + /// + public class CookieExceptionContext : BaseContext + { + /// + /// Creates a new instance of the context object. + /// + /// The HTTP request context + /// The middleware options + /// The location of the exception + /// The exception thrown. + /// The current ticket, if any. + public CookieExceptionContext( + HttpContext context, + CookieAuthenticationOptions options, + ExceptionLocation location, + Exception exception, + AuthenticationTicket ticket) + : base(context, options) + { + Location = location; + Exception = exception; + Rethrow = true; + Ticket = ticket; + } + + /// + /// The code paths where exceptions may be reported. + /// + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Scope = "type", + Target = "Microsoft.Owin.Security.Cookies.CookieExceptionContext+ExceptionLocation", Justification = "It is a directly related option.")] + public enum ExceptionLocation + { + /// + /// The exception was reported in the Authenticate code path. + /// + Authenticate, + + /// + /// The exception was reported in the ApplyResponseGrant code path, during sign-in, sign-out, or refresh. + /// + ApplyResponseGrant, + + /// + /// The exception was reported in the ApplyResponseChallenge code path, during redirect generation. + /// + ApplyResponseChallenge, + } + + /// + /// The code path the exception occurred in. + /// + public ExceptionLocation Location { get; private set; } + + /// + /// The exception thrown. + /// + public Exception Exception { get; private set; } + + /// + /// True if the exception should be re-thrown (default), false if it should be suppressed. + /// + public bool Rethrow { get; set; } + + /// + /// The current authentication ticket, if any. + /// In the AuthenticateAsync code path, if the given exception is not re-thrown then this ticket + /// will be returned to the application. The ticket may be replaced if needed. + /// + public AuthenticationTicket Ticket { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieResponseSignInContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieResponseSignInContext.cs similarity index 65% rename from src/Microsoft.AspNet.Security.Cookies/Notifications/CookieResponseSignInContext.cs rename to src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieResponseSignInContext.cs index 375c85181..f894231ae 100644 --- a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieResponseSignInContext.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieResponseSignInContext.cs @@ -1,11 +1,12 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET 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.AspNet.Http; -using Microsoft.AspNet.Http.Security; -using Microsoft.AspNet.Security.Notifications; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Authentication.Notifications; -namespace Microsoft.AspNet.Security.Cookies +namespace Microsoft.AspNet.Authentication.Cookies { /// /// Context object passed to the ICookieAuthenticationProvider method ResponseSignIn. @@ -15,37 +16,37 @@ public class CookieResponseSignInContext : BaseContext /// Creates a new instance of the context object. /// - /// The OWIN request context + /// The HTTP request context /// The middleware options - /// Initializes AuthenticationType property - /// Initializes Identity property + /// Initializes AuthenticationScheme property + /// Initializes Principal property /// Initializes Extra property /// Initializes options for the authentication cookie. public CookieResponseSignInContext( HttpContext context, CookieAuthenticationOptions options, - string authenticationType, - ClaimsIdentity identity, + string authenticationScheme, + ClaimsPrincipal principal, AuthenticationProperties properties, CookieOptions cookieOptions) : base(context, options) { - AuthenticationType = authenticationType; - Identity = identity; + AuthenticationScheme = authenticationScheme; + Principal = principal; Properties = properties; CookieOptions = cookieOptions; } /// - /// The name of the AuthenticationType creating a cookie + /// The name of the AuthenticationScheme creating a cookie /// - public string AuthenticationType { get; private set; } + public string AuthenticationScheme { get; private set; } /// /// Contains the claims about to be converted into the outgoing cookie. /// May be replaced or altered during the ResponseSignIn call. /// - public ClaimsIdentity Identity { get; set; } + public ClaimsPrincipal Principal { get; set; } /// /// Contains the extra data about to be contained in the outgoing cookie. diff --git a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieResponseSignOutContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieResponseSignOutContext.cs similarity index 72% rename from src/Microsoft.AspNet.Security.Cookies/Notifications/CookieResponseSignOutContext.cs rename to src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieResponseSignOutContext.cs index 7dbd45137..260a31ad9 100644 --- a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieResponseSignOutContext.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieResponseSignOutContext.cs @@ -1,9 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .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.AspNet.Http; -using Microsoft.AspNet.Security.Notifications; +using Microsoft.AspNet.Authentication.Notifications; -namespace Microsoft.AspNet.Security.Cookies +namespace Microsoft.AspNet.Authentication.Cookies { /// /// Context object passed to the ICookieAuthenticationProvider method ResponseSignOut @@ -24,7 +25,7 @@ public CookieResponseSignOutContext(HttpContext context, CookieAuthenticationOpt /// /// The options for creating the outgoing cookie. - /// May be replace or altered during the ResponseSignIn call. + /// May be replace or altered during the ResponseSignOut call. /// public CookieOptions CookieOptions { diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieResponseSignedInContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieResponseSignedInContext.cs new file mode 100644 index 000000000..df88fef98 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieResponseSignedInContext.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Authentication.Notifications; + +namespace Microsoft.AspNet.Authentication.Cookies +{ + /// + /// Context object passed to the ICookieAuthenticationNotifications method ResponseSignedIn. + /// + public class CookieResponseSignedInContext : BaseContext + { + /// + /// Creates a new instance of the context object. + /// + /// The HTTP request context + /// The middleware options + /// Initializes AuthenticationScheme property + /// Initializes Principal property + /// Initializes Properties property + public CookieResponseSignedInContext( + HttpContext context, + CookieAuthenticationOptions options, + string authenticationScheme, + ClaimsPrincipal principal, + AuthenticationProperties properties) + : base(context, options) + { + AuthenticationScheme = authenticationScheme; + Principal = principal; + Properties = properties; + } + + /// + /// The name of the AuthenticationScheme creating a cookie + /// + public string AuthenticationScheme { get; private set; } + + /// + /// Contains the claims that were converted into the outgoing cookie. + /// + public ClaimsPrincipal Principal { get; private set; } + + /// + /// Contains the extra data that was contained in the outgoing cookie. + /// + public AuthenticationProperties Properties { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieValidateIdentityContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieValidateIdentityContext.cs new file mode 100644 index 000000000..ec795c468 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieValidateIdentityContext.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Security.Principal; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Authentication.Notifications; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authentication.Cookies +{ + /// + /// Context object passed to the ICookieAuthenticationProvider method ValidatePrincipal. + /// + public class CookieValidatePrincipalContext : BaseContext + { + /// + /// Creates a new instance of the context object. + /// + /// + /// Contains the initial values for identity and extra data + /// + public CookieValidatePrincipalContext([NotNull] HttpContext context, [NotNull] AuthenticationTicket ticket, [NotNull] CookieAuthenticationOptions options) + : base(context, options) + { + Principal = ticket.Principal; + Properties = ticket.Properties; + } + + /// + /// Contains the claims principal arriving with the request. May be altered to change the + /// details of the authenticated user. + /// + public ClaimsPrincipal Principal { get; private set; } + + /// + /// Contains the extra meta-data arriving with the request ticket. May be altered. + /// + public AuthenticationProperties Properties { get; private set; } + + /// + /// Called to replace the claims principal. The supplied principal will replace the value of the + /// Principal property, which determines the identity of the authenticated request. + /// + /// The identity used as the replacement + public void ReplacePrincipal(IPrincipal principal) + { + Principal = new ClaimsPrincipal(principal); + } + + /// + /// Called to reject the incoming principal. This may be done if the application has determined the + /// account is no longer active, and the request should be treated as if it was anonymous. + /// + public void RejectPrincipal() + { + Principal = null; + } + } +} diff --git a/src/Microsoft.AspNet.Security.Cookies/Notifications/DefaultBehavior.cs b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/DefaultBehavior.cs similarity index 78% rename from src/Microsoft.AspNet.Security.Cookies/Notifications/DefaultBehavior.cs rename to src/Microsoft.AspNet.Authentication.Cookies/Notifications/DefaultBehavior.cs index 98b7da1c8..9807bbdf4 100644 --- a/src/Microsoft.AspNet.Security.Cookies/Notifications/DefaultBehavior.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/DefaultBehavior.cs @@ -1,10 +1,12 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. 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.AspNet.Http; using Newtonsoft.Json; -namespace Microsoft.AspNet.Security.Cookies +namespace Microsoft.AspNet.Authentication.Cookies { internal static class DefaultBehavior { @@ -12,7 +14,7 @@ internal static class DefaultBehavior { if (IsAjaxRequest(context.Request)) { - string jsonResponse = JsonConvert.SerializeObject(new + var jsonResponse = JsonConvert.SerializeObject(new { status = context.Response.StatusCode, headers = new @@ -32,7 +34,7 @@ internal static class DefaultBehavior private static bool IsAjaxRequest(HttpRequest request) { - IReadableStringCollection query = request.Query; + var query = request.Query; if (query != null) { if (query["X-Requested-With"] == "XMLHttpRequest") @@ -41,7 +43,7 @@ private static bool IsAjaxRequest(HttpRequest request) } } - IHeaderDictionary headers = request.Headers; + var headers = request.Headers; if (headers != null) { if (headers["X-Requested-With"] == "XMLHttpRequest") diff --git a/src/Microsoft.AspNet.Security.Cookies/Notifications/ICookieAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/ICookieAuthenticationNotifications.cs similarity index 56% rename from src/Microsoft.AspNet.Security.Cookies/Notifications/ICookieAuthenticationNotifications.cs rename to src/Microsoft.AspNet.Authentication.Cookies/Notifications/ICookieAuthenticationNotifications.cs index c9c07d227..3364207af 100644 --- a/src/Microsoft.AspNet.Security.Cookies/Notifications/ICookieAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/ICookieAuthenticationNotifications.cs @@ -1,8 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System.Threading.Tasks; -namespace Microsoft.AspNet.Security.Cookies +namespace Microsoft.AspNet.Authentication.Cookies { /// /// Specifies callback methods which the invokes to enable developer control over the authentication process. /> @@ -10,12 +12,12 @@ namespace Microsoft.AspNet.Security.Cookies public interface ICookieAuthenticationNotifications { /// - /// Called each time a request identity has been validated by the middleware. By implementing this method the - /// application may alter or reject the identity which has arrived with the request. + /// Called each time a request principal has been validated by the middleware. By implementing this method the + /// application may alter or reject the principal which has arrived with the request. /// /// Contains information about the login session as well as the user . /// A representing the completed operation. - Task ValidateIdentity(CookieValidateIdentityContext context); + Task ValidatePrincipal(CookieValidatePrincipalContext context); /// /// Called when an endpoint has provided sign in information before it is converted into a cookie. By @@ -24,6 +26,12 @@ public interface ICookieAuthenticationNotifications /// Contains information about the login session as well as the user . void ResponseSignIn(CookieResponseSignInContext context); + /// + /// Called when an endpoint has provided sign in information after it is converted into a cookie. + /// + /// Contains information about the login session as well as the user . + void ResponseSignedIn(CookieResponseSignedInContext context); + /// /// Called when a Challenge, SignIn, or SignOut causes a redirect in the cookie middleware /// @@ -31,9 +39,15 @@ public interface ICookieAuthenticationNotifications void ApplyRedirect(CookieApplyRedirectContext context); /// - /// + /// Called during the sign-out flow to augment the cookie cleanup process. /// /// Contains information about the login session as well as information about the authentication cookie. void ResponseSignOut(CookieResponseSignOutContext context); + + /// + /// Called when an exception occurs during request or response processing. + /// + /// Contains information about the exception that occurred + void Exception(CookieExceptionContext context); } } diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Authentication.Cookies/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..025a94598 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +[assembly: AssemblyMetadata("Serviceable", "True")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.Cookies/Resources.Designer.cs new file mode 100644 index 000000000..ef0e62fb4 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/Resources.Designer.cs @@ -0,0 +1,85 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34003 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Authentication.Cookies { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Authentication.Cookies.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The cookie key and options are larger than ChunksSize, leaving no room for data.. + /// + internal static string Exception_CookieLimitTooSmall + { + get + { + return ResourceManager.GetString("Exception_CookieLimitTooSmall", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded.. + /// + internal static string Exception_ImcompleteChunkedCookie + { + get + { + return ResourceManager.GetString("Exception_ImcompleteChunkedCookie", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Resources.resx b/src/Microsoft.AspNet.Authentication.Cookies/Resources.resx new file mode 100644 index 000000000..71debecfa --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 cookie key and options are larger than ChunksSize, leaving no room for data. + + + The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Cookies/project.json b/src/Microsoft.AspNet.Authentication.Cookies/project.json new file mode 100644 index 000000000..28127904d --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Cookies/project.json @@ -0,0 +1,14 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET middleware that enables an application to use cookie based authentication, similar to ASP.NET's forms authentication.", + "dependencies": { + "Microsoft.AspNet.Authentication": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" }, + "Microsoft.Framework.WebEncoders": "1.0.0-*", + "Newtonsoft.Json": "6.0.6" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAppBuilderExtensions.cs b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAppBuilderExtensions.cs new file mode 100644 index 000000000..24e1e0726 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAppBuilderExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Authentication.Facebook; +using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods for using . + /// + public static class FacebookAppBuilderExtensions + { + /// + /// Authenticate users using Facebook. + /// + /// The passed to the configure method. + /// The updated . + public static IApplicationBuilder UseFacebookAuthentication([NotNull] this IApplicationBuilder app, Action configureOptions = null, string optionsName = "") + { + return app.UseMiddleware( + new ConfigureOptions(configureOptions ?? (o => { })) + { + Name = optionsName + }); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationDefaults.cs b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationDefaults.cs new file mode 100644 index 000000000..54d48cbc5 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationDefaults.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Authentication.Facebook +{ + public static class FacebookAuthenticationDefaults + { + public const string AuthenticationScheme = "Facebook"; + + public const string AuthorizationEndpoint = "https://www.facebook.com/v2.2/dialog/oauth"; + + public const string TokenEndpoint = "https://graph.facebook.com/v2.2/oauth/access_token"; + + public const string UserInformationEndpoint = "https://graph.facebook.com/v2.2/me"; + } +} diff --git a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationHandler.cs new file mode 100644 index 000000000..291956409 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationHandler.cs @@ -0,0 +1,126 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Net.Http; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.OAuth; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Http.Extensions; +using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.WebUtilities; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Authentication.Facebook +{ + internal class FacebookAuthenticationHandler : OAuthAuthenticationHandler + { + public FacebookAuthenticationHandler(HttpClient httpClient) + : base(httpClient) + { + } + + protected override async Task ExchangeCodeAsync(string code, string redirectUri) + { + var queryBuilder = new QueryBuilder() + { + { "grant_type", "authorization_code" }, + { "code", code }, + { "redirect_uri", redirectUri }, + { "client_id", Options.AppId }, + { "client_secret", Options.AppSecret }, + }; + + var tokenResponse = await Backchannel.GetAsync(Options.TokenEndpoint + queryBuilder.ToString(), Context.RequestAborted); + tokenResponse.EnsureSuccessStatusCode(); + var oauthTokenResponse = await tokenResponse.Content.ReadAsStringAsync(); + + var form = new FormCollection(FormReader.ReadForm(oauthTokenResponse)); + var response = new JObject(); + foreach (string key in form.Keys) + { + response.Add(string.Equals(key, "expires", StringComparison.OrdinalIgnoreCase) ? "expires_in" : key, form[key]); + } + // The refresh token is not available. + return new TokenResponse(response); + } + + protected override async Task GetUserInformationAsync(AuthenticationProperties properties, TokenResponse tokens) + { + var graphAddress = Options.UserInformationEndpoint + "?access_token=" + UrlEncoder.UrlEncode(tokens.AccessToken); + if (Options.SendAppSecretProof) + { + graphAddress += "&appsecret_proof=" + GenerateAppSecretProof(tokens.AccessToken); + } + + var graphResponse = await Backchannel.GetAsync(graphAddress, Context.RequestAborted); + graphResponse.EnsureSuccessStatusCode(); + var text = await graphResponse.Content.ReadAsStringAsync(); + var user = JObject.Parse(text); + + var context = new FacebookAuthenticatedContext(Context, Options, user, tokens); + var identity = new ClaimsIdentity( + Options.ClaimsIssuer, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType); + if (!string.IsNullOrEmpty(context.Id)) + { + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + if (!string.IsNullOrEmpty(context.UserName)) + { + identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.UserName, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + if (!string.IsNullOrEmpty(context.Email)) + { + identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + if (!string.IsNullOrEmpty(context.Name)) + { + identity.AddClaim(new Claim("urn:facebook:name", context.Name, ClaimValueTypes.String, Options.ClaimsIssuer)); + + // Many Facebook accounts do not set the UserName field. Fall back to the Name field instead. + if (string.IsNullOrEmpty(context.UserName)) + { + identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.Name, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + } + if (!string.IsNullOrEmpty(context.Link)) + { + identity.AddClaim(new Claim("urn:facebook:link", context.Link, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + context.Properties = properties; + context.Principal = new ClaimsPrincipal(identity); + + await Options.Notifications.Authenticated(context); + + return new AuthenticationTicket(context.Principal, context.Properties, context.Options.AuthenticationScheme); + } + + private string GenerateAppSecretProof(string accessToken) + { + using (var algorithm = new HMACSHA256(Encoding.ASCII.GetBytes(Options.AppSecret))) + { + var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(accessToken)); + var builder = new StringBuilder(); + for (int i = 0; i < hash.Length; i++) + { + builder.Append(hash[i].ToString("x2", CultureInfo.InvariantCulture)); + } + return builder.ToString(); + } + } + + protected override string FormatScope() + { + // 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); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationMiddleware.cs new file mode 100644 index 000000000..4eab19ddb --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationMiddleware.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using Microsoft.AspNet.Authentication.OAuth; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.DataProtection; +using Microsoft.Framework.Internal; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Authentication.Facebook +{ + /// + /// An ASP.NET middleware for authenticating users using Facebook. + /// + public class FacebookAuthenticationMiddleware : OAuthAuthenticationMiddleware + { + /// + /// Initializes a new . + /// + /// The next middleware in the application pipeline to invoke. + /// + /// + /// Configuration options for the middleware. + public FacebookAuthenticationMiddleware( + [NotNull] RequestDelegate next, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IUrlEncoder encoder, + [NotNull] IOptions externalOptions, + [NotNull] IOptions options, + ConfigureOptions configureOptions = null) + : base(next, dataProtectionProvider, loggerFactory, encoder, externalOptions, options, configureOptions) + { + if (string.IsNullOrWhiteSpace(Options.AppId)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.AppId))); + } + if (string.IsNullOrWhiteSpace(Options.AppSecret)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.AppSecret))); + } + + if (Options.Notifications == null) + { + Options.Notifications = new FacebookAuthenticationNotifications(); + } + } + + /// + /// Provides the object for processing authentication-related requests. + /// + /// An configured with the supplied to the constructor. + protected override AuthenticationHandler CreateHandler() + { + return new FacebookAuthenticationHandler(Backchannel); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationOptions.cs new file mode 100644 index 000000000..c7ba48aab --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationOptions.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.OAuth; + +namespace Microsoft.AspNet.Authentication.Facebook +{ + /// + /// Configuration options for . + /// + public class FacebookAuthenticationOptions : OAuthAuthenticationOptions + { + /// + /// Initializes a new . + /// + public FacebookAuthenticationOptions() + { + AuthenticationScheme = FacebookAuthenticationDefaults.AuthenticationScheme; + Caption = AuthenticationScheme; + CallbackPath = new PathString("/signin-facebook"); + SendAppSecretProof = true; + AuthorizationEndpoint = FacebookAuthenticationDefaults.AuthorizationEndpoint; + TokenEndpoint = FacebookAuthenticationDefaults.TokenEndpoint; + UserInformationEndpoint = FacebookAuthenticationDefaults.UserInformationEndpoint; + } + + // Facebook uses a non-standard term for this field. + /// + /// Gets or sets the Facebook-assigned appId. + /// + public string AppId + { + get { return ClientId; } + set { ClientId = value; } + } + + // Facebook uses a non-standard term for this field. + /// + /// Gets or sets the Facebook-assigned app secret. + /// + public string AppSecret + { + get { return ClientSecret; } + set { ClientSecret = value; } + } + + /// + /// Gets or sets if the appsecret_proof should be generated and sent with Facebook API calls. + /// This is enabled by default. + /// + public bool SendAppSecretProof { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Facebook/FacebookServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Authentication.Facebook/FacebookServiceCollectionExtensions.cs new file mode 100644 index 000000000..911dd18b8 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/FacebookServiceCollectionExtensions.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 Microsoft.AspNet.Authentication.Facebook; +using Microsoft.Framework.Configuration; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.DependencyInjection +{ + /// + /// Extension methods for using . + /// + public static class FacebookServiceCollectionExtensions + { + public static IServiceCollection ConfigureFacebookAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure) + { + return services.ConfigureFacebookAuthentication(configure, optionsName: ""); + } + + public static IServiceCollection ConfigureFacebookAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure, string optionsName) + { + return services.Configure(configure, optionsName); + } + + public static IServiceCollection ConfigureFacebookAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config) + { + return services.ConfigureFacebookAuthentication(config, optionsName: ""); + } + + public static IServiceCollection ConfigureFacebookAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config, string optionsName) + { + return services.Configure(config, optionsName); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Facebook/Microsoft.AspNet.Authentication.Facebook.xproj b/src/Microsoft.AspNet.Authentication.Facebook/Microsoft.AspNet.Authentication.Facebook.xproj new file mode 100644 index 000000000..68ee50c4b --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/Microsoft.AspNet.Authentication.Facebook.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + eeaaee68-607b-4e33-af3e-45c66b4dba5a + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticatedContext.cs b/src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticatedContext.cs new file mode 100644 index 000000000..321b78f8b --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticatedContext.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.OAuth; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Authentication.Facebook +{ + /// + /// Contains information about the login session as well as the user . + /// + public class FacebookAuthenticatedContext : OAuthAuthenticatedContext + { + /// + /// Initializes a new . + /// + /// The HTTP environment. + /// The JSON-serialized user. + /// The Facebook Access token. + public FacebookAuthenticatedContext(HttpContext context, OAuthAuthenticationOptions options, JObject user, TokenResponse tokens) + : base(context, options, user, tokens) + { + Id = TryGetValue(user, "id"); + Name = TryGetValue(user, "name"); + Link = TryGetValue(user, "link"); + UserName = TryGetValue(user, "username"); + Email = TryGetValue(user, "email"); + } + + /// + /// Gets the Facebook user ID. + /// + public string Id { get; private set; } + + /// + /// Gets the user's name. + /// + public string Name { get; private set; } + + /// + /// Gets the user's link. + /// + public string Link { get; private set; } + + /// + /// Gets the Facebook username. + /// + public string UserName { get; private set; } + + /// + /// Gets the Facebook email. + /// + public string Email { get; private set; } + + private static string TryGetValue(JObject user, string propertyName) + { + JToken value; + return user.TryGetValue(propertyName, out value) ? value.ToString() : null; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticationNotifications.cs new file mode 100644 index 000000000..acee5b54e --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticationNotifications.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.OAuth; + +namespace Microsoft.AspNet.Authentication.Facebook +{ + /// + /// The default implementation. + /// + public class FacebookAuthenticationNotifications : OAuthAuthenticationNotifications, IFacebookAuthenticationNotifications + { + /// + /// Initializes a new . + /// + public FacebookAuthenticationNotifications() + { + OnAuthenticated = context => Task.FromResult(null); + } + + /// + /// Gets or sets the function that is invoked when the Authenticated method is invoked. + /// + public Func OnAuthenticated { get; set; } + + /// + /// Invoked whenever Facebook succesfully authenticates a user. + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task Authenticated(FacebookAuthenticatedContext context) + { + return OnAuthenticated(context); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Facebook/Notifications/IFacebookAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.Facebook/Notifications/IFacebookAuthenticationNotifications.cs new file mode 100644 index 000000000..aff7de6ee --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/Notifications/IFacebookAuthenticationNotifications.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.OAuth; + +namespace Microsoft.AspNet.Authentication.Facebook +{ + /// + /// Specifies callback methods which the invokes to enable developer control over the authentication process. + /// + public interface IFacebookAuthenticationNotifications : IOAuthAuthenticationNotifications + { + /// + /// Invoked when Facebook succesfully authenticates a user. + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + Task Authenticated(FacebookAuthenticatedContext context); + } +} diff --git a/src/Microsoft.AspNet.Authentication.Facebook/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Authentication.Facebook/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..025a94598 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +[assembly: AssemblyMetadata("Serviceable", "True")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Facebook/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.Facebook/Resources.Designer.cs new file mode 100644 index 000000000..dd3f0e3fa --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/Resources.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.32559 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Authentication.Facebook { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Authentication.Facebook.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The '{0}' option must be provided.. + /// + internal static string Exception_OptionMustBeProvided { + get { + return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Facebook/Resources.resx b/src/Microsoft.AspNet.Authentication.Facebook/Resources.resx new file mode 100644 index 000000000..56ef7f56b --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 '{0}' option must be provided. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Facebook/project.json b/src/Microsoft.AspNet.Authentication.Facebook/project.json new file mode 100644 index 000000000..332385ca7 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/project.json @@ -0,0 +1,17 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET 5 middleware that enables an application to support Facebook's OAuth 2.0 authentication workflow.", + "dependencies": { + "Microsoft.AspNet.Authentication.OAuth": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" }, + "Newtonsoft.Json": "6.0.6" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { + "dependencies": { + "System.Security.Cryptography.Hashing.Algorithms": "4.0.0-beta-*" + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Google/GoogleAppBuilderExtensions.cs b/src/Microsoft.AspNet.Authentication.Google/GoogleAppBuilderExtensions.cs new file mode 100644 index 000000000..e00dcffdd --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/GoogleAppBuilderExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Authentication.Google; +using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods for using . + /// + public static class GoogleAppBuilderExtensions + { + /// + /// Authenticate users using Google OAuth 2.0. + /// + /// The passed to the configure method. + /// Used to configure Middleware options. + /// Name of the options instance to be used + /// The updated . + public static IApplicationBuilder UseGoogleAuthentication([NotNull] this IApplicationBuilder app, Action configureOptions = null, string optionsName = "") + { + return app.UseMiddleware( + new ConfigureOptions(configureOptions ?? (o => { })) + { + Name = optionsName + }); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationDefaults.cs b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationDefaults.cs new file mode 100644 index 000000000..88aa4f1f1 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationDefaults.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Authentication.Google +{ + public static class GoogleAuthenticationDefaults + { + public const string AuthenticationScheme = "Google"; + + public const string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/auth"; + + public const string TokenEndpoint = "https://accounts.google.com/o/oauth2/token"; + + public const string UserInformationEndpoint = "https://www.googleapis.com/plus/v1/people/me"; + } +} diff --git a/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationHandler.cs new file mode 100644 index 000000000..621e77d83 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationHandler.cs @@ -0,0 +1,123 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Authentication.OAuth; +using Microsoft.AspNet.WebUtilities; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Authentication.Google +{ + internal class GoogleAuthenticationHandler : OAuthAuthenticationHandler + { + public GoogleAuthenticationHandler(HttpClient httpClient) + : base(httpClient) + { + } + + protected override async Task GetUserInformationAsync(AuthenticationProperties properties, TokenResponse tokens) + { + // Get the Google user + var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + var graphResponse = await Backchannel.SendAsync(request, Context.RequestAborted); + graphResponse.EnsureSuccessStatusCode(); + var text = await graphResponse.Content.ReadAsStringAsync(); + var user = JObject.Parse(text); + + var context = new GoogleAuthenticatedContext(Context, Options, user, tokens); + var identity = new ClaimsIdentity( + Options.ClaimsIssuer, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType); + + if (!string.IsNullOrEmpty(context.Id)) + { + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, + ClaimValueTypes.String, Options.ClaimsIssuer)); + } + if (!string.IsNullOrEmpty(context.GivenName)) + { + identity.AddClaim(new Claim(ClaimTypes.GivenName, context.GivenName, + ClaimValueTypes.String, Options.ClaimsIssuer)); + } + if (!string.IsNullOrEmpty(context.FamilyName)) + { + identity.AddClaim(new Claim(ClaimTypes.Surname, context.FamilyName, + ClaimValueTypes.String, Options.ClaimsIssuer)); + } + if (!string.IsNullOrEmpty(context.Name)) + { + identity.AddClaim(new Claim(ClaimTypes.Name, context.Name, ClaimValueTypes.String, + Options.ClaimsIssuer)); + } + if (!string.IsNullOrEmpty(context.Email)) + { + identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, ClaimValueTypes.String, + Options.ClaimsIssuer)); + } + if (!string.IsNullOrEmpty(context.Profile)) + { + identity.AddClaim(new Claim("urn:google:profile", context.Profile, ClaimValueTypes.String, + Options.ClaimsIssuer)); + } + context.Properties = properties; + context.Principal = new ClaimsPrincipal(identity); + + await Options.Notifications.Authenticated(context); + + return new AuthenticationTicket(context.Principal, context.Properties, context.Options.AuthenticationScheme); + } + + // TODO: Abstract this properties override pattern into the base class? + protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) + { + var scope = FormatScope(); + + var queryStrings = new Dictionary(StringComparer.OrdinalIgnoreCase); + queryStrings.Add("response_type", "code"); + queryStrings.Add("client_id", Options.ClientId); + queryStrings.Add("redirect_uri", redirectUri); + + AddQueryString(queryStrings, properties, "scope", scope); + + AddQueryString(queryStrings, properties, "access_type", Options.AccessType); + AddQueryString(queryStrings, properties, "approval_prompt"); + AddQueryString(queryStrings, properties, "login_hint"); + + var state = Options.StateDataFormat.Protect(properties); + queryStrings.Add("state", state); + + var authorizationEndpoint = QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings); + return authorizationEndpoint; + } + + private static void AddQueryString(IDictionary queryStrings, AuthenticationProperties properties, + string name, string defaultValue = null) + { + string value; + if (!properties.Items.TryGetValue(name, out value)) + { + value = defaultValue; + } + else + { + // Remove the parameter from AuthenticationProperties so it won't be serialized to state parameter + properties.Items.Remove(name); + } + + if (value == null) + { + return; + } + + queryStrings[name] = value; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationMiddleware.cs new file mode 100644 index 000000000..db7a3c988 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationMiddleware.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.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Authentication.OAuth; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.DataProtection; +using Microsoft.Framework.Internal; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Authentication.Google +{ + /// + /// An ASP.NET middleware for authenticating users using Google OAuth 2.0. + /// + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware are not disposable.")] + public class GoogleAuthenticationMiddleware : OAuthAuthenticationMiddleware + { + /// + /// Initializes a new . + /// + /// The next middleware in the HTTP pipeline to invoke. + /// + /// + /// Configuration options for the middleware. + public GoogleAuthenticationMiddleware( + [NotNull] RequestDelegate next, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IUrlEncoder encoder, + [NotNull] IOptions externalOptions, + [NotNull] IOptions options, + ConfigureOptions configureOptions = null) + : base(next, dataProtectionProvider, loggerFactory, encoder, externalOptions, options, configureOptions) + { + if (Options.Notifications == null) + { + Options.Notifications = new GoogleAuthenticationNotifications(); + } + + if (Options.Scope.Count == 0) + { + // Google OAuth 2.0 asks for non-empty scope. If user didn't set it, set default scope to + // "openid profile email" to get basic user information. + // TODO: Should we just add these by default when we create the Options? + Options.Scope.Add("openid"); + Options.Scope.Add("profile"); + Options.Scope.Add("email"); + } + } + + /// + /// Provides the object for processing authentication-related requests. + /// + /// An configured with the supplied to the constructor. + protected override AuthenticationHandler CreateHandler() + { + return new GoogleAuthenticationHandler(Backchannel); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationOptions.cs new file mode 100644 index 000000000..e65b800ed --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationOptions.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Authentication.OAuth; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Authentication.Google +{ + /// + /// Configuration options for . + /// + public class GoogleAuthenticationOptions : OAuthAuthenticationOptions + { + /// + /// Initializes a new . + /// + public GoogleAuthenticationOptions() + { + AuthenticationScheme = GoogleAuthenticationDefaults.AuthenticationScheme; + Caption = AuthenticationScheme; + CallbackPath = new PathString("/signin-google"); + AuthorizationEndpoint = GoogleAuthenticationDefaults.AuthorizationEndpoint; + TokenEndpoint = GoogleAuthenticationDefaults.TokenEndpoint; + UserInformationEndpoint = GoogleAuthenticationDefaults.UserInformationEndpoint; + } + + /// + /// access_type. Set to 'offline' to request a refresh token. + /// + public string AccessType { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Google/GoogleServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Authentication.Google/GoogleServiceCollectionExtensions.cs new file mode 100644 index 000000000..c5f7041e4 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/GoogleServiceCollectionExtensions.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 Microsoft.AspNet.Authentication.Google; +using Microsoft.Framework.Configuration; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.DependencyInjection +{ + /// + /// Extension methods for using . + /// + public static class GoogleServiceCollectionExtensions + { + public static IServiceCollection ConfigureGoogleAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure) + { + return services.ConfigureGoogleAuthentication(configure, optionsName: ""); + } + + public static IServiceCollection ConfigureGoogleAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure, string optionsName) + { + return services.Configure(configure, optionsName); + } + + public static IServiceCollection ConfigureGoogleAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config) + { + return services.ConfigureGoogleAuthentication(config, optionsName: ""); + } + + public static IServiceCollection ConfigureGoogleAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config, string optionsName) + { + return services.Configure(config, optionsName); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Google/Microsoft.AspNet.Authentication.Google.xproj b/src/Microsoft.AspNet.Authentication.Google/Microsoft.AspNet.Authentication.Google.xproj new file mode 100644 index 000000000..f92646ce6 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/Microsoft.AspNet.Authentication.Google.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 76579c39-b829-490d-b8be-1bd35fe8412e + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Google/Notifications/GoogleAuthenticatedContext.cs b/src/Microsoft.AspNet.Authentication.Google/Notifications/GoogleAuthenticatedContext.cs new file mode 100644 index 000000000..915b80436 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/Notifications/GoogleAuthenticatedContext.cs @@ -0,0 +1,110 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Net.Http; +using System.Security.Claims; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Authentication.OAuth; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Authentication.Google +{ + /// + /// Contains information about the login session as well as the user . + /// + public class GoogleAuthenticatedContext : OAuthAuthenticatedContext + { + /// + /// Initializes a new . + /// + /// The HTTP environment. + /// The JSON-serialized Google user info. + /// Google OAuth 2.0 access token, refresh token, etc. + public GoogleAuthenticatedContext(HttpContext context, OAuthAuthenticationOptions options, JObject user, TokenResponse tokens) + : base(context, options, user, tokens) + { + Id = TryGetValue(user, "id"); + Name = TryGetValue(user, "displayName"); + GivenName = TryGetValue(user, "name", "givenName"); + FamilyName = TryGetValue(user, "name", "familyName"); + Profile = TryGetValue(user, "url"); + Email = TryGetFirstValue(user, "emails", "value"); + } + + /// + /// Gets the Google user ID. + /// + public string Id { get; private set; } + + /// + /// Gets the user's name. + /// + public string Name { get; private set; } + + /// + /// Gets the user's given name. + /// + public string GivenName { get; set; } + + /// + /// Gets the user's family name. + /// + public string FamilyName { get; set; } + + /// + /// Gets the user's profile link. + /// + public string Profile { get; private set; } + + /// + /// Gets the user's email. + /// + public string Email { get; private set; } + + private static string TryGetValue(JObject user, string propertyName) + { + JToken value; + return user.TryGetValue(propertyName, out value) ? value.ToString() : null; + } + + // Get the given subProperty from a property. + private static string TryGetValue(JObject user, string propertyName, string subProperty) + { + JToken value; + if (user.TryGetValue(propertyName, out value)) + { + var subObject = JObject.Parse(value.ToString()); + if (subObject != null && subObject.TryGetValue(subProperty, out value)) + { + return value.ToString(); + } + } + return null; + } + + // Get the given subProperty from a list property. + private static string TryGetFirstValue(JObject user, string propertyName, string subProperty) + { + JToken value; + if (user.TryGetValue(propertyName, out value)) + { + var array = JArray.Parse(value.ToString()); + if (array != null && array.Count > 0) + { + var subObject = JObject.Parse(array.First.ToString()); + if (subObject != null) + { + if (subObject.TryGetValue(subProperty, out value)) + { + return value.ToString(); + } + } + } + } + return null; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Google/Notifications/GoogleAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.Google/Notifications/GoogleAuthenticationNotifications.cs new file mode 100644 index 000000000..51269aeff --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/Notifications/GoogleAuthenticationNotifications.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.OAuth; + +namespace Microsoft.AspNet.Authentication.Google +{ + /// + /// The default implementation. + /// + public class GoogleAuthenticationNotifications : OAuthAuthenticationNotifications, IGoogleAuthenticationNotifications + { + /// + /// Initializes a new . + /// + public GoogleAuthenticationNotifications() + { + OnAuthenticated = context => Task.FromResult(null); + } + + /// + /// Gets or sets the function that is invoked when the Authenticated method is invoked. + /// + public Func OnAuthenticated { get; set; } + + /// + /// Invoked whenever Google succesfully authenticates a user. + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task Authenticated(GoogleAuthenticatedContext context) + { + return OnAuthenticated(context); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Google/Notifications/IGoogleAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.Google/Notifications/IGoogleAuthenticationNotifications.cs new file mode 100644 index 000000000..30ce69778 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/Notifications/IGoogleAuthenticationNotifications.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.OAuth; + +namespace Microsoft.AspNet.Authentication.Google +{ + /// + /// Specifies callback methods which the invokes to enable developer control over the authentication process. + /// + public interface IGoogleAuthenticationNotifications : IOAuthAuthenticationNotifications + { + /// + /// Invoked whenever Google succesfully authenticates a user. + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + Task Authenticated(GoogleAuthenticatedContext context); + } +} diff --git a/src/Microsoft.AspNet.Authentication.Google/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Authentication.Google/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..025a94598 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +[assembly: AssemblyMetadata("Serviceable", "True")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Google/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.Google/Resources.Designer.cs new file mode 100644 index 000000000..efa7d64ed --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/Resources.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34003 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Authentication.Google { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Authentication.Google.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The '{0}' option must be provided.. + /// + internal static string Exception_OptionMustBeProvided { + get { + return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.. + /// + internal static string Exception_ValidatorHandlerMismatch { + get { + return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Google/Resources.resx b/src/Microsoft.AspNet.Authentication.Google/Resources.resx new file mode 100644 index 000000000..2a19bea96 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Google/project.json b/src/Microsoft.AspNet.Authentication.Google/project.json new file mode 100644 index 000000000..b7ffdd0bc --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Google/project.json @@ -0,0 +1,12 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET 5 contains middlewares to support Google's OpenId and OAuth 2.0 authentication workflows.", + "dependencies": { + "Microsoft.AspNet.Authentication.OAuth": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" } + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + } +} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Microsoft.AspNet.Authentication.MicrosoftAccount.xproj b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Microsoft.AspNet.Authentication.MicrosoftAccount.xproj new file mode 100644 index 000000000..c15c0534e --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Microsoft.AspNet.Authentication.MicrosoftAccount.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + acb45e19-f520-4d0c-8916-b0ceb9c017fe + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs new file mode 100644 index 000000000..b7c1d0ca9 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAppBuilderExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Authentication.MicrosoftAccount; +using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods for using + /// + public static class MicrosoftAccountAuthenticationExtensions + { + public static IApplicationBuilder UseMicrosoftAccountAuthentication([NotNull] this IApplicationBuilder app, Action configureOptions = null, string optionsName = "") + { + return app.UseMiddleware( + new ConfigureOptions(configureOptions ?? (o => { })) + { + Name = optionsName + }); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationDefaults.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationDefaults.cs new file mode 100644 index 000000000..d42368cdc --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationDefaults.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Authentication.MicrosoftAccount +{ + public static class MicrosoftAccountAuthenticationDefaults + { + public const string AuthenticationScheme = "Microsoft"; + + public const string AuthorizationEndpoint = "https://login.live.com/oauth20_authorize.srf"; + + public const string TokenEndpoint = "https://login.live.com/oauth20_token.srf"; + + public const string UserInformationEndpoint = "https://apis.live.net/v5.0/me"; + } +} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs new file mode 100644 index 000000000..31484f998 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.OAuth; +using Microsoft.AspNet.Http.Authentication; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Authentication.MicrosoftAccount +{ + internal class MicrosoftAccountAuthenticationHandler : OAuthAuthenticationHandler + { + public MicrosoftAccountAuthenticationHandler(HttpClient httpClient) + : base(httpClient) + { + } + + protected override async Task GetUserInformationAsync(AuthenticationProperties properties, TokenResponse tokens) + { + var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + var graphResponse = await Backchannel.SendAsync(request, Context.RequestAborted); + graphResponse.EnsureSuccessStatusCode(); + var accountString = await graphResponse.Content.ReadAsStringAsync(); + var accountInformation = JObject.Parse(accountString); + + var context = new MicrosoftAccountAuthenticatedContext(Context, Options, accountInformation, tokens); + context.Properties = properties; + var identity = new ClaimsIdentity( + new[] + { + new Claim(ClaimTypes.NameIdentifier, context.Id, ClaimValueTypes.String, Options.ClaimsIssuer), + new Claim(ClaimTypes.Name, context.Name, ClaimValueTypes.String, Options.ClaimsIssuer), + new Claim("urn:microsoftaccount:id", context.Id, ClaimValueTypes.String, Options.ClaimsIssuer), + new Claim("urn:microsoftaccount:name", context.Name, ClaimValueTypes.String, Options.ClaimsIssuer) + }, + Options.ClaimsIssuer, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType); + + if (!string.IsNullOrWhiteSpace(context.Email)) + { + identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + context.Principal = new ClaimsPrincipal(identity); + + await Options.Notifications.Authenticated(context); + + return new AuthenticationTicket(context.Principal, context.Properties, context.Options.AuthenticationScheme); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs new file mode 100644 index 000000000..362664629 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Authentication.OAuth; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.DataProtection; +using Microsoft.Framework.Internal; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Authentication.MicrosoftAccount +{ + /// + /// An ASP.NET middleware for authenticating users using the Microsoft Account service. + /// + public class MicrosoftAccountAuthenticationMiddleware : OAuthAuthenticationMiddleware + { + /// + /// Initializes a new . + /// + /// The next middleware in the HTTP pipeline to invoke. + /// + /// + /// Configuration options for the middleware. + public MicrosoftAccountAuthenticationMiddleware( + [NotNull] RequestDelegate next, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IUrlEncoder encoder, + [NotNull] IOptions externalOptions, + [NotNull] IOptions options, + ConfigureOptions configureOptions = null) + : base(next, dataProtectionProvider, loggerFactory, encoder, externalOptions, options, configureOptions) + { + if (Options.Notifications == null) + { + Options.Notifications = new MicrosoftAccountAuthenticationNotifications(); + } + if (Options.Scope.Count == 0) + { + // LiveID requires a scope string, so if the user didn't set one we go for the least possible. + // TODO: Should we just add these by default when we create the Options? + Options.Scope.Add("wl.basic"); + } + } + + /// + /// Provides the object for processing authentication-related requests. + /// + /// An configured with the supplied to the constructor. + protected override AuthenticationHandler CreateHandler() + { + return new MicrosoftAccountAuthenticationHandler(Backchannel); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs new file mode 100644 index 000000000..0212b6e7d --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.OAuth; + +namespace Microsoft.AspNet.Authentication.MicrosoftAccount +{ + /// + /// Configuration options for . + /// + public class MicrosoftAccountAuthenticationOptions : OAuthAuthenticationOptions + { + /// + /// Initializes a new . + /// + public MicrosoftAccountAuthenticationOptions() + { + AuthenticationScheme = MicrosoftAccountAuthenticationDefaults.AuthenticationScheme; + Caption = AuthenticationScheme; + CallbackPath = new PathString("/signin-microsoft"); + AuthorizationEndpoint = MicrosoftAccountAuthenticationDefaults.AuthorizationEndpoint; + TokenEndpoint = MicrosoftAccountAuthenticationDefaults.TokenEndpoint; + UserInformationEndpoint = MicrosoftAccountAuthenticationDefaults.UserInformationEndpoint; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountServiceCollectionExtensions.cs new file mode 100644 index 000000000..86b68230d --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountServiceCollectionExtensions.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 Microsoft.AspNet.Authentication.MicrosoftAccount; +using Microsoft.Framework.Configuration; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.DependencyInjection +{ + /// + /// Extension methods for using + /// + public static class MicrosoftAccountServiceCollectionExtensions + { + public static IServiceCollection ConfigureMicrosoftAccountAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure) + { + return services.ConfigureMicrosoftAccountAuthentication(configure, optionsName: ""); + } + + public static IServiceCollection ConfigureMicrosoftAccountAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure, string optionsName) + { + return services.Configure(configure, optionsName); + } + + public static IServiceCollection ConfigureMicrosoftAccountAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config) + { + return services.ConfigureMicrosoftAccountAuthentication(config, optionsName: ""); + } + + public static IServiceCollection ConfigureMicrosoftAccountAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config, string optionsName) + { + return services.Configure(config, optionsName); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.cs new file mode 100644 index 000000000..d9647e4cd --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.OAuth; + +namespace Microsoft.AspNet.Authentication.MicrosoftAccount +{ + /// + /// Specifies callback methods which the invokes to enable developer control over the authentication process. + /// + public interface IMicrosoftAccountAuthenticationNotifications : IOAuthAuthenticationNotifications + { + /// + /// Invoked whenever Microsoft succesfully authenticates a user. + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + Task Authenticated(MicrosoftAccountAuthenticatedContext context); + } +} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs new file mode 100644 index 000000000..c5bfa3f9b --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Authentication.OAuth; +using Microsoft.AspNet.Http; +using Microsoft.Framework.Internal; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Authentication.MicrosoftAccount +{ + /// + /// Contains information about the login session as well as the user . + /// + public class MicrosoftAccountAuthenticatedContext : OAuthAuthenticatedContext + { + /// + /// Initializes a new . + /// + /// The HTTP environment. + /// The JSON-serialized user. + /// The access token provided by the Microsoft authentication service. + public MicrosoftAccountAuthenticatedContext(HttpContext context, OAuthAuthenticationOptions options, [NotNull] JObject user, TokenResponse tokens) + : base(context, options, user, tokens) + { + IDictionary userAsDictionary = user; + + JToken userId = User["id"]; + if (userId == null) + { + throw new ArgumentException(Resources.Exception_MissingId, nameof(user)); + } + + Id = userId.ToString(); + Name = PropertyValueIfExists("name", userAsDictionary); + FirstName = PropertyValueIfExists("first_name", userAsDictionary); + LastName = PropertyValueIfExists("last_name", userAsDictionary); + + if (userAsDictionary.ContainsKey("emails")) + { + JToken emailsNode = user["emails"]; + foreach (var childAsProperty in emailsNode.OfType().Where(childAsProperty => childAsProperty.Name == "preferred")) + { + Email = childAsProperty.Value.ToString(); + } + } + } + + /// + /// Gets the Microsoft Account user ID. + /// + public string Id { get; private set; } + + /// + /// Gets the user's name. + /// + public string Name { get; private set; } + + /// + /// Gets the user's first name. + /// + public string FirstName { get; private set; } + + /// + /// Gets the user's last name. + /// + public string LastName { get; private set; } + + /// + /// Gets the user's email address. + /// + public string Email { get; private set; } + + private static string PropertyValueIfExists(string property, IDictionary dictionary) + { + return dictionary.ContainsKey(property) ? dictionary[property].ToString() : null; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs new file mode 100644 index 000000000..16491a4ed --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.OAuth; + +namespace Microsoft.AspNet.Authentication.MicrosoftAccount +{ + /// + /// Default implementation. + /// + public class MicrosoftAccountAuthenticationNotifications : OAuthAuthenticationNotifications, IMicrosoftAccountAuthenticationNotifications + { + /// + /// Initializes a new + /// + public MicrosoftAccountAuthenticationNotifications() + { + OnAuthenticated = context => Task.FromResult(0); + } + + /// + /// Gets or sets the function that is invoked when the Authenticated method is invoked. + /// + public Func OnAuthenticated { get; set; } + + /// + /// Invoked whenever Microsoft succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task Authenticated(MicrosoftAccountAuthenticatedContext context) + { + return OnAuthenticated(context); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..025a94598 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +[assembly: AssemblyMetadata("Serviceable", "True")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Resources.Designer.cs similarity index 63% rename from src/Microsoft.AspNet.Security/Resources.Designer.cs rename to src/Microsoft.AspNet.Authentication.MicrosoftAccount/Resources.Designer.cs index 26e74bb6c..080d4bc07 100644 --- a/src/Microsoft.AspNet.Security/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Resources.Designer.cs @@ -1,14 +1,14 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.34003 +// Runtime Version:4.0.30319.33440 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ -namespace Microsoft.AspNet.Security { +namespace Microsoft.AspNet.Authentication.MicrosoftAccount { using System; @@ -39,7 +39,7 @@ internal Resources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Security.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Authentication.MicrosoftAccount.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); resourceMan = temp; } return resourceMan; @@ -61,38 +61,29 @@ internal Resources() { } /// - /// Looks up a localized string similar to The AuthenticationTokenProvider's required synchronous events have not been registered.. + /// Looks up a localized string similar to The user does not have an id.. /// - internal static string Exception_AuthenticationTokenDoesNotProvideSyncMethods { + internal static string Exception_MissingId { get { - return ResourceManager.GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods", resourceCulture); + return ResourceManager.GetString("Exception_MissingId", resourceCulture); } } /// - /// Looks up a localized string similar to The default data protection provider may only be used when the IAppBuilder.Properties contains an appropriate 'host.AppName' key.. + /// Looks up a localized string similar to The '{0}' option must be provided.. /// - internal static string Exception_DefaultDpapiRequiresAppNameKey { + internal static string Exception_OptionMustBeProvided { get { - return ResourceManager.GetString("Exception_DefaultDpapiRequiresAppNameKey", resourceCulture); + return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture); } } /// - /// Looks up a localized string similar to A default value for SignInAsAuthenticationType was not found in IAppBuilder Properties. This can happen if your authentication middleware are added in the wrong order, or if one is missing.. + /// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.. /// - internal static string Exception_MissingDefaultSignInAsAuthenticationType { + internal static string Exception_ValidatorHandlerMismatch { get { - return ResourceManager.GetString("Exception_MissingDefaultSignInAsAuthenticationType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The state passed to UnhookAuthentication may only be the return value from HookAuthentication.. - /// - internal static string Exception_UnhookAuthenticationStateType { - get { - return ResourceManager.GetString("Exception_UnhookAuthenticationStateType", resourceCulture); + return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture); } } } diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Resources.resx b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Resources.resx new file mode 100644 index 000000000..26eb43888 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Resources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 user does not have an id. + + + The '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/project.json b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/project.json new file mode 100644 index 000000000..41ef74556 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/project.json @@ -0,0 +1,17 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET 5 middleware that enables an application to support the Microsoft Account authentication workflow.", + "dependencies": { + "Microsoft.AspNet.Authentication.OAuth": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" } + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { + "dependencies": { + "System.Dynamic.Runtime": "4.0.10-beta-*", + "System.ObjectModel": "4.0.10-beta-*" + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Microsoft.AspNet.Authentication.OAuth.xproj b/src/Microsoft.AspNet.Authentication.OAuth/Microsoft.AspNet.Authentication.OAuth.xproj new file mode 100644 index 000000000..962888b9d --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Microsoft.AspNet.Authentication.OAuth.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 1657c79e-7755-4aee-9d61-571295b69a30 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/BaseValidatingContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/BaseValidatingContext.cs new file mode 100644 index 000000000..1dec37860 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/BaseValidatingContext.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.Notifications; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + /// + /// Base class used for certain event contexts + /// + public abstract class BaseValidatingContext : BaseContext + { + /// + /// Initializes base class used for certain event contexts + /// + protected BaseValidatingContext( + HttpContext context, + TOptions options) + : base(context, options) + { + } + + /// + /// True if application code has called any of the Validate methods on this context. + /// + public bool IsValidated { get; private set; } + + /// + /// True if application code has called any of the SetError methods on this context. + /// + public bool HasError { get; private set; } + + /// + /// The error argument provided when SetError was called on this context. This is eventually + /// returned to the client app as the OAuth "error" parameter. + /// + public string Error { get; private set; } + + /// + /// The optional errorDescription argument provided when SetError was called on this context. This is eventually + /// returned to the client app as the OAuth "error_description" parameter. + /// + public string ErrorDescription { get; private set; } + + /// + /// The optional errorUri argument provided when SetError was called on this context. This is eventually + /// returned to the client app as the OAuth "error_uri" parameter. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "error_uri is a string value in the protocol")] + public string ErrorUri { get; private set; } + + /// + /// Marks this context as validated by the application. IsValidated becomes true and HasError becomes false as a result of calling. + /// + /// True if the validation has taken effect. + public virtual bool Validated() + { + IsValidated = true; + HasError = false; + return true; + } + + /// + /// Marks this context as not validated by the application. IsValidated and HasError become false as a result of calling. + /// + public virtual void Rejected() + { + IsValidated = false; + HasError = false; + } + + /// + /// Marks this context as not validated by the application and assigns various error information properties. + /// HasError becomes true and IsValidated becomes false as a result of calling. + /// + /// Assigned to the Error property + public void SetError(string error) + { + SetError(error, null); + } + + /// + /// Marks this context as not validated by the application and assigns various error information properties. + /// HasError becomes true and IsValidated becomes false as a result of calling. + /// + /// Assigned to the Error property + /// Assigned to the ErrorDescription property + public void SetError(string error, + string errorDescription) + { + SetError(error, errorDescription, null); + } + + /// + /// Marks this context as not validated by the application and assigns various error information properties. + /// HasError becomes true and IsValidated becomes false as a result of calling. + /// + /// Assigned to the Error property + /// Assigned to the ErrorDescription property + /// Assigned to the ErrorUri property + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "error_uri is a string value in the protocol")] + public void SetError(string error, + string errorDescription, + string errorUri) + { + Error = error; + ErrorDescription = errorDescription; + ErrorUri = errorUri; + Rejected(); + HasError = true; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/BaseValidatingTicketContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/BaseValidatingTicketContext.cs new file mode 100644 index 000000000..6d088c853 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/BaseValidatingTicketContext.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + /// + /// Base class used for certain event contexts + /// + public abstract class BaseValidatingTicketContext : BaseValidatingContext + { + /// + /// Initializes base class used for certain event contexts + /// + protected BaseValidatingTicketContext( + HttpContext context, + TOptions options, + AuthenticationTicket ticket) + : base(context, options) + { + Ticket = ticket; + } + + /// + /// Contains the identity and properties for the application to authenticate. If the Validated method + /// is invoked with an AuthenticationTicket or ClaimsIdentity argument, that new value is assigned to + /// this property in addition to changing IsValidated to true. + /// + public AuthenticationTicket Ticket { get; private set; } + + /// + /// Replaces the ticket information on this context and marks it as as validated by the application. + /// IsValidated becomes true and HasError becomes false as a result of calling. + /// + /// Assigned to the Ticket property + /// True if the validation has taken effect. + public bool Validated(AuthenticationTicket ticket) + { + Ticket = ticket; + return Validated(); + } + + /// + /// Alters the ticket information on this context and marks it as as validated by the application. + /// IsValidated becomes true and HasError becomes false as a result of calling. + /// + /// Assigned to the Ticket.Identity property + /// True if the validation has taken effect. + public bool Validated(ClaimsPrincipal principal) + { + AuthenticationProperties properties = Ticket != null ? Ticket.Properties : new AuthenticationProperties(); + // TODO: Ticket can be null, need to revisit + return Validated(new AuthenticationTicket(principal, properties, Ticket.AuthenticationScheme)); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/IOAuthAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/IOAuthAuthenticationNotifications.cs new file mode 100644 index 000000000..6d386d089 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/IOAuthAuthenticationNotifications.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.Threading.Tasks; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + /// + /// Specifies callback methods which the invokes to enable developer control over the authentication process. + /// + public interface IOAuthAuthenticationNotifications + { + /// + /// Invoked after the provider successfully authenticates a user. This can be used to retrieve user information. + /// This notification may not be invoked by sub-classes of OAuthAuthenticationHandler if they override GetUserInformationAsync. + /// + /// Contains information about the login session. + /// A representing the completed operation. + Task GetUserInformationAsync(OAuthGetUserInformationContext context); + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// + /// A representing the completed operation. + Task ReturnEndpoint(OAuthReturnEndpointContext context); + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the Microsoft middleware. + /// + /// Contains redirect URI and of the challenge. + void ApplyRedirect(OAuthApplyRedirectContext context); + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthApplyRedirectContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthApplyRedirectContext.cs new file mode 100644 index 000000000..71c61f261 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthApplyRedirectContext.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Authentication.Notifications; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + /// + /// Context passed when a Challenge causes a redirect to authorize endpoint in the Microsoft account middleware. + /// + public class OAuthApplyRedirectContext : BaseContext + { + /// + /// Creates a new context object. + /// + /// The HTTP request context. + /// The authentication properties of the challenge. + /// The initial redirect URI. + public OAuthApplyRedirectContext(HttpContext context, OAuthAuthenticationOptions options, AuthenticationProperties properties, string redirectUri) + : base(context, options) + { + RedirectUri = redirectUri; + Properties = properties; + } + + /// + /// Gets the URI used for the redirect operation. + /// + public string RedirectUri { get; private set; } + + /// + /// Gets the authentication properties of the challenge. + /// + public AuthenticationProperties Properties { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticatedContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticatedContext.cs new file mode 100644 index 000000000..d03411747 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticatedContext.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Security.Claims; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Authentication.Notifications; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + /// + /// Contains information about the login session as well as the user . + /// + public class OAuthAuthenticatedContext : BaseContext + { + /// + /// Initializes a new . + /// + /// The HTTP environment. + /// The JSON-serialized user. + /// The tokens returned from the token endpoint. + public OAuthAuthenticatedContext(HttpContext context, OAuthAuthenticationOptions options, JObject user, + TokenResponse tokens) + : base(context, options) + { + User = user; + AccessToken = tokens.AccessToken; + TokenType = tokens.TokenType; + RefreshToken = tokens.RefreshToken; + + int expiresInValue; + if (Int32.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out expiresInValue)) + { + ExpiresIn = TimeSpan.FromSeconds(expiresInValue); + } + } + + /// + /// Gets the JSON-serialized user. + /// + public JObject User { get; protected set; } + + /// + /// Gets the access token provided by the authentication service. + /// + public string AccessToken { get; protected set; } + + /// + /// Gets the access token type provided by the authentication service. + /// + public string TokenType { get; protected set; } + + /// + /// Gets the refresh token provided by the authentication service. + /// + public string RefreshToken { get; protected set; } + + /// + /// Gets the access token expiration time. + /// + public TimeSpan? ExpiresIn { get; protected set; } + + /// + /// Gets the representing the user. + /// + public ClaimsPrincipal Principal { get; set; } + + /// + /// Gets or sets a property bag for common authentication properties. + /// + public AuthenticationProperties Properties { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticationNotifications.cs new file mode 100644 index 000000000..aa6d7a811 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticationNotifications.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + /// + /// Default implementation. + /// + public class OAuthAuthenticationNotifications : IOAuthAuthenticationNotifications + { + /// + /// Initializes a new + /// + public OAuthAuthenticationNotifications() + { + OnGetUserInformationAsync = OAuthAuthenticationDefaults.DefaultOnGetUserInformationAsync; + OnReturnEndpoint = context => Task.FromResult(0); + OnApplyRedirect = context => context.Response.Redirect(context.RedirectUri); + } + + /// + /// Gets or sets the function that is invoked when the Authenticated method is invoked. + /// + public Func OnGetUserInformationAsync { get; set; } + + /// + /// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked. + /// + public Func OnReturnEndpoint { get; set; } + + /// + /// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked. + /// + public Action OnApplyRedirect { get; set; } + + /// + /// Invoked after the provider successfully authenticates a user. + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task GetUserInformationAsync(OAuthGetUserInformationContext context) + { + return OnGetUserInformationAsync(context); + } + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// Contains information about the login session as well as the user + /// A representing the completed operation. + public virtual Task ReturnEndpoint(OAuthReturnEndpointContext context) + { + return OnReturnEndpoint(context); + } + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the OAuth middleware. + /// + /// Contains redirect URI and of the challenge. + public virtual void ApplyRedirect(OAuthApplyRedirectContext context) + { + OnApplyRedirect(context); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthChallengeContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthChallengeContext.cs new file mode 100644 index 000000000..00560432d --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthChallengeContext.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.Notifications; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + /// + /// Specifies the HTTP response header for the bearer authentication scheme. + /// + public class OAuthChallengeContext : BaseContext + { + /// + /// Initializes a new + /// + /// HTTP environment + /// The www-authenticate header value. + public OAuthChallengeContext( + HttpContext context, + string challenge) + : base(context) + { + Challenge = challenge; + } + + /// + /// The www-authenticate header value. + /// + public string Challenge { get; protected set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthGetUserInformationContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthGetUserInformationContext.cs new file mode 100644 index 000000000..74d02cd58 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthGetUserInformationContext.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Net.Http; +using System.Security.Claims; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Authentication.Notifications; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + /// + /// Contains information about the login session as well as the user . + /// + public class OAuthGetUserInformationContext : BaseContext + { + /// + /// Initializes a new . + /// + /// The HTTP environment. + /// The JSON-serialized user. + /// The tokens returned from the token endpoint. + public OAuthGetUserInformationContext(HttpContext context, OAuthAuthenticationOptions options, HttpClient backchannel, TokenResponse tokens) + : base(context, options) + { + AccessToken = tokens.AccessToken; + TokenType = tokens.TokenType; + RefreshToken = tokens.RefreshToken; + Backchannel = backchannel; + + int expiresInValue; + if (Int32.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out expiresInValue)) + { + ExpiresIn = TimeSpan.FromSeconds(expiresInValue); + } + } + + /// + /// Gets the access token provided by the authentication service. + /// + public string AccessToken { get; protected set; } + + /// + /// Gets the access token type provided by the authentication service. + /// + public string TokenType { get; protected set; } + + /// + /// Gets the refresh token provided by the authentication service. + /// + public string RefreshToken { get; protected set; } + + /// + /// Gets the access token expiration time. + /// + public TimeSpan? ExpiresIn { get; protected set; } + + /// + /// Gets the backchannel used to communicate with the provider. + /// + public HttpClient Backchannel { get; protected set; } + + /// + /// Gets the representing the user. + /// + public ClaimsPrincipal Principal { get; set; } + + /// + /// Gets or sets a property bag for common authentication properties. + /// + public AuthenticationProperties Properties { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthRequestTokenContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthRequestTokenContext.cs new file mode 100644 index 000000000..8af4bf6c6 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthRequestTokenContext.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.Notifications; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + /// + /// Specifies the HTTP request header for the bearer authentication scheme. + /// + public class OAuthRequestTokenContext : BaseContext + { + /// + /// Initializes a new + /// + /// HTTP environment + /// The authorization header value. + public OAuthRequestTokenContext( + HttpContext context, + string token) + : base(context) + { + Token = token; + } + + /// + /// The authorization header value + /// + public string Token { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthReturnEndpointContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthReturnEndpointContext.cs new file mode 100644 index 000000000..222420797 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthReturnEndpointContext.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.Notifications; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + /// + /// Provides context information to middleware providers. + /// + public class OAuthReturnEndpointContext : ReturnEndpointContext + { + /// + /// Initializes a new . + /// + /// The HTTP environment. + /// The authentication ticket. + public OAuthReturnEndpointContext( + HttpContext context, + AuthenticationTicket ticket) + : base(context, ticket) + { + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationDefaults.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationDefaults.cs new file mode 100644 index 000000000..a9b93d43a --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationDefaults.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.Globalization; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + public static class OAuthAuthenticationDefaults + { + public static readonly Func DefaultOnGetUserInformationAsync = context => + { + // If the developer doesn't specify a user-info callback, just give them the tokens. + var identity = new ClaimsIdentity( + context.Options.AuthenticationScheme, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType); + + identity.AddClaim(new Claim("access_token", context.AccessToken, ClaimValueTypes.String, context.Options.AuthenticationScheme)); + if (!string.IsNullOrEmpty(context.RefreshToken)) + { + identity.AddClaim(new Claim("refresh_token", context.RefreshToken, ClaimValueTypes.String, context.Options.AuthenticationScheme)); + } + if (!string.IsNullOrEmpty(context.TokenType)) + { + identity.AddClaim(new Claim("token_type", context.TokenType, ClaimValueTypes.String, context.Options.AuthenticationScheme)); + } + if (context.ExpiresIn.HasValue) + { + identity.AddClaim(new Claim("expires_in", context.ExpiresIn.Value.TotalSeconds.ToString(CultureInfo.InvariantCulture), + ClaimValueTypes.String, context.Options.AuthenticationScheme)); + } + context.Principal = new ClaimsPrincipal(identity); + return Task.FromResult(0); + }; + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationExtensions.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationExtensions.cs new file mode 100644 index 000000000..3c6b3db9a --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationExtensions.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Authentication.OAuth; +using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods for using + /// + public static class OAuthAuthenticationExtensions + { + /// + /// Authenticate users using OAuth. + /// + /// The passed to the configure method. + /// The middleware configuration options. + /// The updated . + public static IApplicationBuilder UseOAuthAuthentication([NotNull] this IApplicationBuilder app, [NotNull] string authenticationScheme, Action> configureOptions = null) + { + return app.UseMiddleware, IOAuthAuthenticationNotifications>>( + new ConfigureOptions>(options => + { + options.AuthenticationScheme = authenticationScheme; + options.Caption = authenticationScheme; + if (configureOptions != null) + { + configureOptions(options); + } + if (options.Notifications == null) + { + options.Notifications = new OAuthAuthenticationNotifications(); + } + }) + { + Name = authenticationScheme, + }); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs new file mode 100644 index 000000000..d9c218344 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs @@ -0,0 +1,250 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Http.Extensions; +using Microsoft.AspNet.WebUtilities; +using Microsoft.Framework.Logging; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + public class OAuthAuthenticationHandler : AuthenticationHandler + where TOptions : OAuthAuthenticationOptions + where TNotifications : IOAuthAuthenticationNotifications + { + public OAuthAuthenticationHandler(HttpClient backchannel) + { + Backchannel = backchannel; + } + + protected HttpClient Backchannel { get; private set; } + + public override async Task InvokeAsync() + { + if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path) + { + return await InvokeReturnPathAsync(); + } + return false; + } + + public async Task InvokeReturnPathAsync() + { + AuthenticationTicket ticket = await AuthenticateAsync(); + if (ticket == null) + { + Logger.LogWarning("Invalid return state, unable to redirect."); + Response.StatusCode = 500; + return true; + } + + var context = new OAuthReturnEndpointContext(Context, ticket) + { + SignInScheme = Options.SignInScheme, + RedirectUri = ticket.Properties.RedirectUri, + }; + ticket.Properties.RedirectUri = null; + + await Options.Notifications.ReturnEndpoint(context); + + if (context.SignInScheme != null && context.Principal != null) + { + Context.Authentication.SignIn(context.SignInScheme, context.Principal, context.Properties); + } + + if (!context.IsRequestCompleted && context.RedirectUri != null) + { + if (context.Principal == null) + { + // add a redirect hint that sign-in failed in some way + context.RedirectUri = QueryHelpers.AddQueryString(context.RedirectUri, "error", "access_denied"); + } + Response.Redirect(context.RedirectUri); + context.RequestCompleted(); + } + + return context.IsRequestCompleted; + } + + protected override AuthenticationTicket AuthenticateCore() + { + return AuthenticateCoreAsync().GetAwaiter().GetResult(); + } + + protected override async Task AuthenticateCoreAsync() + { + AuthenticationProperties properties = null; + try + { + IReadableStringCollection query = Request.Query; + + // TODO: Is this a standard error returned by servers? + var value = query.Get("error"); + if (!string.IsNullOrEmpty(value)) + { + Logger.LogVerbose("Remote server returned an error: " + Request.QueryString); + // TODO: Fail request rather than passing through? + return null; + } + + string code = query.Get("code"); + string state = query.Get("state"); + + properties = Options.StateDataFormat.Unprotect(state); + if (properties == null) + { + return null; + } + + // OAuth2 10.12 CSRF + if (!ValidateCorrelationId(properties)) + { + return new AuthenticationTicket(properties, Options.AuthenticationScheme); + } + + if (string.IsNullOrEmpty(code)) + { + // Null if the remote server returns an error. + return new AuthenticationTicket(properties, Options.AuthenticationScheme); + } + + string requestPrefix = Request.Scheme + "://" + Request.Host; + string redirectUri = requestPrefix + RequestPathBase + Options.CallbackPath; + + var tokens = await ExchangeCodeAsync(code, redirectUri); + + if (string.IsNullOrWhiteSpace(tokens.AccessToken)) + { + Logger.LogWarning("Access token was not found"); + return new AuthenticationTicket(properties, Options.AuthenticationScheme); + } + + return await GetUserInformationAsync(properties, tokens); + } + catch (Exception ex) + { + Logger.LogError("Authentication failed", ex); + return new AuthenticationTicket(properties, Options.AuthenticationScheme); + } + } + + protected virtual async Task ExchangeCodeAsync(string code, string redirectUri) + { + var tokenRequestParameters = new Dictionary() + { + { "client_id", Options.ClientId }, + { "redirect_uri", redirectUri }, + { "client_secret", Options.ClientSecret }, + { "code", code }, + { "grant_type", "authorization_code" }, + }; + + var requestContent = new FormUrlEncodedContent(tokenRequestParameters); + + var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint); + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + requestMessage.Content = requestContent; + HttpResponseMessage response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted); + response.EnsureSuccessStatusCode(); + string oauthTokenResponse = await response.Content.ReadAsStringAsync(); + + JObject oauth2Token = JObject.Parse(oauthTokenResponse); + return new TokenResponse(oauth2Token); + } + + protected virtual async Task GetUserInformationAsync(AuthenticationProperties properties, TokenResponse tokens) + { + var context = new OAuthGetUserInformationContext(Context, Options, Backchannel, tokens) + { + Properties = properties, + }; + await Options.Notifications.GetUserInformationAsync(context); + return new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme); + } + + protected override void ApplyResponseChallenge() + { + if (ShouldConvertChallengeToForbidden()) + { + Response.StatusCode = 403; + return; + } + + if (Response.StatusCode != 401) + { + return; + } + + // When Automatic should redirect on 401 even if there wasn't an explicit challenge. + if (ChallengeContext == null && !Options.AutomaticAuthentication) + { + return; + } + + string baseUri = Request.Scheme + "://" + Request.Host + Request.PathBase; + + string currentUri = baseUri + Request.Path + Request.QueryString; + + string redirectUri = baseUri + Options.CallbackPath; + + AuthenticationProperties properties; + if (ChallengeContext == null) + { + properties = new AuthenticationProperties(); + } + else + { + properties = new AuthenticationProperties(ChallengeContext.Properties); + } + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = currentUri; + } + + // OAuth2 10.12 CSRF + GenerateCorrelationId(properties); + + string authorizationEndpoint = BuildChallengeUrl(properties, redirectUri); + + var redirectContext = new OAuthApplyRedirectContext( + Context, Options, + properties, authorizationEndpoint); + Options.Notifications.ApplyRedirect(redirectContext); + } + + protected virtual string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) + { + string scope = FormatScope(); + + string state = Options.StateDataFormat.Protect(properties); + + var queryBuilder = new QueryBuilder() + { + { "client_id", Options.ClientId }, + { "scope", scope }, + { "response_type", "code" }, + { "redirect_uri", redirectUri }, + { "state", state }, + }; + return Options.AuthorizationEndpoint + queryBuilder.ToString(); + } + + protected virtual string FormatScope() + { + // OAuth2 3.3 space separated + return string.Join(" ", Options.Scope); + } + + protected override void ApplyResponseGrant() + { + // N/A - No SignIn or SignOut support. + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationMiddleware.cs new file mode 100644 index 000000000..67041821c --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationMiddleware.cs @@ -0,0 +1,125 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Net.Http; +using Microsoft.AspNet.Authentication.DataHandler; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.DataProtection; +using Microsoft.Framework.Internal; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + /// + /// An ASP.NET middleware for authenticating users using OAuth services. + /// + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware are not disposable.")] + public class OAuthAuthenticationMiddleware : AuthenticationMiddleware + where TOptions : OAuthAuthenticationOptions, new() + where TNotifications : IOAuthAuthenticationNotifications + { + /// + /// Initializes a new . + /// + /// The next middleware in the HTTP pipeline to invoke. + /// + /// + /// Configuration options for the middleware. + public OAuthAuthenticationMiddleware( + [NotNull] RequestDelegate next, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IUrlEncoder encoder, + [NotNull] IOptions externalOptions, + [NotNull] IOptions options, + ConfigureOptions configureOptions = null) + : base(next, options, loggerFactory, encoder, configureOptions) + { + // todo: review error handling + if (string.IsNullOrWhiteSpace(Options.AuthenticationScheme)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.AuthenticationScheme))); + } + + if (string.IsNullOrWhiteSpace(Options.ClientId)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.ClientId))); + } + + if (string.IsNullOrWhiteSpace(Options.ClientSecret)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.ClientSecret))); + } + + if (string.IsNullOrWhiteSpace(Options.AuthorizationEndpoint)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "AuthorizationEndpoint")); + } + + if (string.IsNullOrWhiteSpace(Options.TokenEndpoint)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "TokenEndpoint")); + } + + if (Options.StateDataFormat == null) + { + var dataProtector = dataProtectionProvider.CreateProtector( + GetType().FullName, Options.AuthenticationScheme, "v1"); + Options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + Backchannel = new HttpClient(ResolveHttpMessageHandler(Options)); + Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET OAuth middleware"); + Backchannel.Timeout = Options.BackchannelTimeout; + Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + + if (string.IsNullOrEmpty(Options.SignInScheme)) + { + Options.SignInScheme = externalOptions.Options.SignInScheme; + } + if (string.IsNullOrEmpty(Options.SignInScheme)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "SignInScheme")); + } + } + + protected HttpClient Backchannel { get; private set; } + + /// + /// Provides the object for processing authentication-related requests. + /// + /// An configured with the supplied to the constructor. + protected override AuthenticationHandler CreateHandler() + { + return new OAuthAuthenticationHandler(Backchannel); + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] + private static HttpMessageHandler ResolveHttpMessageHandler(OAuthAuthenticationOptions options) + { + HttpMessageHandler handler = options.BackchannelHttpHandler ?? +#if DNX451 + new WebRequestHandler(); + // If they provided a validator, apply it or fail. + if (options.BackchannelCertificateValidator != null) + { + // Set the cert validate callback + var webRequestHandler = handler as WebRequestHandler; + if (webRequestHandler == null) + { + throw new InvalidOperationException(Resources.Exception_ValidatorHandlerMismatch); + } + webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate; + } +#else + new WinHttpHandler(); +#endif + return handler; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions.cs new file mode 100644 index 000000000..62296c4a9 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions.cs @@ -0,0 +1,113 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + /// + /// Configuration options for . + /// + public class OAuthAuthenticationOptions : AuthenticationOptions + { + /// + /// Initializes a new . + /// + public OAuthAuthenticationOptions() + { + Scope = new List(); + BackchannelTimeout = TimeSpan.FromSeconds(60); + } + + /// + /// Gets or sets the provider-assigned client id. + /// + public string ClientId { get; set; } + + /// + /// Gets or sets the provider-assigned client secret. + /// + public string ClientSecret { get; set; } + + /// + /// Gets or sets the URI where the client will be redirected to authenticate. + /// + public string AuthorizationEndpoint { get; set; } + + /// + /// Gets or sets the URI the middleware will access to exchange the OAuth token. + /// + public string TokenEndpoint { get; set; } + + /// + /// Gets or sets the URI the middleware will access to obtain the user information. + /// This value is not used in the default implementation, it is for use in custom implementations of + /// IOAuthAuthenticationNotifications.GetUserInformationAsync or OAuthAuthenticationHandler.GetUserInformationAsync. + /// + public string UserInformationEndpoint { get; set; } + +#if DNX451 + /// + /// Gets or sets the a pinned certificate validator to use to validate the endpoints used + /// in back channel communications belong to the auth provider. + /// + /// + /// The pinned certificate validator. + /// + /// If this property is null then the default certificate checks are performed, + /// validating the subject name and if the signing chain is a trusted party. + public ICertificateValidator BackchannelCertificateValidator { get; set; } +#endif + /// + /// Get or sets the text that the user can display on a sign in user interface. + /// + public string Caption + { + get { return Description.Caption; } + set { Description.Caption = value; } + } + + /// + /// Gets or sets timeout value in milliseconds for back channel communications with the auth provider. + /// + /// + /// The back channel timeout. + /// + public TimeSpan BackchannelTimeout { get; set; } + + /// + /// The HttpMessageHandler used to communicate with the auth provider. + /// This cannot be set at the same time as BackchannelCertificateValidator unless the value + /// can be downcast to a WebRequestHandler. + /// + public HttpMessageHandler BackchannelHttpHandler { get; set; } + + /// + /// A list of permissions to request. + /// + public IList Scope { get; private set; } + + /// + /// The request path within the application's base path where the user-agent will be returned. + /// The middleware will process this request when it arrives. + /// + public PathString CallbackPath { get; set; } + + /// + /// Gets or sets the authentication scheme corresponding to the middleware + /// responsible of persisting user's identity after a successful authentication. + /// This value typically corresponds to a cookie middleware registered in the Startup class. + /// When omitted, is used as a fallback value. + /// + public string SignInScheme { get; set; } + + /// + /// Gets or sets the type used to secure data handled by the middleware. + /// + public ISecureDataFormat StateDataFormat { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions`1.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions`1.cs new file mode 100644 index 000000000..aa989dfe5 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions`1.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Authentication.OAuth +{ + /// + /// Configuration options for . + /// + public class OAuthAuthenticationOptions : OAuthAuthenticationOptions where TNotifications : IOAuthAuthenticationNotifications + { + /// + /// Gets or sets the used to handle authentication events. + /// + public TNotifications Notifications { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Authentication.OAuth/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..025a94598 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +[assembly: AssemblyMetadata("Serviceable", "True")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.OAuth/Resources.Designer.cs new file mode 100644 index 000000000..635838274 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Resources.Designer.cs @@ -0,0 +1,83 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.33440 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Authentication.OAuth { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Authentication.OAuth.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The '{0}' option must be provided.. + /// + internal static string Exception_OptionMustBeProvided + { + get + { + return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.. + /// + internal static string Exception_ValidatorHandlerMismatch { + get { + return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Resources.resx b/src/Microsoft.AspNet.Authentication.OAuth/Resources.resx new file mode 100644 index 000000000..2a19bea96 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OAuth/TokenResponse.cs b/src/Microsoft.AspNet.Authentication.OAuth/TokenResponse.cs new file mode 100644 index 000000000..5bcb80ad8 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/TokenResponse.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Authentication.OAuth +{ + public class TokenResponse + { + public TokenResponse(JObject response) + { + Response = response; + AccessToken = response.Value("access_token"); + TokenType = response.Value("token_type"); + RefreshToken = response.Value("refresh_token"); + ExpiresIn = response.Value("expires_in"); + } + + public JObject Response { get; set; } + public string AccessToken { get; set; } + public string TokenType { get; set; } + public string RefreshToken { get; set; } + public string ExpiresIn { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OAuth/project.json b/src/Microsoft.AspNet.Authentication.OAuth/project.json new file mode 100644 index 000000000..ba2cc53dc --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuth/project.json @@ -0,0 +1,22 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET 5 middleware that enables an application to support any standard OAuth 2.0 authentication workflow.", + "dependencies": { + "Microsoft.AspNet.Authentication": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" }, + "Newtonsoft.Json": "6.0.6" + }, + "frameworks": { + "dnx451": { + "frameworkAssemblies": { + "System.Net.Http.WebRequest": "", + "System.Net.Http": "" + } + }, + "dnxcore50": { + "dependencies": { + "System.Net.Http.WinHttpHandler": "4.0.0-beta-*" + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/Microsoft.AspNet.Authentication.OAuthBearer.xproj b/src/Microsoft.AspNet.Authentication.OAuthBearer/Microsoft.AspNet.Authentication.OAuthBearer.xproj new file mode 100644 index 000000000..ef8673b48 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/Microsoft.AspNet.Authentication.OAuthBearer.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 2755BFE5-7421-4A31-A644-F817DF5CAA98 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/Notifications/AuthenticationChallengeNotification.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/Notifications/AuthenticationChallengeNotification.cs new file mode 100644 index 000000000..a3bc8af03 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/Notifications/AuthenticationChallengeNotification.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.Notifications; + +namespace Microsoft.AspNet.Authentication.OAuthBearer +{ + public class AuthenticationChallengeNotification : BaseNotification + { + public AuthenticationChallengeNotification(HttpContext context, TOptions options) : base(context, options) + { + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs new file mode 100644 index 000000000..648ff036d --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.Notifications; + +/// +/// Specifies events which the invokes to enable developer control over the authentication process. /> +/// +namespace Microsoft.AspNet.Authentication.OAuthBearer +{ + /// + /// OAuth bearer token middleware provider + /// + public class OAuthBearerAuthenticationNotifications + { + /// + /// Initializes a new instance of the class + /// + public OAuthBearerAuthenticationNotifications() + { + ApplyChallenge = notification => { notification.HttpContext.Response.Headers.AppendValues("WWW-Authenticate", notification.Options.Challenge); return Task.FromResult(0); }; + AuthenticationFailed = notification => Task.FromResult(0); + MessageReceived = notification => Task.FromResult(0); + SecurityTokenReceived = notification => Task.FromResult(0); + SecurityTokenValidated = notification => Task.FromResult(0); + } + + /// + /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. + /// + public Func, Task> AuthenticationFailed { get; set; } + + /// + /// Invoked when a protocol message is first received. + /// + public Func, Task> MessageReceived { get; set; } + + /// + /// Invoked with the security token that has been extracted from the protocol message. + /// + public Func, Task> SecurityTokenReceived { get; set; } + + /// + /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. + /// + public Func, Task> SecurityTokenValidated { get; set; } + + /// + /// Invoked to apply a challenge sent back to the caller. + /// + public Func, Task> ApplyChallenge { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAppBuilderExtensions.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAppBuilderExtensions.cs new file mode 100644 index 000000000..f50b2a401 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAppBuilderExtensions.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 Microsoft.AspNet.Authentication.OAuthBearer; +using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods to add OAuth Bearer authentication capabilities to an HTTP application pipeline + /// + public static class OAuthBearerAppBuilderExtensions + { + /// + /// Adds Bearer token processing to an HTTP application pipeline. This middleware understands appropriately + /// formatted and secured tokens which appear in the request header. If the Options.AuthenticationMode is Active, the + /// claims within the bearer token are added to the current request's IPrincipal User. If the Options.AuthenticationMode + /// is Passive, then the current request is not modified, but IAuthenticationManager AuthenticateAsync may be used at + /// any time to obtain the claims from the request's bearer token. + /// See also http://tools.ietf.org/html/rfc6749 + /// + /// The application builder + /// Options which control the processing of the bearer header. + /// The application builder + public static IApplicationBuilder UseOAuthBearerAuthentication([NotNull] this IApplicationBuilder app, Action configureOptions = null, string optionsName = "") + { + return app.UseMiddleware( + new ConfigureOptions(configureOptions ?? (o => { })) + { + Name = optionsName + }); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationDefaults.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationDefaults.cs new file mode 100644 index 000000000..2dc1797e9 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationDefaults.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Authentication.OAuthBearer +{ + /// + /// Default values used by authorization server and bearer authentication. + /// + public static class OAuthBearerAuthenticationDefaults + { + /// + /// Default value for AuthenticationScheme property in the OAuthBearerAuthenticationOptions and + /// OAuthAuthorizationServerOptions. + /// + public const string AuthenticationScheme = "Bearer"; + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs new file mode 100644 index 000000000..b08851950 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs @@ -0,0 +1,208 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.Notifications; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.Framework.Logging; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.AspNet.Authentication.OAuthBearer +{ + public class OAuthBearerAuthenticationHandler : AuthenticationHandler + { + private OpenIdConnectConfiguration _configuration; + + protected override AuthenticationTicket AuthenticateCore() + { + return AuthenticateCoreAsync().GetAwaiter().GetResult(); + } + + /// + /// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using set in the options. + /// + /// + protected override async Task AuthenticateCoreAsync() + { + string token = null; + try + { + // Give application opportunity to find from a different location, adjust, or reject token + var messageReceivedNotification = + new MessageReceivedNotification(Context, Options) + { + ProtocolMessage = Context, + }; + + // notification can set the token + await Options.Notifications.MessageReceived(messageReceivedNotification); + if (messageReceivedNotification.HandledResponse) + { + return messageReceivedNotification.AuthenticationTicket; + } + + if (messageReceivedNotification.Skipped) + { + return null; + } + + // If application retrieved token from somewhere else, use that. + token = messageReceivedNotification.Token; + + if (string.IsNullOrEmpty(token)) + { + var authorization = Request.Headers.Get("Authorization"); + + // If no authorization header found, nothing to process further + if (string.IsNullOrEmpty(authorization)) + { + return null; + } + + if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + token = authorization.Substring("Bearer ".Length).Trim(); + } + + // If no token found, no further work possible + if (string.IsNullOrEmpty(token)) + { + return null; + } + } + + // notify user token was received + var securityTokenReceivedNotification = + new SecurityTokenReceivedNotification(Context, Options) + { + ProtocolMessage = Context, + SecurityToken = token, + }; + + await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); + if (securityTokenReceivedNotification.HandledResponse) + { + return securityTokenReceivedNotification.AuthenticationTicket; + } + + if (securityTokenReceivedNotification.Skipped) + { + return null; + } + + if (_configuration == null && Options.ConfigurationManager != null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + var validationParameters = Options.TokenValidationParameters.Clone(); + if (_configuration != null) + { + if (validationParameters.ValidIssuer == null && !string.IsNullOrWhiteSpace(_configuration.Issuer)) + { + validationParameters.ValidIssuer = _configuration.Issuer; + } + else + { + var issuers = new[] { _configuration.Issuer }; + validationParameters.ValidIssuers = (validationParameters.ValidIssuers == null ? issuers : validationParameters.ValidIssuers.Concat(issuers)); + } + + validationParameters.IssuerSigningKeys = (validationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : validationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys)); + } + + SecurityToken validatedToken; + foreach (var validator in Options.SecurityTokenValidators) + { + if (validator.CanReadToken(token)) + { + var principal = validator.ValidateToken(token, validationParameters, out validatedToken); + var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), Options.AuthenticationScheme); + var securityTokenValidatedNotification = new SecurityTokenValidatedNotification(Context, Options) + { + ProtocolMessage = Context, + AuthenticationTicket = ticket + }; + + await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification); + if (securityTokenValidatedNotification.HandledResponse) + { + return securityTokenValidatedNotification.AuthenticationTicket; + } + + if (securityTokenValidatedNotification.Skipped) + { + return null; + } + + return ticket; + } + } + + throw new InvalidOperationException("No SecurityTokenValidator available for token: " + token ?? "null"); + } + catch (Exception ex) + { + Logger.LogError("Exception occurred while processing message", ex); + + // 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 && ex.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException))) + { + Options.ConfigurationManager.RequestRefresh(); + } + + var authenticationFailedNotification = + new AuthenticationFailedNotification(Context, Options) + { + ProtocolMessage = Context, + Exception = ex + }; + + await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); + if (authenticationFailedNotification.HandledResponse) + { + return authenticationFailedNotification.AuthenticationTicket; + } + + if (authenticationFailedNotification.Skipped) + { + return null; + } + + throw; + } + } + + protected override void ApplyResponseChallenge() + { + ApplyResponseChallengeAsync().GetAwaiter().GetResult(); + } + + protected override async Task ApplyResponseChallengeAsync() + { + if (ShouldConvertChallengeToForbidden()) + { + Response.StatusCode = 403; + return; + } + + if ((Response.StatusCode != 401) || (ChallengeContext == null && !Options.AutomaticAuthentication)) + { + return; + } + + await Options.Notifications.ApplyChallenge(new AuthenticationChallengeNotification(Context, Options)); + } + + protected override void ApplyResponseGrant() + { + // N/A + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs new file mode 100644 index 000000000..c76071b6a --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IdentityModel.Tokens; +using System.Net.Http; +using Microsoft.AspNet.Authentication; +using Microsoft.AspNet.Builder; +using Microsoft.Framework.Internal; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.WebEncoders; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.AspNet.Authentication.OAuthBearer +{ + /// + /// Bearer authentication middleware component which is added to an HTTP pipeline. This class is not + /// created by application code directly, instead it is added by calling the the IAppBuilder UseOAuthBearerAuthentication + /// extension method. + /// + public class OAuthBearerAuthenticationMiddleware : AuthenticationMiddleware + { + /// + /// Bearer authentication component which is added to an HTTP pipeline. This constructor is not + /// called by application code directly, instead it is added by calling the the IAppBuilder UseOAuthBearerAuthentication + /// extension method. + /// + public OAuthBearerAuthenticationMiddleware( + [NotNull] RequestDelegate next, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IUrlEncoder encoder, + [NotNull] IOptions options, + ConfigureOptions configureOptions) + : base(next, options, loggerFactory, encoder, configureOptions) + { + if (Options.Notifications == null) + { + Options.Notifications = new OAuthBearerAuthenticationNotifications(); + } + + if (Options.SecurityTokenValidators == null) + { + Options.SecurityTokenValidators = new List { new JwtSecurityTokenHandler() }; + } + + if (string.IsNullOrWhiteSpace(Options.TokenValidationParameters.ValidAudience) && !string.IsNullOrWhiteSpace(Options.Audience)) + { + Options.TokenValidationParameters.ValidAudience = Options.Audience; + } + + if (Options.ConfigurationManager == null) + { + if (Options.Configuration != null) + { + Options.ConfigurationManager = new StaticConfigurationManager(Options.Configuration); + } + else if (!(string.IsNullOrWhiteSpace(Options.MetadataAddress) && string.IsNullOrWhiteSpace(Options.Authority))) + { + if (string.IsNullOrWhiteSpace(Options.MetadataAddress) && !string.IsNullOrWhiteSpace(Options.Authority)) + { + Options.MetadataAddress = Options.Authority; + if (!Options.MetadataAddress.EndsWith("/", StringComparison.Ordinal)) + { + Options.MetadataAddress += "/"; + } + + Options.MetadataAddress += ".well-known/openid-configuration"; + } + + var httpClient = new HttpClient(ResolveHttpMessageHandler(Options)); + httpClient.Timeout = Options.BackchannelTimeout; + httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + + Options.ConfigurationManager = new ConfigurationManager(Options.MetadataAddress, httpClient); + } + } + } + + /// + /// Called by the AuthenticationMiddleware base class to create a per-request handler. + /// + /// A new instance of the request handler + protected override AuthenticationHandler CreateHandler() + { + return new OAuthBearerAuthenticationHandler(); + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] + private static HttpMessageHandler ResolveHttpMessageHandler(OAuthBearerAuthenticationOptions options) + { + HttpMessageHandler handler = options.BackchannelHttpHandler ?? +#if DNX451 + new WebRequestHandler(); + // If they provided a validator, apply it or fail. + if (options.BackchannelCertificateValidator != null) + { + // Set the cert validate callback + var webRequestHandler = handler as WebRequestHandler; + if (webRequestHandler == null) + { + throw new InvalidOperationException(Resources.Exception_ValidatorHandlerMismatch); + } + webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate; + } +#else + new WinHttpHandler(); +#endif + return handler; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationOptions.cs new file mode 100644 index 000000000..8f632a7a5 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationOptions.cs @@ -0,0 +1,155 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens; +using System.Net.Http; +using Microsoft.IdentityModel.Protocols; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authentication.OAuthBearer +{ + /// + /// Options class provides information needed to control Bearer Authentication middleware behavior + /// + public class OAuthBearerAuthenticationOptions : AuthenticationOptions + { + private ICollection _securityTokenValidators; + private TokenValidationParameters _tokenValidationParameters; + + /// + /// Creates an instance of bearer authentication options with default values. + /// + public OAuthBearerAuthenticationOptions() : base() + { + AuthenticationScheme = OAuthBearerAuthenticationDefaults.AuthenticationScheme; + BackchannelTimeout = TimeSpan.FromMinutes(1); + Challenge = OAuthBearerAuthenticationDefaults.AuthenticationScheme; + Notifications = new OAuthBearerAuthenticationNotifications(); + RefreshOnIssuerKeyNotFound = true; + SystemClock = new SystemClock(); + TokenValidationParameters = new TokenValidationParameters(); + } + + /// + /// Gets or sets the discovery endpoint for obtaining metadata + /// + public string MetadataAddress { get; set; } + + /// + /// Gets or sets the Authority to use when making OpenIdConnect calls. + /// + public string Authority { get; set; } + + /// + /// Gets or sets the audience for any received JWT token. + /// + /// + /// The expected audience for any received JWT token. + /// + public string Audience { get; set; } + + /// + /// Gets or sets the challenge to put in the "WWW-Authenticate" header. + /// + /// TODO - brentschmaltz, should not be null. + public string Challenge { get; set; } + + /// + /// The object provided by the application to process events raised by the bearer authentication middleware. + /// The application may implement the interface fully, or it may create an instance of OAuthBearerAuthenticationProvider + /// and assign delegates only to the events it wants to process. + /// + public OAuthBearerAuthenticationNotifications Notifications { get; set; } + + /// + /// The HttpMessageHandler used to retrieve metadata. + /// This cannot be set at the same time as BackchannelCertificateValidator unless the value + /// is a WebRequestHandler. + /// + public HttpMessageHandler BackchannelHttpHandler { get; set; } + + /// + /// Gets or sets the timeout when using the backchannel to make an http call. + /// + public TimeSpan BackchannelTimeout { get; set; } + +#if DNX451 + /// + /// Gets or sets the a pinned certificate validator to use to validate the endpoints used + /// when retrieving metadata. + /// + /// + /// The pinned certificate validator. + /// + /// If this property is null then the default certificate checks are performed, + /// validating the subject name and if the signing chain is a trusted party. + public ICertificateValidator BackchannelCertificateValidator { get; set; } +#endif + /// + /// 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 OpenIdConnectConfiguration Configuration { 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; } + + /// + /// Used to know what the current clock time is when calculating or validating token expiration. When not assigned default is based on + /// DateTimeOffset.UtcNow. This is typically needed only for unit testing. + /// + public ISystemClock SystemClock { get; set; } + + /// + /// Gets or sets the for validating tokens. + /// + /// if 'value' is null. + public ICollection SecurityTokenValidators + { + get + { + return _securityTokenValidators; + } + + [param: NotNull] + set + { + _securityTokenValidators = value; + } + } + + /// + /// Gets or sets the TokenValidationParameters + /// + /// Contains the types and definitions required for validating a token. + /// if 'value' is null. + public TokenValidationParameters TokenValidationParameters + { + get + { + return _tokenValidationParameters; + } + + set + { + if (value == null) + { + throw new ArgumentNullException("TokenValidationParameters"); + } + + _tokenValidationParameters = value; + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerServiceCollectionExtensions.cs new file mode 100644 index 000000000..76fb362d3 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerServiceCollectionExtensions.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 Microsoft.AspNet.Authentication.OAuthBearer; +using Microsoft.Framework.Configuration; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.DependencyInjection +{ + /// + /// Extension methods to add OAuth Bearer authentication capabilities to an HTTP application pipeline + /// + public static class OAuthBearerServiceCollectionExtensions + { + public static IServiceCollection ConfigureOAuthBearerAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure) + { + return services.ConfigureOAuthBearerAuthentication(configure, optionsName: ""); + } + + public static IServiceCollection ConfigureOAuthBearerAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure, string optionsName) + { + return services.Configure(configure, optionsName); + } + + public static IServiceCollection ConfigureOAuthBearerAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config) + { + return services.ConfigureOAuthBearerAuthentication(config, optionsName: ""); + } + + public static IServiceCollection ConfigureOAuthBearerAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config, string optionsName) + { + return services.Configure(config, optionsName); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..025a94598 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +[assembly: AssemblyMetadata("Serviceable", "True")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/Resources.Designer.cs new file mode 100644 index 000000000..27298f47d --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/Resources.Designer.cs @@ -0,0 +1,83 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.33440 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Authentication.OAuthBearer { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Authentication.OAuth.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The '{0}' option must be provided.. + /// + internal static string Exception_OptionMustBeProvided + { + get + { + return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.. + /// + internal static string Exception_ValidatorHandlerMismatch { + get { + return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/Resources.resx b/src/Microsoft.AspNet.Authentication.OAuthBearer/Resources.resx new file mode 100644 index 000000000..2a19bea96 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/project.json b/src/Microsoft.AspNet.Authentication.OAuthBearer/project.json new file mode 100644 index 000000000..e81bd3499 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/project.json @@ -0,0 +1,22 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET 5 middleware that enables an application to receive a OAuth bearer token.", + "dependencies": { + "Microsoft.AspNet.Authentication": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" }, + "Microsoft.IdentityModel.Protocol.Extensions": "2.0.0-beta4-*" + }, + "frameworks": { + "dnx451": { + "frameworkAssemblies": { + "System.Net.Http.WebRequest": "", + "System.Net.Http": "" + } + }, + "dnxcore50": { + "dependencies": { + "System.Net.Http.WinHttpHandler": "4.0.0-beta-*" + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/INonceCache.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/INonceCache.cs new file mode 100644 index 000000000..8dc39aa9d --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/INonceCache.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Authentication.OpenIdConnect +{ + public interface INonceCache + { + bool TryAddNonce(string nonce); + + bool TryRemoveNonce(string nonce); + } +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Microsoft.AspNet.Authentication.OpenIdConnect.xproj b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Microsoft.AspNet.Authentication.OpenIdConnect.xproj new file mode 100644 index 000000000..9ae8192f2 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Microsoft.AspNet.Authentication.OpenIdConnect.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 35115d55-b69e-46d4-bb33-c9e9e6ec5e7a + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs new file mode 100644 index 000000000..740a0e63c --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.OpenIdConnect; +using Microsoft.IdentityModel.Protocols; +using System.Diagnostics.CodeAnalysis; +using System.IdentityModel.Tokens; + +namespace Microsoft.AspNet.Authentication.Notifications +{ + /// + /// This Notification can be used to be informed when an 'AuthorizationCode' is received over the OpenIdConnect protocol. + /// + public class AuthorizationCodeReceivedNotification : BaseNotification + { + /// + /// Creates a + /// + public AuthorizationCodeReceivedNotification(HttpContext context, OpenIdConnectAuthenticationOptions options) : base(context, options) + { + } + + /// + /// Gets or sets the 'code'. + /// + public string Code { get; set; } + + /// + /// Gets or sets the that was received in the id_token + code OpenIdConnectRequest. + /// + public JwtSecurityToken JwtSecurityToken { get; set; } + + /// + /// Gets or sets the . + /// + public OpenIdConnectMessage ProtocolMessage { get; set; } + + /// + /// Gets or sets the 'redirect_uri'. + /// + /// This is the redirect_uri that was sent in the id_token + code OpenIdConnectRequest. + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "user controlled, not necessarily a URI")] + public string RedirectUri { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs new file mode 100644 index 000000000..763ef11d3 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Authentication.OpenIdConnect +{ + /// + /// Default values related to OpenIdConnect authentication middleware + /// + public static class OpenIdConnectAuthenticationDefaults + { + /// + /// The default value used for OpenIdConnectAuthenticationOptions.AuthenticationScheme + /// + public const string AuthenticationScheme = "OpenIdConnect"; + + /// + /// The prefix used to provide a default OpenIdConnectAuthenticationOptions.CookieName + /// + public const string CookiePrefix = ".AspNet.OpenIdConnect."; + + /// + /// The default value for OpenIdConnectAuthenticationOptions.Caption. + /// + public const string Caption = "OpenIdConnect"; + + /// + /// The prefix used to for the a nonce in the cookie + /// + public const string CookieNoncePrefix = ".AspNet.OpenIdConnect.Nonce."; + + /// + /// The property for the RedirectUri that was used when asking for a 'authorizationCode' + /// + public const string RedirectUriUsedForCodeKey = "OpenIdConnect.Code.RedirectUri"; + + /// + /// Constant used to identify state in openIdConnect protocal message + /// + public const string AuthenticationPropertiesKey = "OpenIdConnect.AuthenticationProperties"; + } +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationExtensions.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationExtensions.cs new file mode 100644 index 000000000..c4a1961f1 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationExtensions.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Authentication.OpenIdConnect; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods for using + /// + public static class OpenIdConnectAuthenticationExtensions + { + /// + /// Adds the into the ASP.NET runtime. + /// + /// The application builder + /// Options which control the processing of the OpenIdConnect protocol and token validation. + /// The application builder + public static IApplicationBuilder UseOpenIdConnectAuthentication(this IApplicationBuilder app, Action configureOptions = null, string optionsName = "") + { + return app.UseMiddleware( + new ConfigureOptions(configureOptions ?? (o => { })) + { + Name = optionsName + }); + } + + /// + /// Adds the into the ASP.NET runtime. + /// + /// The application builder + /// Options which control the processing of the OpenIdConnect protocol and token validation. + /// The application builder + public static IApplicationBuilder UseOpenIdConnectAuthentication(this IApplicationBuilder app, IOptions options) + { + return app.UseMiddleware(options); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs new file mode 100644 index 000000000..cc132e9bb --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs @@ -0,0 +1,649 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.IdentityModel.Tokens; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.Notifications; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.Framework.Logging; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.AspNet.Authentication.OpenIdConnect +{ + /// + /// A per-request authentication handler for the OpenIdConnectAuthenticationMiddleware. + /// + public class OpenIdConnectAuthenticationHandler : AuthenticationHandler + { + private const string NonceProperty = "N"; + private const string UriSchemeDelimiter = "://"; + private OpenIdConnectConfiguration _configuration; + + private string CurrentUri + { + get + { + return Request.Scheme + + UriSchemeDelimiter + + Request.Host + + Request.PathBase + + Request.Path + + Request.QueryString; + } + } + + protected override void ApplyResponseGrant() + { + ApplyResponseGrantAsync().GetAwaiter().GetResult(); + } + + /// + /// Handles Signout + /// + /// + protected override async Task ApplyResponseGrantAsync() + { + var signout = SignOutContext; + if (signout != null) + { + if (_configuration == null && Options.ConfigurationManager != null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + var openIdConnectMessage = new OpenIdConnectMessage() + { + IssuerAddress = _configuration == null ? string.Empty : (_configuration.EndSessionEndpoint ?? string.Empty), + RequestType = OpenIdConnectRequestType.LogoutRequest, + }; + + // Set End_Session_Endpoint in order: + // 1. properties.Redirect + // 2. Options.PostLogoutRedirectUri + var properties = new AuthenticationProperties(signout.Properties); + if (!string.IsNullOrEmpty(properties.RedirectUri)) + { + openIdConnectMessage.PostLogoutRedirectUri = properties.RedirectUri; + } + else if (!string.IsNullOrWhiteSpace(Options.PostLogoutRedirectUri)) + { + openIdConnectMessage.PostLogoutRedirectUri = Options.PostLogoutRedirectUri; + } + + var notification = new RedirectToIdentityProviderNotification(Context, Options) + { + ProtocolMessage = openIdConnectMessage + }; + + await Options.Notifications.RedirectToIdentityProvider(notification); + + if (!notification.HandledResponse) + { + var redirectUri = notification.ProtocolMessage.CreateLogoutRequestUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + Logger.LogWarning(Resources.OIDCH_0051_RedirectUriLogoutIsNotWellFormed, redirectUri); + } + + Response.Redirect(redirectUri); + } + } + } + + protected override void ApplyResponseChallenge() + { + ApplyResponseChallengeAsync().GetAwaiter().GetResult(); + } + + /// + /// Responds to a 401 Challenge. Sends an OpenIdConnect message to the 'identity authority' to obtain an identity. + /// + /// + /// Uses log id's OIDCH-0026 - OIDCH-0050, next num: 37 + protected override async Task ApplyResponseChallengeAsync() + { + Logger.LogDebug(Resources.OIDCH_0026_ApplyResponseChallengeAsync, this.GetType()); + + if (ShouldConvertChallengeToForbidden()) + { + Logger.LogDebug(Resources.OIDCH_0027_401_ConvertedTo_403); + Response.StatusCode = 403; + return; + } + + if (Response.StatusCode != 401) + { + Logger.LogDebug(Resources.OIDCH_0028_StatusCodeNot401, Response.StatusCode); + return; + } + + // When Automatic should redirect on 401 even if there wasn't an explicit challenge. + if (ChallengeContext == null && !Options.AutomaticAuthentication) + { + Logger.LogDebug(Resources.OIDCH_0029_ChallengeContextEqualsNull); + return; + } + + // order for local RedirectUri + // 1. challenge.Properties.RedirectUri + // 2. CurrentUri if Options.DefaultToCurrentUriOnRedirect is true) + AuthenticationProperties properties; + if (ChallengeContext == null) + { + properties = new AuthenticationProperties(); + } + else + { + properties = new AuthenticationProperties(ChallengeContext.Properties); + } + + if (!string.IsNullOrWhiteSpace(properties.RedirectUri)) + { + Logger.LogDebug(Resources.OIDCH_0030_Using_Properties_RedirectUri, properties.RedirectUri); + } + else if (Options.DefaultToCurrentUriOnRedirect) + { + Logger.LogDebug(Resources.OIDCH_0032_UsingCurrentUriRedirectUri, CurrentUri); + properties.RedirectUri = CurrentUri; + } + + if (!string.IsNullOrWhiteSpace(Options.RedirectUri)) + { + Logger.LogDebug(Resources.OIDCH_0031_Using_Options_RedirectUri, Options.RedirectUri); + } + + // When redeeming a 'code' for an AccessToken, this value is needed + if (!string.IsNullOrWhiteSpace(Options.RedirectUri)) + { + properties.Items.Add(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey, Options.RedirectUri); + } + + if (_configuration == null && Options.ConfigurationManager != null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + var message = new OpenIdConnectMessage + { + ClientId = Options.ClientId, + IssuerAddress = _configuration?.AuthorizationEndpoint ?? string.Empty, + RedirectUri = Options.RedirectUri, + // [brentschmaltz] - this should be a property on RedirectToIdentityProviderNotification not on the OIDCMessage. + RequestType = OpenIdConnectRequestType.AuthenticationRequest, + Resource = Options.Resource, + ResponseMode = Options.ResponseMode, + ResponseType = Options.ResponseType, + Scope = Options.Scope, + State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + UrlEncoder.UrlEncode(Options.StateDataFormat.Protect(properties)) + }; + + if (Options.ProtocolValidator.RequireNonce) + { + message.Nonce = Options.ProtocolValidator.GenerateNonce(); + if (Options.NonceCache != null) + { + if (!Options.NonceCache.TryAddNonce(message.Nonce)) + { + Logger.LogError(Resources.OIDCH_0033_TryAddNonceFailed, message.Nonce); + throw new OpenIdConnectProtocolException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0033_TryAddNonceFailed, message.Nonce)); + } + } + else + { + WriteNonceCookie(message.Nonce); + } + } + + var redirectToIdentityProviderNotification = new RedirectToIdentityProviderNotification(Context, Options) + { + ProtocolMessage = message + }; + + await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification); + if (redirectToIdentityProviderNotification.HandledResponse) + { + Logger.LogInformation(Resources.OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse); + return; + } + else if (redirectToIdentityProviderNotification.Skipped) + { + Logger.LogInformation(Resources.OIDCH_0035_RedirectToIdentityProviderNotificationSkipped); + return; + } + + var redirectUri = redirectToIdentityProviderNotification.ProtocolMessage.CreateAuthenticationRequestUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + Logger.LogWarning(Resources.OIDCH_0036_UriIsNotWellFormed, redirectUri); + } + + Response.Redirect(redirectUri); + } + + protected override AuthenticationTicket AuthenticateCore() + { + return AuthenticateCoreAsync().GetAwaiter().GetResult(); + } + + /// + /// Invoked to process incoming OpenIdConnect messages. + /// + /// An if successful. + /// Uses log id's OIDCH-0000 - OIDCH-0025 + protected override async Task AuthenticateCoreAsync() + { + Logger.LogDebug(Resources.OIDCH_0000_AuthenticateCoreAsync, this.GetType()); + + // Allow login to be constrained to a specific path. Need to make this runtime configurable. + if (Options.CallbackPath.HasValue && Options.CallbackPath != (Request.PathBase + Request.Path)) + { + return null; + } + + OpenIdConnectMessage message = null; + + // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small. + if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) + && !string.IsNullOrWhiteSpace(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(); + Request.Body.Seek(0, SeekOrigin.Begin); + message = new OpenIdConnectMessage(form); + } + + if (message == null) + { + return null; + } + + try + { + if (Logger.IsEnabled(LogLevel.Debug)) + { + Logger.LogDebug(Resources.OIDCH_0001_MessageReceived, message.BuildRedirectUrl()); + } + + var messageReceivedNotification = + new MessageReceivedNotification(Context, Options) + { + ProtocolMessage = message + }; + + await Options.Notifications.MessageReceived(messageReceivedNotification); + if (messageReceivedNotification.HandledResponse) + { + Logger.LogInformation(Resources.OIDCH_0002_MessageReceivedNotificationHandledResponse); + return messageReceivedNotification.AuthenticationTicket; + } + + if (messageReceivedNotification.Skipped) + { + Logger.LogInformation(Resources.OIDCH_0003_MessageReceivedNotificationSkipped); + return null; + } + + // runtime always adds state, if we don't find it OR we failed to 'unprotect' it this is not a message we should process. + if (string.IsNullOrWhiteSpace(message.State)) + { + Logger.LogError(Resources.OIDCH_0004_MessageStateIsNullOrWhiteSpace); + return null; + } + + var properties = GetPropertiesFromState(message.State); + if (properties == null) + { + Logger.LogError(Resources.OIDCH_0005_MessageStateIsInvalid); + return null; + } + + // devs will need to hook AuthenticationFailedNotification to avoid having 'raw' runtime errors displayed to users. + if (!string.IsNullOrWhiteSpace(message.Error)) + { + Logger.LogError(Resources.OIDCH_0006_MessageErrorNotNull, message.Error); + throw new OpenIdConnectProtocolException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0006_MessageErrorNotNull, message.Error)); + } + + AuthenticationTicket ticket = null; + JwtSecurityToken jwt = null; + + if (_configuration == null && Options.ConfigurationManager != null) + { + Logger.LogDebug(Resources.OIDCH_0007_UpdatingConfiguration); + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + // OpenIdConnect protocol allows a Code to be received without the id_token + if (!string.IsNullOrWhiteSpace(message.IdToken)) + { + Logger.LogDebug(Resources.OIDCH_0020_IdTokenReceived, message.IdToken); + var securityTokenReceivedNotification = + new SecurityTokenReceivedNotification(Context, Options) + { + ProtocolMessage = message + }; + + await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); + if (securityTokenReceivedNotification.HandledResponse) + { + Logger.LogInformation(Resources.OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse); + return securityTokenReceivedNotification.AuthenticationTicket; + } + + if (securityTokenReceivedNotification.Skipped) + { + Logger.LogInformation(Resources.OIDCH_0009_SecurityTokenReceivedNotificationSkipped); + return null; + } + + // Copy and augment to avoid cross request race conditions for updated configurations. + var validationParameters = Options.TokenValidationParameters.Clone(); + if (_configuration != null) + { + if (string.IsNullOrWhiteSpace(validationParameters.ValidIssuer)) + { + validationParameters.ValidIssuer = _configuration.Issuer; + } + else if (!string.IsNullOrWhiteSpace(_configuration.Issuer)) + { + validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(new[] { _configuration.Issuer }) ?? new[] { _configuration.Issuer }; + } + + validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys; + } + + SecurityToken validatedToken = null; + ClaimsPrincipal principal = null; + foreach (var validator in Options.SecurityTokenValidators) + { + if (validator.CanReadToken(message.IdToken)) + { + principal = validator.ValidateToken(message.IdToken, validationParameters, out validatedToken); + jwt = validatedToken as JwtSecurityToken; + if (jwt == null) + { + Logger.LogError(Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType()); + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0010_ValidatedSecurityTokenNotJwt, validatedToken?.GetType())); + } + } + } + + if (validatedToken == null) + { + Logger.LogError(Resources.OIDCH_0011_UnableToValidateToken, message.IdToken); + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.OIDCH_0011_UnableToValidateToken, message.IdToken)); + } + + ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); + if (!string.IsNullOrWhiteSpace(message.SessionState)) + { + ticket.Properties.Items[OpenIdConnectSessionProperties.SessionState] = message.SessionState; + } + + if (_configuration != null && !string.IsNullOrWhiteSpace(_configuration.CheckSessionIframe)) + { + ticket.Properties.Items[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe; + } + + // Rename? + if (Options.UseTokenLifetime) + { + var issued = validatedToken.ValidFrom; + if (issued != DateTime.MinValue) + { + ticket.Properties.IssuedUtc = issued; + } + + var expires = validatedToken.ValidTo; + if (expires != DateTime.MinValue) + { + ticket.Properties.ExpiresUtc = expires; + } + } + + var securityTokenValidatedNotification = + new SecurityTokenValidatedNotification(Context, Options) + { + AuthenticationTicket = ticket, + ProtocolMessage = message + }; + + await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification); + if (securityTokenValidatedNotification.HandledResponse) + { + Logger.LogInformation(Resources.OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse); + return securityTokenValidatedNotification.AuthenticationTicket; + } + + if (securityTokenValidatedNotification.Skipped) + { + Logger.LogInformation(Resources.OIDCH_0013_SecurityTokenValidatedNotificationSkipped); + return null; + } + + string nonce = jwt.Payload.Nonce; + if (Options.NonceCache != null) + { + // if the nonce cannot be removed, it was used + if (!Options.NonceCache.TryRemoveNonce(nonce)) + { + nonce = null; + } + } + else + { + nonce = ReadNonceCookie(nonce); + } + + var protocolValidationContext = new OpenIdConnectProtocolValidationContext + { + AuthorizationCode = message.Code, + Nonce = nonce, + }; + + Options.ProtocolValidator.Validate(jwt, protocolValidationContext); + } + + if (message.Code != null) + { + Logger.LogDebug(Resources.OIDCH_0014_CodeReceived, message.Code); + if (ticket == null) + { + ticket = new AuthenticationTicket(properties, Options.AuthenticationScheme); + } + + var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options) + { + AuthenticationTicket = ticket, + Code = message.Code, + JwtSecurityToken = jwt, + ProtocolMessage = message, + RedirectUri = ticket.Properties.Items.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey) ? + ticket.Properties.Items[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey] : string.Empty, + }; + + await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification); + if (authorizationCodeReceivedNotification.HandledResponse) + { + Logger.LogInformation(Resources.OIDCH_0015_CodeReceivedNotificationHandledResponse); + return authorizationCodeReceivedNotification.AuthenticationTicket; + } + + if (authorizationCodeReceivedNotification.Skipped) + { + Logger.LogInformation(Resources.OIDCH_0016_CodeReceivedNotificationSkipped); + return null; + } + } + + return ticket; + } + catch (Exception exception) + { + Logger.LogError(Resources.OIDCH_0017_ExceptionOccurredWhileProcessingMessage, 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 authenticationFailedNotification = + new AuthenticationFailedNotification(Context, Options) + { + ProtocolMessage = message, + Exception = exception + }; + + await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); + if (authenticationFailedNotification.HandledResponse) + { + Logger.LogInformation(Resources.OIDCH_0018_AuthenticationFailedNotificationHandledResponse); + return authenticationFailedNotification.AuthenticationTicket; + } + + if (authenticationFailedNotification.Skipped) + { + Logger.LogInformation(Resources.OIDCH_0019_AuthenticationFailedNotificationSkipped); + return null; + } + + throw; + } + } + + /// + /// Adds the nonce to . + /// + /// the nonce to remember. + /// is called to add a cookie with the name: 'OpenIdConnectAuthenticationDefaults.Nonce + (nonce)'. + /// The value of the cookie is: "N". + private void WriteNonceCookie(string nonce) + { + if (string.IsNullOrWhiteSpace(nonce)) + { + throw new ArgumentNullException("nonce"); + } + + Response.Cookies.Append( + OpenIdConnectAuthenticationDefaults.CookieNoncePrefix + Options.StringDataFormat.Protect(nonce), + NonceProperty, + new CookieOptions + { + HttpOnly = true, + Secure = Request.IsHttps + }); + } + + /// + /// Searches for a matching nonce. + /// + /// the nonce that we are looking for. + /// echos 'nonce' if a cookie is found that matches, null otherwise. + /// Examine that start with the prefix: 'OpenIdConnectAuthenticationDefaults.Nonce'. + /// is used to obtain the actual 'nonce'. If the nonce is found, then is called. + private string ReadNonceCookie(string nonce) + { + if (nonce == null) + { + return null; + } + + foreach (var nonceKey in Request.Cookies.Keys) + { + if (nonceKey.StartsWith(OpenIdConnectAuthenticationDefaults.CookieNoncePrefix)) + { + try + { + var nonceDecodedValue = Options.StringDataFormat.Unprotect(nonceKey.Substring(OpenIdConnectAuthenticationDefaults.CookieNoncePrefix.Length, nonceKey.Length - OpenIdConnectAuthenticationDefaults.CookieNoncePrefix.Length)); + if (nonceDecodedValue == nonce) + { + var cookieOptions = new CookieOptions + { + HttpOnly = true, + Secure = Request.IsHttps + }; + + Response.Cookies.Delete(nonceKey, cookieOptions); + return nonce; + } + } + catch (Exception ex) + { + Logger.LogWarning("Failed to un-protect the nonce cookie.", ex); + } + } + } + + return null; + } + + private AuthenticationProperties GetPropertiesFromState(string state) + { + // assume a well formed query string: OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey=kasjd;fljasldkjflksdj<&c=d> + var startIndex = 0; + if (string.IsNullOrWhiteSpace(state) || (startIndex = state.IndexOf(OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey, StringComparison.Ordinal)) == -1) + { + return null; + } + + var authenticationIndex = startIndex + OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey.Length; + if (authenticationIndex == -1 || authenticationIndex == state.Length || state[authenticationIndex] != '=') + { + return null; + } + + // scan rest of string looking for '&' + authenticationIndex++; + var endIndex = state.Substring(authenticationIndex, state.Length - authenticationIndex).IndexOf("&", StringComparison.Ordinal); + + // -1 => no other parameters are after the AuthenticationPropertiesKey + if (endIndex == -1) + { + return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex).Replace('+', ' '))); + } + else + { + return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex, endIndex).Replace('+', ' '))); + } + } + + /// + /// Calls InvokeReplyPathAsync + /// + /// True if the request was handled, false if the next middleware should be invoked. + public override Task InvokeAsync() + { + return InvokeReplyPathAsync(); + } + + private async Task InvokeReplyPathAsync() + { + var ticket = await AuthenticateAsync(); + if (ticket != null) + { + if (ticket.Principal != null) + { + Request.HttpContext.Authentication.SignIn(Options.SignInScheme, ticket.Principal, ticket.Properties); + } + + // Redirect back to the original secured resource, if any. + if (!string.IsNullOrWhiteSpace(ticket.Properties.RedirectUri)) + { + Response.Redirect(ticket.Properties.RedirectUri); + return true; + } + } + + return false; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs new file mode 100644 index 000000000..9353f385c --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs @@ -0,0 +1,175 @@ +// Copyright (c) .NET Foundation. All 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.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.IdentityModel.Tokens; +using System.Net.Http; +using System.Text; +using Microsoft.AspNet.Authentication.DataHandler; +using Microsoft.AspNet.Authentication.DataHandler.Encoder; +using Microsoft.AspNet.Authentication.DataHandler.Serializer; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.DataProtection; +using Microsoft.AspNet.Http; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.IdentityModel.Protocols; +using Microsoft.Framework.Internal; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Authentication.OpenIdConnect +{ + /// + /// ASP.NET middleware for obtaining identities using OpenIdConnect protocol. + /// + public class OpenIdConnectAuthenticationMiddleware : AuthenticationMiddleware + { + /// + /// Initializes a + /// + /// The next middleware in the ASP.NET pipeline to invoke. + /// provider for creating a data protector. + /// factory for creating a . + /// a instance that will supply + /// if configureOptions is null. + /// a instance that will be passed to an instance of + /// that is retrieved by calling where string == provides runtime configuration. + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] + public OpenIdConnectAuthenticationMiddleware( + [NotNull] RequestDelegate next, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IUrlEncoder encoder, + [NotNull] IOptions externalOptions, + [NotNull] IOptions options, + ConfigureOptions configureOptions = null) + : base(next, options, loggerFactory, encoder, configureOptions) + { + if (string.IsNullOrEmpty(Options.SignInScheme) && !string.IsNullOrEmpty(externalOptions.Options.SignInScheme)) + { + Options.SignInScheme = externalOptions.Options.SignInScheme; + } + + if (Options.StateDataFormat == null) + { + var dataProtector = dataProtectionProvider.CreateProtector( + typeof(OpenIdConnectAuthenticationMiddleware).FullName, + typeof(string).FullName, + Options.AuthenticationScheme, + "v1"); + + Options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + if (Options.StringDataFormat == null) + { + var dataProtector = dataProtectionProvider.CreateProtector( + typeof(OpenIdConnectAuthenticationMiddleware).FullName, + typeof(string).FullName, + Options.AuthenticationScheme, + "v1"); + + Options.StringDataFormat = new SecureDataFormat(new StringSerializer(), dataProtector, TextEncodings.Base64Url); + } + + if (Options.SecurityTokenValidators == null) + { + Options.SecurityTokenValidators = new Collection { new JwtSecurityTokenHandler() }; + } + + // if the user has not set the AuthorizeCallback, set it from the redirect_uri + if (!Options.CallbackPath.HasValue) + { + Uri redirectUri; + if (!string.IsNullOrEmpty(Options.RedirectUri) && Uri.TryCreate(Options.RedirectUri, UriKind.Absolute, out redirectUri)) + { + // Redirect_Uri must be a very specific, case sensitive value, so we can't generate it. Instead we generate AuthorizeCallback from it. + Options.CallbackPath = PathString.FromUriComponent(redirectUri); + } + } + + if (Options.Notifications == null) + { + Options.Notifications = new OpenIdConnectAuthenticationNotifications(); + } + + if (string.IsNullOrWhiteSpace(Options.TokenValidationParameters.ValidAudience) && !string.IsNullOrWhiteSpace(Options.ClientId)) + { + Options.TokenValidationParameters.ValidAudience = Options.ClientId; + } + + if (Options.ConfigurationManager == null) + { + if (Options.Configuration != null) + { + Options.ConfigurationManager = new StaticConfigurationManager(Options.Configuration); + } + else if (!(string.IsNullOrWhiteSpace(Options.MetadataAddress) && string.IsNullOrWhiteSpace(Options.Authority))) + { + if (string.IsNullOrWhiteSpace(Options.MetadataAddress) && !string.IsNullOrWhiteSpace(Options.Authority)) + { + Options.MetadataAddress = Options.Authority; + if (!Options.MetadataAddress.EndsWith("/", StringComparison.Ordinal)) + { + Options.MetadataAddress += "/"; + } + + Options.MetadataAddress += ".well-known/openid-configuration"; + } + + var httpClient = new HttpClient(ResolveHttpMessageHandler(Options)); + httpClient.Timeout = Options.BackchannelTimeout; + httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + Options.ConfigurationManager = new ConfigurationManager(Options.MetadataAddress, httpClient); + } + } + } + + /// + /// Provides the object for processing authentication-related requests. + /// + /// An configured with the supplied to the constructor. + protected override AuthenticationHandler CreateHandler() + { + return new OpenIdConnectAuthenticationHandler(); + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] + private static HttpMessageHandler ResolveHttpMessageHandler(OpenIdConnectAuthenticationOptions options) + { + var handler = options.BackchannelHttpHandler ?? +#if DNX451 + new WebRequestHandler(); + // If they provided a validator, apply it or fail. + if (options.BackchannelCertificateValidator != null) + { + // Set the cert validate callback + var webRequestHandler = handler as WebRequestHandler; + if (webRequestHandler == null) + { + throw new InvalidOperationException(Resources.OIDCH_0102_ExceptionValidatorHandlerMismatch); + } + webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate; + } +#else + new WinHttpHandler(); +#endif + return handler; + } + + private class StringSerializer : IDataSerializer + { + public string Deserialize(byte[] data) + { + return Encoding.UTF8.GetString(data); + } + + public byte[] Serialize(string model) + { + return Encoding.UTF8.GetBytes(model); + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs new file mode 100644 index 000000000..11c8c9ccc --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.Notifications; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.AspNet.Authentication.OpenIdConnect +{ + /// + /// Specifies events which the invokes to enable developer control over the authentication process. + /// + public class OpenIdConnectAuthenticationNotifications + { + /// + /// Creates a new set of notifications. Each notification has a default no-op behavior unless otherwise documented. + /// + public OpenIdConnectAuthenticationNotifications() + { + AuthenticationFailed = notification => Task.FromResult(0); + AuthorizationCodeReceived = notification => Task.FromResult(0); + MessageReceived = notification => Task.FromResult(0); + SecurityTokenReceived = notification => Task.FromResult(0); + SecurityTokenValidated = notification => Task.FromResult(0); + RedirectToIdentityProvider = notification => Task.FromResult(0); + } + + /// + /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. + /// + public Func, Task> AuthenticationFailed { get; set; } + + /// + /// Invoked after security token validation if an authorization code is present in the protocol message. + /// + public Func AuthorizationCodeReceived { get; set; } + + /// + /// Invoked when a protocol message is first received. + /// + public Func, Task> MessageReceived { get; set; } + + /// + /// Invoked to manipulate redirects to the identity provider for SignIn, SignOut, or Challenge. + /// + public Func, Task> RedirectToIdentityProvider { get; set; } + + /// + /// Invoked with the security token that has been extracted from the protocol message. + /// + public Func, Task> SecurityTokenReceived { get; set; } + + /// + /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. + /// + public Func, Task> SecurityTokenValidated { get; set; } + + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs new file mode 100644 index 000000000..9e4bfa55d --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs @@ -0,0 +1,341 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IdentityModel.Tokens; +using System.Net.Http; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.IdentityModel.Protocols; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authentication.OpenIdConnect +{ + /// + /// Configuration options for + /// + public class OpenIdConnectAuthenticationOptions : AuthenticationOptions + { + private TimeSpan _backchannelTimeout; + private OpenIdConnectProtocolValidator _protocolValidator; + private ICollection _securityTokenValidators; + private ISecureDataFormat _stateDataFormat; + private ISecureDataFormat _stringDataFormat; + private TokenValidationParameters _tokenValidationParameters; + + /// + /// Initializes a new + /// + public OpenIdConnectAuthenticationOptions() + : this(OpenIdConnectAuthenticationDefaults.AuthenticationScheme) + { + } + + /// + /// Initializes a new + /// + /// + /// Defaults: + /// AddNonceToRequest: true. + /// BackchannelTimeout: 1 minute. + /// Caption: . + /// ProtocolValidator: new . + /// RefreshOnIssuerKeyNotFound: true + /// ResponseType: + /// Scope: . + /// TokenValidationParameters: new with AuthenticationScheme = authenticationScheme. + /// UseTokenLifetime: true. + /// + /// will be used to when creating the for the AuthenticationScheme property. + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationOptions.set_Caption(System.String)", Justification = "Not a LOC field")] + public OpenIdConnectAuthenticationOptions(string authenticationScheme) + { + AuthenticationScheme = authenticationScheme; + BackchannelTimeout = TimeSpan.FromMinutes(1); + Caption = OpenIdConnectAuthenticationDefaults.Caption; + ProtocolValidator = new OpenIdConnectProtocolValidator(); + RefreshOnIssuerKeyNotFound = true; + ResponseMode = OpenIdConnectResponseModes.FormPost; + ResponseType = OpenIdConnectResponseTypes.CodeIdToken; + Scope = OpenIdConnectScopes.OpenIdProfile; + TokenValidationParameters = new TokenValidationParameters(); + UseTokenLifetime = true; + } + + /// + /// Gets or sets the expected audience for any received JWT token. + /// + /// + /// The expected audience for any received JWT token. + /// + public string Audience { get; set; } + + /// + /// Gets or sets the Authority to use when making OpenIdConnect calls. + /// + public string Authority { get; set; } + +#if DNX451 + /// + /// Gets or sets the a pinned certificate validator to use to validate the endpoints used + /// when retrieving metadata. + /// + /// + /// The pinned certificate validator. + /// + /// If this property is null then the default certificate checks are performed, + /// validating the subject name and if the signing chain is a trusted party. + public ICertificateValidator BackchannelCertificateValidator { get; set; } +#endif + /// + /// The HttpMessageHandler used to retrieve metadata. + /// This cannot be set at the same time as BackchannelCertificateValidator unless the value + /// is a WebRequestHandler. + /// + public HttpMessageHandler BackchannelHttpHandler { get; set; } + + /// + /// Gets or sets the timeout when using the backchannel to make an http call. + /// + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "By design we use the property name in the exception")] + public TimeSpan BackchannelTimeout + { + get + { + return _backchannelTimeout; + } + + set + { + if (value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(BackchannelTimeout), value, Resources.OIDCH_0101_BackChallnelLessThanZero); + } + + _backchannelTimeout = value; + } + } + + /// + /// Get or sets the text that the user can display on a sign in user interface. + /// + public string Caption + { + get { return Description.Caption; } + set { Description.Caption = value; } + } + + /// + /// An optional constrained path on which to process the authentication callback. + /// If not provided and RedirectUri is available, this value will be generated from RedirectUri. + /// + /// If you set this value, then the will only listen for posts at this address. + /// If the IdentityProvider does not post to this address, you may end up in a 401 -> IdentityProvider -> Client -> 401 -> ... + public PathString CallbackPath { get; set; } + + /// + /// Gets or sets the 'client_id'. + /// + public string ClientId { get; set; } + + /// + /// Gets or sets the 'client_secret'. + /// + public string ClientSecret { get; set; } + + /// + /// 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 OpenIdConnectConfiguration Configuration { 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 a value controlling if the 'CurrentUri' should be used as the 'local redirect' post authentication + /// if AuthenticationProperties.RedirectUri is null or empty. + /// + public bool DefaultToCurrentUriOnRedirect { get; set; } + + /// + /// Gets or sets the discovery endpoint for obtaining metadata + /// + public string MetadataAddress { get; set; } + + /// + /// The OpenIdConnect protocol http://openid.net/specs/openid-connect-core-1_0.html + /// recommends adding a nonce to a request as a mitigation against replay attacks when requesting id_tokens. + /// By default the runtime uses cookies with unique names generated from a hash of the nonce. + /// + public INonceCache NonceCache { get; set; } + + /// + /// Gets or sets the to notify when processing OpenIdConnect messages. + /// + public OpenIdConnectAuthenticationNotifications Notifications { get; set; } + + /// + /// Gets or sets the that is used to ensure that the 'id_token' received + /// is valid per: http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + /// + /// if 'value' is null. + public OpenIdConnectProtocolValidator ProtocolValidator + { + get + { + return _protocolValidator; + } + [param: NotNull] + set + { + _protocolValidator = value; + } + } + + /// + /// Gets or sets the 'post_logout_redirect_uri' + /// + /// This is sent to the OP as the redirect for the user-agent. + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "By design")] + [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Logout", Justification = "This is the term used in the spec.")] + public string PostLogoutRedirectUri { get; set; } + + /// + /// Gets or sets the 'redirect_uri'. + /// + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "By Design")] + public string RedirectUri { 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; } + + /// + /// Gets or sets the 'resource'. + /// + public string Resource { get; set; } + + /// + /// Gets or sets the 'response_mode'. + /// + public string ResponseMode { get; private set; } + + /// + /// Gets or sets the 'response_type'. + /// + public string ResponseType { get; set; } + + /// + /// Gets or sets the 'scope'. + /// + public string Scope { get; set; } + + /// + /// Gets or sets the SignInScheme which will be used to set the . + /// + public string SignInScheme { get; set; } + + /// + /// Gets or sets the type used to secure data handled by the middleware. + /// + public ISecureDataFormat StateDataFormat + { + get + { + return _stateDataFormat; + } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + _stateDataFormat = value; + } + } + + /// + /// Gets or sets the type used to secure strings used by the middleware. + /// + public ISecureDataFormat StringDataFormat + { + get + { + return _stringDataFormat; + } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + _stringDataFormat = value; + } + } + + /// + /// Gets or sets the for validating tokens. + /// + /// if 'value' is null. + public ICollection SecurityTokenValidators + { + get + { + return _securityTokenValidators; + } + + set + { + if (value == null) + { + throw new ArgumentNullException("SecurityTokenValidators"); + } + + _securityTokenValidators = value; + } + } + + /// + /// Gets or sets the TokenValidationParameters + /// + /// Contains the types and definitions required for validating a token. + public TokenValidationParameters TokenValidationParameters + { + get + { + return _tokenValidationParameters; + } + + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + _tokenValidationParameters = value; + } + } + + /// + /// 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; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectServiceCollectionExtensions.cs new file mode 100644 index 000000000..394ebe3f3 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectServiceCollectionExtensions.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 Microsoft.AspNet.Authentication.OpenIdConnect; +using Microsoft.Framework.Configuration; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.DependencyInjection +{ + /// + /// Extension methods to configure OpenIdConnect authentication options + /// + public static class OpenIdConnectServiceCollectionExtensions + { + public static IServiceCollection ConfigureOpenIdConnectAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure) + { + return ConfigureOpenIdConnectAuthentication(services, configure, null); + } + + public static IServiceCollection ConfigureOpenIdConnectAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure, string optionsName) + { + return services.Configure(configure, optionsName); + } + + public static IServiceCollection ConfigureOpenIdConnectAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config) + { + return ConfigureOpenIdConnectAuthentication(services, config, null); + } + + public static IServiceCollection ConfigureOpenIdConnectAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config, string optionsName) + { + return services.Configure(config, optionsName); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..025a94598 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +[assembly: AssemblyMetadata("Serviceable", "True")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs new file mode 100644 index 000000000..b95de8318 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.Designer.cs @@ -0,0 +1,351 @@ +// +namespace Microsoft.AspNet.Authentication.OpenIdConnect +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if (object.ReferenceEquals(resourceMan, null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Authentication.OpenIdConnect.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + + /// + /// OIDCH_0101: BackchannelTimeout cannot be less or equal to TimeSpan.Zero. + /// + internal static string OIDCH_0101_BackChallnelLessThanZero + { + get { return ResourceManager.GetString("OIDCH_0101_BackChallnelLessThanZero"); } + } + + /// + /// OIDCH0102: An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + /// + internal static string OIDCH_0102_ExceptionValidatorHandlerMismatch + { + get { return ResourceManager.GetString("OIDCH_0102_Exception_ValidatorHandlerMismatch"); } + } + + /// + /// OIDCH_0051: The query string for Logout is not a well formed URI. The runtime cannot redirect. Redirect uri: '{0}'. + /// + internal static string OIDCH_0051_RedirectUriLogoutIsNotWellFormed + { + get { return ResourceManager.GetString("OIDCH_0051_RedirectUriLogoutIsNotWellFormed"); } + } + + /// + /// OIDCH_0026: Entering: '{0}' + /// + internal static string OIDCH_0026_ApplyResponseChallengeAsync + { + get { return ResourceManager.GetString("OIDCH_0026_ApplyResponseChallengeAsync"); } + } + + /// + /// OIDCH_0027: converted 401 to 403. + /// + internal static string OIDCH_0027_401_ConvertedTo_403 + { + get { return ResourceManager.GetString("OIDCH_0027_401_ConvertedTo_403"); } + } + + /// + /// OIDCH_0028: Response.StatusCode != 401, StatusCode: '{0}'." + /// + internal static string OIDCH_0028_StatusCodeNot401 + { + get { return ResourceManager.GetString("OIDCH_0028_StatusCodeNot401"); } + } + + /// + /// OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthentication + /// + internal static string OIDCH_0029_ChallengeContextEqualsNull + { + get { return ResourceManager.GetString("OIDCH_0029_ChallengeContextEqualsNull"); } + } + + /// + /// OIDCH_0030: using properties.RedirectUri for 'local redirect' post authentication: '{0}'. + /// + internal static string OIDCH_0030_Using_Properties_RedirectUri + { + get { return ResourceManager.GetString("OIDCH_0030_Using_Properties_RedirectUri"); } + } + + /// + /// OIDCH_0031: using Options.RedirectUri for 'redirect_uri': '{0}'. + /// + internal static string OIDCH_0031_Using_Options_RedirectUri + { + get { return ResourceManager.GetString("OIDCH_0031_Using_Options_RedirectUri"); } + } + + /// + /// OIDCH_0032: using the CurrentUri for 'local redirect' post authentication: '{0}'. + /// + internal static string OIDCH_0032_UsingCurrentUriRedirectUri + { + get { return ResourceManager.GetString("OIDCH_0032_UsingCurrentUriRedirectUri"); } + } + + /// + /// OIDCH_0033: ProtocolValidator.RequireNonce == true. Options.NonceCache.TryAddNonce returned false. This usually indicates the nonce is not unique or has been used. The nonce is: '{0}'. + /// + internal static string OIDCH_0033_TryAddNonceFailed + { + get { return ResourceManager.GetString("OIDCH_0033_TryAddNonceFailed"); } + } + + /// + /// OIDCH_0034: redirectToIdentityProviderNotification.HandledResponse + /// + internal static string OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse + { + get { return ResourceManager.GetString("OIDCH_0034_RedirectToIdentityProviderNotificationHandledResponse"); } + } + + /// + /// OIDCH_0035: redirectToIdentityProviderNotification.Skipped + /// + internal static string OIDCH_0035_RedirectToIdentityProviderNotificationSkipped + { + get { return ResourceManager.GetString("OIDCH_0035_RedirectToIdentityProviderNotificationSkipped"); } + } + + /// + /// OIDCH_0036: Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute) returned 'false', redirectUri is: {0}", (redirectUri ?? "null")) + /// + internal static string OIDCH_0036_UriIsNotWellFormed + { + get { return ResourceManager.GetString("OIDCH_0036_UriIsNotWellFormed"); } + } + + /// + /// OIDCH_0000: Entering: '{0}'. + /// + internal static string OIDCH_0000_AuthenticateCoreAsync + { + get { return ResourceManager.GetString("OIDCH_0000_AuthenticateCoreAsync"); } + } + + /// + /// OIDCH_0001: MessageReceived: '{0}'. + /// + internal static string OIDCH_0001_MessageReceived + { + get { return ResourceManager.GetString("OIDCH_0001_MessageReceived"); } + } + + /// + /// OIDCH_0001: MessageReceived: '{0}'. + /// + internal static string FormatOIDCH_0001_MessageReceived(object p0) + { + return string.Format(CultureInfo.CurrentCulture, ResourceManager.GetString("OIDCH_0001_MessageReceived"), p0); + } + + /// + /// OIDCH_0002: messageReceivedNotification.HandledResponse + /// + internal static string OIDCH_0002_MessageReceivedNotificationHandledResponse + { + get { return ResourceManager.GetString("OIDCH_0002_MessageReceivedNotificationHandledResponse"); } + } + + /// + /// OIDCH_0003: messageReceivedNotification.Skipped + /// + internal static string OIDCH_0003_MessageReceivedNotificationSkipped + { + get { return ResourceManager.GetString("OIDCH_0003_MessageReceivedNotificationSkipped"); } + } + + /// + /// OIDCH_0004: OpenIdConnectAuthenticationHandler: message.State is null or whitespace. State is required to process the message. + /// + internal static string OIDCH_0004_MessageStateIsNullOrWhiteSpace + { + get { return ResourceManager.GetString("OIDCH_0004_MessageStateIsNullOrWhiteSpace"); } + } + + /// + /// OIDCH_0005: unable to unprotect the message.State + /// + internal static string OIDCH_0005_MessageStateIsInvalid + { + get { return ResourceManager.GetString("OIDCH_0005_MessageStateIsInvalid"); } + } + + /// + /// OIDCH_0006_MessageErrorNotNull: '{0}'. + /// + internal static string OIDCH_0006_MessageErrorNotNull + { + get { return ResourceManager.GetString("OIDCH_0006_MessageErrorNotNull"); } + } + + /// + /// OIDCH_0007: updating configuration + /// + internal static string OIDCH_0007_UpdatingConfiguration + { + get { return ResourceManager.GetString("OIDCH_0007_UpdatingConfiguration"); } + } + + /// + /// OIDCH_0008: securityTokenReceivedNotification.HandledResponse + /// + internal static string OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse + { + get { return ResourceManager.GetString("OIDCH_0008_SecurityTokenReceivedNotificationHandledResponse"); } + } + + /// + /// OIDCH_0009: securityTokenReceivedNotification.Skipped + /// + internal static string OIDCH_0009_SecurityTokenReceivedNotificationSkipped + { + get { return ResourceManager.GetString("OIDCH_0009_SecurityTokenReceivedNotificationSkipped:"); } + } + + /// + /// OIDCH_0010: Validated Security Token must be a JwtSecurityToken was: '{0}'. + /// + internal static string OIDCH_0010_ValidatedSecurityTokenNotJwt + { + get { return ResourceManager.GetString("OIDCH_0010_ValidatedSecurityTokenNotJwt"); } + } + + /// + /// OIDCH_0011: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: {0}." + /// + internal static string OIDCH_0011_UnableToValidateToken + { + get { return ResourceManager.GetString("OIDCH_0011_UnableToValidateToken"); } + } + + /// + /// OIDCH_0012: securityTokenValidatedNotification.HandledResponse + /// + internal static string OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse + { + get { return ResourceManager.GetString("OIDCH_0012_SecurityTokenValidatedNotificationHandledResponse"); } + } + + /// + /// OIDCH_0013: securityTokenValidatedNotification.Skipped + /// + internal static string OIDCH_0013_SecurityTokenValidatedNotificationSkipped + { + get { return ResourceManager.GetString("OIDCH_0013_SecurityTokenValidatedNotificationSkipped"); } + } + + /// + /// OIDCH_0014: 'code' received: '{0}' + /// + internal static string OIDCH_0014_CodeReceived + { + get { return ResourceManager.GetString("OIDCH_0014_CodeReceived"); } + } + + /// + /// OIDCH_0015: codeReceivedNotification.HandledResponse") + /// + internal static string OIDCH_0015_CodeReceivedNotificationHandledResponse + { + get { return ResourceManager.GetString("OIDCH_0015_CodeReceivedNotificationHandledResponse"); } + } + + /// + /// OIDCH_0016: codeReceivedNotification.Skipped + /// + internal static string OIDCH_0016_CodeReceivedNotificationSkipped + { + get { return ResourceManager.GetString("OIDCH_0016_CodeReceivedNotificationSkipped"); } + } + + /// + /// OIDCH_0017: Exception occurred while processing message + /// + internal static string OIDCH_0017_ExceptionOccurredWhileProcessingMessage + { + get { return ResourceManager.GetString("OIDCH_0017_ExceptionOccurredWhileProcessingMessage"); } + } + + /// + /// OIDCH_0018: authenticationFailedNotification.HandledResponse + /// + internal static string OIDCH_0018_AuthenticationFailedNotificationHandledResponse + { + get { return ResourceManager.GetString("OIDCH_0018_AuthenticationFailedNotificationHandledResponse"); } + } + + /// + /// OIDCH_0019: authenticationFailedNotification.Skipped + /// + internal static string OIDCH_0019_AuthenticationFailedNotificationSkipped + { + get { return ResourceManager.GetString("OIDCH_0019_AuthenticationFailedNotificationSkipped"); } + } + + /// + /// OIDCH_0020: 'id_token' received: '{0}' + /// + internal static string OIDCH_0020_IdTokenReceived + { + get { return ResourceManager.GetString("OIDCH_0020_IdTokenReceived"); } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx new file mode 100644 index 000000000..454dae209 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/Resources.resx @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + OIDCH_0101: BackchannelTimeout cannot be less or equal to TimeSpan.Zero. + + + OIDCH_0102: An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + + OIDC_0051: The query string for Logout is not a well formed URI. The runtime cannot redirect. Redirect uri: '{0}'. + + + OIDCH_0026: Entering: '{0}' + + + OIDCH_0027: converted 401 to 403. + + + OIDCH_0028: Response.StatusCode != 401, StatusCode: '{0}'. + + + OIDCH_0029: ChallengeContext == null AND !Options.AutomaticAuthentication + + + OIDCH_0030: using properties.RedirectUri for 'local redirect' post authentication: '{0}'. + + + OIDCH_0031: using Options.RedirectUri for 'redirect_uri': '{0}'. + + + OIDCH_0032: using the CurrentUri for 'local redirect' post authentication: '{0}'. + + + OIDCH_0033: ProtocolValidator.RequireNonce == true. Options.NonceCache.TryAddNonce returned false. This usually indicates the nonce is not unique or has been used. The nonce is: '{0}'. + + + OIDCH_0034: redirectToIdentityProviderNotification.HandledResponse + + + OIDCH_0035: redirectToIdentityProviderNotification.Skipped + + + OIDCH_0036: Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute) returned 'false', redirectUri is: {0}", (redirectUri ?? "null")) + + + OIDCH_0000: Entering: '{0}'. + + + OIDCH_0001: MessageReceived: '{0}'. + + + OIDCH_0002: messageReceivedNotification.HandledResponse + + + OIDCH_0003: messageReceivedNotification.Skipped + + + OIDCH_0004: OpenIdConnectAuthenticationHandler: message.State is null or whitespace. State is required to process the message. + + + OIDCH_0005: unable to unprotect the message.State + + + OIDCH_0006_MessageErrorNotNull: '{0}'. + + + OIDCH_0007: updating configuration + + + OIDCH_0008: securityTokenReceivedNotification.HandledResponse + + + OIDCH_0009: securityTokenReceivedNotification.Skipped + + + OIDCH_0010: Validated Security Token must be a JwtSecurityToken was: '{0}'. + + + OIDCH_0011: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: {0}." + + + OIDCH_0012: securityTokenValidatedNotification.HandledResponse + + + OIDCH_0013: securityTokenValidatedNotification.Skipped + + + OIDCH_0014: 'code' received: '{0}' + + + OIDCH_0015: codeReceivedNotification.HandledResponse + + + OIDCH_0016: codeReceivedNotification.Skipped + + + OIDCH_0017: Exception occurred while processing message + + + OIDCH_0018: authenticationFailedNotification.HandledResponse + + + OIDCH_0019: authenticationFailedNotification.Skipped + + + OIDCH_0020: 'id_token' received: '{0}' + + diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/project.json b/src/Microsoft.AspNet.Authentication.OpenIdConnect/project.json new file mode 100644 index 000000000..b61834b53 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/project.json @@ -0,0 +1,23 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET 5 middleware that enables an application to support OpenIdConnect authentication workflow.", + "dependencies": { + "Microsoft.AspNet.Authentication": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" }, + "Microsoft.IdentityModel.Protocol.Extensions": "2.0.0-beta4-*" + }, + "frameworks": { + "dnx451": { + "frameworkAssemblies": { + "System.Net.Http": "", + "System.Net.Http.WebRequest": "" + } + }, + "dnxcore50": { + "dependencies": { + "System.Collections.Specialized": "4.0.0-beta-*", + "System.Net.Http.WinHttpHandler": "4.0.0-beta-*" + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Messages/AccessToken.cs b/src/Microsoft.AspNet.Authentication.Twitter/Messages/AccessToken.cs new file mode 100644 index 000000000..f723da742 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Messages/AccessToken.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Authentication.Twitter.Messages +{ + /// + /// The Twitter access token retrieved from the access token endpoint. + /// + public class AccessToken : RequestToken + { + /// + /// Gets or sets the Twitter User ID. + /// + public string UserId { get; set; } + + /// + /// Gets or sets the Twitter screen name. + /// + public string ScreenName { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Messages/RequestToken.cs b/src/Microsoft.AspNet.Authentication.Twitter/Messages/RequestToken.cs new file mode 100644 index 000000000..963ad1d7d --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Messages/RequestToken.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http.Authentication; + +namespace Microsoft.AspNet.Authentication.Twitter.Messages +{ + /// + /// The Twitter request token obtained from the request token endpoint. + /// + public class RequestToken + { + /// + /// Gets or sets the Twitter request token. + /// + public string Token { get; set; } + + /// + /// Gets or sets the Twitter token secret. + /// + public string TokenSecret { get; set; } + + public bool CallbackConfirmed { get; set; } + + /// + /// Gets or sets a property bag for common authentication properties. + /// + public AuthenticationProperties Properties { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Messages/RequestTokenSerializer.cs b/src/Microsoft.AspNet.Authentication.Twitter/Messages/RequestTokenSerializer.cs new file mode 100644 index 000000000..21ef478a8 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Messages/RequestTokenSerializer.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Microsoft.AspNet.Authentication.DataHandler.Serializer; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authentication.Twitter.Messages +{ + /// + /// Serializes and deserializes Twitter request and access tokens so that they can be used by other application components. + /// + public class RequestTokenSerializer : IDataSerializer + { + private const int FormatVersion = 1; + + /// + /// Serialize a request token. + /// + /// The token to serialize + /// A byte array containing the serialized token + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Dispose is idempotent")] + public virtual byte[] Serialize(RequestToken model) + { + using (var memory = new MemoryStream()) + { + using (var writer = new BinaryWriter(memory)) + { + Write(writer, model); + writer.Flush(); + return memory.ToArray(); + } + } + } + + /// + /// Deserializes a request token. + /// + /// A byte array containing the serialized token + /// The Twitter request token + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Dispose is idempotent")] + public virtual RequestToken Deserialize(byte[] data) + { + using (var memory = new MemoryStream(data)) + { + using (var reader = new BinaryReader(memory)) + { + return Read(reader); + } + } + } + + /// + /// Writes a Twitter request token as a series of bytes. Used by the method. + /// + /// The writer to use in writing the token + /// The token to write + public static void Write([NotNull] BinaryWriter writer, [NotNull] RequestToken token) + { + writer.Write(FormatVersion); + writer.Write(token.Token); + writer.Write(token.TokenSecret); + writer.Write(token.CallbackConfirmed); + PropertiesSerializer.Write(writer, token.Properties); + } + + /// + /// Reads a Twitter request token from a series of bytes. Used by the method. + /// + /// The reader to use in reading the token bytes + /// The token + public static RequestToken Read([NotNull] BinaryReader reader) + { + if (reader.ReadInt32() != FormatVersion) + { + return null; + } + + string token = reader.ReadString(); + string tokenSecret = reader.ReadString(); + bool callbackConfirmed = reader.ReadBoolean(); + AuthenticationProperties properties = PropertiesSerializer.Read(reader); + if (properties == null) + { + return null; + } + + return new RequestToken { Token = token, TokenSecret = tokenSecret, CallbackConfirmed = callbackConfirmed, Properties = properties }; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Messages/Serializers.cs b/src/Microsoft.AspNet.Authentication.Twitter/Messages/Serializers.cs new file mode 100644 index 000000000..b40ac22c1 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Messages/Serializers.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Authentication.DataHandler.Serializer; + +namespace Microsoft.AspNet.Authentication.Twitter.Messages +{ + /// + /// Provides access to a request token serializer. + /// + public static class Serializers + { + static Serializers() + { + RequestToken = new RequestTokenSerializer(); + } + + /// + /// Gets or sets a statically-avaliable serializer object. The value for this property will be by default. + /// + public static IDataSerializer RequestToken { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Microsoft.AspNet.Authentication.Twitter.xproj b/src/Microsoft.AspNet.Authentication.Twitter/Microsoft.AspNet.Authentication.Twitter.xproj new file mode 100644 index 000000000..55deb0714 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Microsoft.AspNet.Authentication.Twitter.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 0330fff6-b4b5-42dd-8c99-26a789569000 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Notifications/ITwitterAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.Twitter/Notifications/ITwitterAuthenticationNotifications.cs new file mode 100644 index 000000000..19fe1b1cc --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Notifications/ITwitterAuthenticationNotifications.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Authentication.Twitter +{ + /// + /// Specifies callback methods which the invokes to enable developer control over the authentication process. /> + /// + public interface ITwitterAuthenticationNotifications + { + /// + /// Invoked whenever Twitter succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + Task Authenticated(TwitterAuthenticatedContext context); + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// + /// A representing the completed operation. + Task ReturnEndpoint(TwitterReturnEndpointContext context); + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the Twitter middleware + /// + /// Contains redirect URI and of the challenge + void ApplyRedirect(TwitterApplyRedirectContext context); + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Notifications/TwitterApplyRedirectContext.cs b/src/Microsoft.AspNet.Authentication.Twitter/Notifications/TwitterApplyRedirectContext.cs new file mode 100644 index 000000000..2186644ed --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Notifications/TwitterApplyRedirectContext.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Authentication.Notifications; + +namespace Microsoft.AspNet.Authentication.Twitter +{ + /// + /// The Context passed when a Challenge causes a redirect to authorize endpoint in the Twitter middleware. + /// + public class TwitterApplyRedirectContext : BaseContext + { + /// + /// Creates a new context object. + /// + /// The HTTP request context. + /// The Twitter middleware options. + /// The authentication properties of the challenge. + /// The initial redirect URI. + public TwitterApplyRedirectContext(HttpContext context, TwitterAuthenticationOptions options, + AuthenticationProperties properties, string redirectUri) + : base(context, options) + { + RedirectUri = redirectUri; + Properties = properties; + } + + /// + /// Gets the URI used for the redirect operation. + /// + public string RedirectUri { get; private set; } + + /// + /// Gets the authentication properties of the challenge. + /// + public AuthenticationProperties Properties { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Notifications/TwitterAuthenticatedContext.cs b/src/Microsoft.AspNet.Authentication.Twitter/Notifications/TwitterAuthenticatedContext.cs new file mode 100644 index 000000000..9cf3b085c --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Notifications/TwitterAuthenticatedContext.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Authentication.Notifications; + +namespace Microsoft.AspNet.Authentication.Twitter +{ + /// + /// Contains information about the login session as well as the user . + /// + public class TwitterAuthenticatedContext : BaseContext + { + /// + /// Initializes a + /// + /// The HTTP environment + /// Twitter user ID + /// Twitter screen name + /// Twitter access token + /// Twitter access token secret + public TwitterAuthenticatedContext( + HttpContext context, + string userId, + string screenName, + string accessToken, + string accessTokenSecret) + : base(context) + { + UserId = userId; + ScreenName = screenName; + AccessToken = accessToken; + AccessTokenSecret = accessTokenSecret; + } + + /// + /// Gets the Twitter user ID + /// + public string UserId { get; private set; } + + /// + /// Gets the Twitter screen name + /// + public string ScreenName { get; private set; } + + /// + /// Gets the Twitter access token + /// + public string AccessToken { get; private set; } + + /// + /// Gets the Twitter access token secret + /// + public string AccessTokenSecret { get; private set; } + + /// + /// Gets the representing the user + /// + public ClaimsPrincipal Principal { get; set; } + + /// + /// Gets or sets a property bag for common authentication properties + /// + public AuthenticationProperties Properties { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Notifications/TwitterAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.Twitter/Notifications/TwitterAuthenticationNotifications.cs new file mode 100644 index 000000000..5b7f7948d --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Notifications/TwitterAuthenticationNotifications.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Authentication.Twitter +{ + /// + /// Default implementation. + /// + public class TwitterAuthenticationNotifications : ITwitterAuthenticationNotifications + { + /// + /// Initializes a + /// + public TwitterAuthenticationNotifications() + { + OnAuthenticated = context => Task.FromResult(null); + OnReturnEndpoint = context => Task.FromResult(null); + OnApplyRedirect = context => context.Response.Redirect(context.RedirectUri); + } + + /// + /// Gets or sets the function that is invoked when the Authenticated method is invoked. + /// + public Func OnAuthenticated { get; set; } + + /// + /// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked. + /// + public Func OnReturnEndpoint { get; set; } + + /// + /// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked. + /// + public Action OnApplyRedirect { get; set; } + + /// + /// Invoked whenever Twitter succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task Authenticated(TwitterAuthenticatedContext context) + { + return OnAuthenticated(context); + } + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// + /// A representing the completed operation. + public virtual Task ReturnEndpoint(TwitterReturnEndpointContext context) + { + return OnReturnEndpoint(context); + } + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the Twitter middleware + /// + /// Contains redirect URI and of the challenge + public virtual void ApplyRedirect(TwitterApplyRedirectContext context) + { + OnApplyRedirect(context); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Notifications/TwitterReturnEndpointContext.cs b/src/Microsoft.AspNet.Authentication.Twitter/Notifications/TwitterReturnEndpointContext.cs new file mode 100644 index 000000000..6d71bc95f --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Notifications/TwitterReturnEndpointContext.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.Notifications; + +namespace Microsoft.AspNet.Authentication.Twitter +{ + /// + /// Provides context information to middleware providers. + /// + public class TwitterReturnEndpointContext : ReturnEndpointContext + { + /// + /// Initializes a new . + /// + /// HTTP environment + /// The authentication ticket + public TwitterReturnEndpointContext( + HttpContext context, + AuthenticationTicket ticket) + : base(context, ticket) + { + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Authentication.Twitter/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..025a94598 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +[assembly: AssemblyMetadata("Serviceable", "True")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication.Twitter/Resources.Designer.cs new file mode 100644 index 000000000..5e6c599ae --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Resources.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.32559 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Authentication.Twitter { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Authentication.Twitter.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The '{0}' option must be provided.. + /// + internal static string Exception_OptionMustBeProvided { + get { + return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.. + /// + internal static string Exception_ValidatorHandlerMismatch { + get { + return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Resources.resx b/src/Microsoft.AspNet.Authentication.Twitter/Resources.resx new file mode 100644 index 000000000..2a19bea96 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAppBuilderExtensions.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAppBuilderExtensions.cs new file mode 100644 index 000000000..04e36058f --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAppBuilderExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Authentication.Twitter; +using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods for using + /// + public static class TwitterAppBuilderExtensions + { + public static IApplicationBuilder UseTwitterAuthentication([NotNull] this IApplicationBuilder app, Action configureOptions = null, string optionsName = "") + { + return app.UseMiddleware( + new ConfigureOptions(configureOptions ?? (o => { })) + { + Name = optionsName + }); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationDefaults.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationDefaults.cs new file mode 100644 index 000000000..9b7d8c0fa --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationDefaults.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Authentication.Twitter +{ + public static class TwitterAuthenticationDefaults + { + public const string AuthenticationScheme = "Twitter"; + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs new file mode 100644 index 000000000..eecc1a043 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs @@ -0,0 +1,389 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.Twitter.Messages; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.WebUtilities; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Authentication.Twitter +{ + internal class TwitterAuthenticationHandler : AuthenticationHandler + { + private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private const string StateCookie = "__TwitterState"; + private const string RequestTokenEndpoint = "https://api.twitter.com/oauth/request_token"; + private const string AuthenticationEndpoint = "https://twitter.com/oauth/authenticate?oauth_token="; + private const string AccessTokenEndpoint = "https://api.twitter.com/oauth/access_token"; + + private readonly HttpClient _httpClient; + + public TwitterAuthenticationHandler(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public override async Task InvokeAsync() + { + if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path) + { + return await InvokeReturnPathAsync(); + } + return false; + } + + protected override AuthenticationTicket AuthenticateCore() + { + return AuthenticateCoreAsync().GetAwaiter().GetResult(); + } + + protected override async Task AuthenticateCoreAsync() + { + AuthenticationProperties properties = null; + try + { + var query = Request.Query; + var protectedRequestToken = Request.Cookies[StateCookie]; + + var requestToken = Options.StateDataFormat.Unprotect(protectedRequestToken); + + if (requestToken == null) + { + Logger.LogWarning("Invalid state"); + return null; + } + + properties = requestToken.Properties; + + var returnedToken = query.Get("oauth_token"); + if (string.IsNullOrWhiteSpace(returnedToken)) + { + Logger.LogWarning("Missing oauth_token"); + return new AuthenticationTicket(properties, Options.AuthenticationScheme); + } + + if (returnedToken != requestToken.Token) + { + Logger.LogWarning("Unmatched token"); + return new AuthenticationTicket(properties, Options.AuthenticationScheme); + } + + var oauthVerifier = query.Get("oauth_verifier"); + if (string.IsNullOrWhiteSpace(oauthVerifier)) + { + Logger.LogWarning("Missing or blank oauth_verifier"); + return new AuthenticationTicket(properties, Options.AuthenticationScheme); + } + + var accessToken = await ObtainAccessTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, requestToken, oauthVerifier); + + var context = new TwitterAuthenticatedContext(Context, accessToken.UserId, accessToken.ScreenName, accessToken.Token, accessToken.TokenSecret); + + context.Principal = new ClaimsPrincipal( + new ClaimsIdentity( + new[] + { + new Claim(ClaimTypes.NameIdentifier, accessToken.UserId, "http://www.w3.org/2001/XMLSchema#string", Options.ClaimsIssuer), + new Claim(ClaimTypes.Name, accessToken.ScreenName, "http://www.w3.org/2001/XMLSchema#string", Options.ClaimsIssuer), + new Claim("urn:twitter:userid", accessToken.UserId, "http://www.w3.org/2001/XMLSchema#string", Options.ClaimsIssuer), + new Claim("urn:twitter:screenname", accessToken.ScreenName, "http://www.w3.org/2001/XMLSchema#string", Options.ClaimsIssuer) + }, + Options.ClaimsIssuer, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType)); + context.Properties = requestToken.Properties; + + var cookieOptions = new CookieOptions + { + HttpOnly = true, + Secure = Request.IsHttps + }; + + Response.Cookies.Delete(StateCookie, cookieOptions); + + await Options.Notifications.Authenticated(context); + + return new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme); + } + catch (Exception ex) + { + Logger.LogError("Authentication failed", ex); + return new AuthenticationTicket(properties, Options.AuthenticationScheme); + } + } + protected override void ApplyResponseChallenge() + { + ApplyResponseChallengeAsync().GetAwaiter().GetResult(); + } + + protected override async Task ApplyResponseChallengeAsync() + { + if (ShouldConvertChallengeToForbidden()) + { + Response.StatusCode = 403; + return; + } + + if (Response.StatusCode != 401) + { + return; + } + + // When Automatic should redirect on 401 even if there wasn't an explicit challenge. + if (ChallengeContext == null && !Options.AutomaticAuthentication) + { + return; + } + + var requestPrefix = Request.Scheme + "://" + Request.Host; + var callBackUrl = requestPrefix + RequestPathBase + Options.CallbackPath; + + AuthenticationProperties properties; + if (ChallengeContext == null) + { + properties = new AuthenticationProperties(); + } + else + { + properties = new AuthenticationProperties(ChallengeContext.Properties); + } + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = requestPrefix + Request.PathBase + Request.Path + Request.QueryString; + } + + var requestToken = await ObtainRequestTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, callBackUrl, properties); + + if (requestToken.CallbackConfirmed) + { + var twitterAuthenticationEndpoint = AuthenticationEndpoint + requestToken.Token; + + var cookieOptions = new CookieOptions + { + HttpOnly = true, + Secure = Request.IsHttps + }; + + Response.Cookies.Append(StateCookie, Options.StateDataFormat.Protect(requestToken), cookieOptions); + + var redirectContext = new TwitterApplyRedirectContext( + Context, Options, + properties, twitterAuthenticationEndpoint); + Options.Notifications.ApplyRedirect(redirectContext); + } + else + { + Logger.LogError("requestToken CallbackConfirmed!=true"); + } + } + + public async Task InvokeReturnPathAsync() + { + var model = await AuthenticateAsync(); + if (model == null) + { + Logger.LogWarning("Invalid return state, unable to redirect."); + Response.StatusCode = 500; + return true; + } + + var context = new TwitterReturnEndpointContext(Context, model) + { + SignInScheme = Options.SignInScheme, + RedirectUri = model.Properties.RedirectUri + }; + model.Properties.RedirectUri = null; + + await Options.Notifications.ReturnEndpoint(context); + + if (context.SignInScheme != null && context.Principal != null) + { + Context.Authentication.SignIn(context.SignInScheme, context.Principal, context.Properties); + } + + if (!context.IsRequestCompleted && context.RedirectUri != null) + { + if (context.Principal == null) + { + // add a redirect hint that sign-in failed in some way + context.RedirectUri = QueryHelpers.AddQueryString(context.RedirectUri, "error", "access_denied"); + } + Response.Redirect(context.RedirectUri); + context.RequestCompleted(); + } + + return context.IsRequestCompleted; + } + + private async Task ObtainRequestTokenAsync(string consumerKey, string consumerSecret, string callBackUri, AuthenticationProperties properties) + { + Logger.LogVerbose("ObtainRequestToken"); + + var nonce = Guid.NewGuid().ToString("N"); + + var authorizationParts = new SortedDictionary + { + { "oauth_callback", callBackUri }, + { "oauth_consumer_key", consumerKey }, + { "oauth_nonce", nonce }, + { "oauth_signature_method", "HMAC-SHA1" }, + { "oauth_timestamp", GenerateTimeStamp() }, + { "oauth_version", "1.0" } + }; + + var parameterBuilder = new StringBuilder(); + foreach (var authorizationKey in authorizationParts) + { + parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.UrlEncode(authorizationKey.Key), UrlEncoder.UrlEncode(authorizationKey.Value)); + } + parameterBuilder.Length--; + var parameterString = parameterBuilder.ToString(); + + var canonicalizedRequestBuilder = new StringBuilder(); + canonicalizedRequestBuilder.Append(HttpMethod.Post.Method); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(UrlEncoder.UrlEncode(RequestTokenEndpoint)); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(UrlEncoder.UrlEncode(parameterString)); + + var signature = ComputeSignature(consumerSecret, null, canonicalizedRequestBuilder.ToString()); + authorizationParts.Add("oauth_signature", signature); + + var authorizationHeaderBuilder = new StringBuilder(); + authorizationHeaderBuilder.Append("OAuth "); + foreach (var authorizationPart in authorizationParts) + { + authorizationHeaderBuilder.AppendFormat( + "{0}=\"{1}\", ", authorizationPart.Key, UrlEncoder.UrlEncode(authorizationPart.Value)); + } + authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2; + + var request = new HttpRequestMessage(HttpMethod.Post, RequestTokenEndpoint); + request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString()); + + var response = await _httpClient.SendAsync(request, Context.RequestAborted); + response.EnsureSuccessStatusCode(); + string responseText = await response.Content.ReadAsStringAsync(); + + var responseParameters = new FormCollection(FormReader.ReadForm(responseText)); + if (string.Equals(responseParameters["oauth_callback_confirmed"], "true", StringComparison.Ordinal)) + { + return new RequestToken { Token = Uri.UnescapeDataString(responseParameters["oauth_token"]), TokenSecret = Uri.UnescapeDataString(responseParameters["oauth_token_secret"]), CallbackConfirmed = true, Properties = properties }; + } + + return new RequestToken(); + } + + private async Task ObtainAccessTokenAsync(string consumerKey, string consumerSecret, RequestToken token, string verifier) + { + // https://dev.twitter.com/docs/api/1/post/oauth/access_token + + Logger.LogVerbose("ObtainAccessToken"); + + var nonce = Guid.NewGuid().ToString("N"); + + var authorizationParts = new SortedDictionary + { + { "oauth_consumer_key", consumerKey }, + { "oauth_nonce", nonce }, + { "oauth_signature_method", "HMAC-SHA1" }, + { "oauth_token", token.Token }, + { "oauth_timestamp", GenerateTimeStamp() }, + { "oauth_verifier", verifier }, + { "oauth_version", "1.0" }, + }; + + var parameterBuilder = new StringBuilder(); + foreach (var authorizationKey in authorizationParts) + { + parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.UrlEncode(authorizationKey.Key), UrlEncoder.UrlEncode(authorizationKey.Value)); + } + parameterBuilder.Length--; + var parameterString = parameterBuilder.ToString(); + + var canonicalizedRequestBuilder = new StringBuilder(); + canonicalizedRequestBuilder.Append(HttpMethod.Post.Method); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(UrlEncoder.UrlEncode(AccessTokenEndpoint)); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(UrlEncoder.UrlEncode(parameterString)); + + var signature = ComputeSignature(consumerSecret, token.TokenSecret, canonicalizedRequestBuilder.ToString()); + authorizationParts.Add("oauth_signature", signature); + authorizationParts.Remove("oauth_verifier"); + + var authorizationHeaderBuilder = new StringBuilder(); + authorizationHeaderBuilder.Append("OAuth "); + foreach (var authorizationPart in authorizationParts) + { + authorizationHeaderBuilder.AppendFormat( + "{0}=\"{1}\", ", authorizationPart.Key, UrlEncoder.UrlEncode(authorizationPart.Value)); + } + authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2; + + var request = new HttpRequestMessage(HttpMethod.Post, AccessTokenEndpoint); + request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString()); + + var formPairs = new Dictionary() + { + { "oauth_verifier", verifier }, + }; + + request.Content = new FormUrlEncodedContent(formPairs); + + var response = await _httpClient.SendAsync(request, Context.RequestAborted); + + if (!response.IsSuccessStatusCode) + { + Logger.LogError("AccessToken request failed with a status code of " + response.StatusCode); + response.EnsureSuccessStatusCode(); // throw + } + + var responseText = await response.Content.ReadAsStringAsync(); + var responseParameters = new FormCollection(FormReader.ReadForm(responseText)); + + return new AccessToken + { + Token = Uri.UnescapeDataString(responseParameters["oauth_token"]), + TokenSecret = Uri.UnescapeDataString(responseParameters["oauth_token_secret"]), + UserId = Uri.UnescapeDataString(responseParameters["user_id"]), + ScreenName = Uri.UnescapeDataString(responseParameters["screen_name"]) + }; + } + + private static string GenerateTimeStamp() + { + var secondsSinceUnixEpocStart = DateTime.UtcNow - Epoch; + return Convert.ToInt64(secondsSinceUnixEpocStart.TotalSeconds).ToString(CultureInfo.InvariantCulture); + } + + private string ComputeSignature(string consumerSecret, string tokenSecret, string signatureData) + { + using (var algorithm = new HMACSHA1()) + { + algorithm.Key = Encoding.ASCII.GetBytes( + string.Format(CultureInfo.InvariantCulture, + "{0}&{1}", + UrlEncoder.UrlEncode(consumerSecret), + string.IsNullOrEmpty(tokenSecret) ? string.Empty : UrlEncoder.UrlEncode(tokenSecret))); + var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(signatureData)); + return Convert.ToBase64String(hash); + } + } + + protected override void ApplyResponseGrant() + { + // N/A - No SignIn or SignOut support. + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationMiddleware.cs new file mode 100644 index 000000000..1536bd807 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationMiddleware.cs @@ -0,0 +1,118 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Net.Http; +using Microsoft.AspNet.Authentication.DataHandler; +using Microsoft.AspNet.Authentication.DataHandler.Encoder; +using Microsoft.AspNet.Authentication.Twitter.Messages; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.DataProtection; +using Microsoft.Framework.Internal; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Authentication.Twitter +{ + /// + /// ASP.NET middleware for authenticating users using Twitter + /// + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware are not disposable.")] + public class TwitterAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly HttpClient _httpClient; + + /// + /// Initializes a + /// + /// The next middleware in the HTTP pipeline to invoke + /// + /// + /// + /// Configuration options for the middleware + public TwitterAuthenticationMiddleware( + [NotNull] RequestDelegate next, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IUrlEncoder encoder, + [NotNull] IOptions externalOptions, + [NotNull] IOptions options, + ConfigureOptions configureOptions = null) + : base(next, options, loggerFactory, encoder, configureOptions) + { + if (string.IsNullOrWhiteSpace(Options.ConsumerSecret)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.ConsumerSecret))); + } + if (string.IsNullOrWhiteSpace(Options.ConsumerKey)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.ConsumerKey))); + } + + if (Options.Notifications == null) + { + Options.Notifications = new TwitterAuthenticationNotifications(); + } + if (Options.StateDataFormat == null) + { + IDataProtector dataProtector = dataProtectionProvider.CreateProtector( + typeof(TwitterAuthenticationMiddleware).FullName, Options.AuthenticationScheme, "v1"); + Options.StateDataFormat = new SecureDataFormat( + Serializers.RequestToken, + dataProtector, + TextEncodings.Base64Url); + } + + if (string.IsNullOrEmpty(Options.SignInScheme)) + { + Options.SignInScheme = externalOptions.Options.SignInScheme; + } + if (string.IsNullOrEmpty(Options.SignInScheme)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "SignInScheme")); + } + + _httpClient = new HttpClient(ResolveHttpMessageHandler(Options)); + _httpClient.Timeout = Options.BackchannelTimeout; + _httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + _httpClient.DefaultRequestHeaders.Accept.ParseAdd("*/*"); + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Twitter middleware"); + _httpClient.DefaultRequestHeaders.ExpectContinue = false; + } + + /// + /// Provides the object for processing authentication-related requests. + /// + /// An configured with the supplied to the constructor. + protected override AuthenticationHandler CreateHandler() + { + return new TwitterAuthenticationHandler(_httpClient); + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] + private static HttpMessageHandler ResolveHttpMessageHandler(TwitterAuthenticationOptions options) + { + HttpMessageHandler handler = options.BackchannelHttpHandler ?? +#if DNX451 + new WebRequestHandler(); + // If they provided a validator, apply it or fail. + if (options.BackchannelCertificateValidator != null) + { + // Set the cert validate callback + var webRequestHandler = handler as WebRequestHandler; + if (webRequestHandler == null) + { + throw new InvalidOperationException(Resources.Exception_ValidatorHandlerMismatch); + } + webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate; + } +#else + new WinHttpHandler(); +#endif + return handler; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationOptions.cs new file mode 100644 index 000000000..4768f59b5 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationOptions.cs @@ -0,0 +1,109 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.Twitter.Messages; + +namespace Microsoft.AspNet.Authentication.Twitter +{ + /// + /// Options for the Twitter authentication middleware. + /// + public class TwitterAuthenticationOptions : AuthenticationOptions + { + /// + /// Initializes a new instance of the class. + /// + public TwitterAuthenticationOptions() + { + AuthenticationScheme = TwitterAuthenticationDefaults.AuthenticationScheme; + Caption = AuthenticationScheme; + CallbackPath = new PathString("/signin-twitter"); + BackchannelTimeout = TimeSpan.FromSeconds(60); +#if DNX451 + // Twitter lists its valid Subject Key Identifiers at https://dev.twitter.com/docs/security/using-ssl + BackchannelCertificateValidator = new CertificateSubjectKeyIdentifierValidator( + new[] + { + "A5EF0B11CEC04103A34A659048B21CE0572D7D47", // VeriSign Class 3 Secure Server CA - G2 + "0D445C165344C1827E1D20AB25F40163D8BE79A5", // VeriSign Class 3 Secure Server CA - G3 + "5F60CF619055DF8443148A602AB2F57AF44318EF", // Symantec Class 3 Secure Server CA - G4 + }); +#endif + } + + /// + /// Gets or sets the consumer key used to communicate with Twitter. + /// + /// The consumer key used to communicate with Twitter. + public string ConsumerKey { get; set; } + + /// + /// Gets or sets the consumer secret used to sign requests to Twitter. + /// + /// The consumer secret used to sign requests to Twitter. + public string ConsumerSecret { get; set; } + + /// + /// Gets or sets timeout value in milliseconds for back channel communications with Twitter. + /// + /// + /// The back channel timeout. + /// + public TimeSpan BackchannelTimeout { get; set; } +#if DNX451 + /// + /// Gets or sets the a pinned certificate validator to use to validate the endpoints used + /// in back channel communications belong to Twitter. + /// + /// + /// The pinned certificate validator. + /// + /// If this property is null then the default certificate checks are performed, + /// validating the subject name and if the signing chain is a trusted party. + public ICertificateValidator BackchannelCertificateValidator { get; set; } +#endif + /// + /// The HttpMessageHandler used to communicate with Twitter. + /// This cannot be set at the same time as BackchannelCertificateValidator unless the value + /// can be downcast to a WebRequestHandler. + /// + public HttpMessageHandler BackchannelHttpHandler { get; set; } + + /// + /// Get or sets the text that the user can display on a sign in user interface. + /// + public string Caption + { + get { return Description.Caption; } + set { Description.Caption = value; } + } + + /// + /// The request path within the application's base path where the user-agent will be returned. + /// The middleware will process this request when it arrives. + /// Default value is "/signin-twitter". + /// + public PathString CallbackPath { get; set; } + + /// + /// Gets or sets the authentication scheme corresponding to the middleware + /// responsible of persisting user's identity after a successful authentication. + /// This value typically corresponds to a cookie middleware registered in the Startup class. + /// When omitted, is used as a fallback value. + /// + public string SignInScheme { get; set; } + + /// + /// Gets or sets the type used to secure data handled by the middleware. + /// + public ISecureDataFormat StateDataFormat { get; set; } + + /// + /// Gets or sets the used to handle authentication events. + /// + public ITwitterAuthenticationNotifications Notifications { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterServiceCollectionExtensions.cs new file mode 100644 index 000000000..553bd1ba8 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterServiceCollectionExtensions.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 Microsoft.AspNet.Authentication.Twitter; +using Microsoft.Framework.Configuration; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.DependencyInjection +{ + /// + /// Extension methods for using + /// + public static class TwitterAuthenticationExtensions + { + public static IServiceCollection ConfigureTwitterAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure) + { + return services.ConfigureTwitterAuthentication(configure, optionsName: ""); + } + + public static IServiceCollection ConfigureTwitterAuthentication([NotNull] this IServiceCollection services, [NotNull] Action configure, string optionsName) + { + return services.Configure(configure, optionsName); + } + + public static IServiceCollection ConfigureTwitterAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config) + { + return services.ConfigureTwitterAuthentication(config, optionsName: ""); + } + + public static IServiceCollection ConfigureTwitterAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config, string optionsName) + { + return services.Configure(config, optionsName); + } + } +} diff --git a/src/Microsoft.AspNet.Authentication.Twitter/project.json b/src/Microsoft.AspNet.Authentication.Twitter/project.json new file mode 100644 index 000000000..6b2ddf521 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Twitter/project.json @@ -0,0 +1,22 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET 5 middleware that enables an application to support Twitter's OAuth 2.0 authentication workflow.", + "dependencies": { + "Microsoft.AspNet.Authentication": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" } + }, + "frameworks": { + "dnx451": { + "frameworkAssemblies": { + "System.Net.Http.WebRequest": "", + "System.Net.Http": "" + } + }, + "dnxcore50": { + "dependencies": { + "System.Net.Http.WinHttpHandler": "4.0.0-beta-*", + "System.Security.Cryptography.Hashing.Algorithms": "4.0.0-beta-*" + } + } + } +} diff --git a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs similarity index 55% rename from src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler.cs rename to src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs index 313038e26..1d1d8e193 100644 --- a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs @@ -1,27 +1,26 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Security.Claims; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.DataHandler.Encoder; using Microsoft.AspNet.Http; -using Microsoft.AspNet.Http.Security; -using Microsoft.AspNet.HttpFeature.Security; -using Microsoft.AspNet.Security.DataHandler.Encoder; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Http.Features.Authentication; +using Microsoft.Framework.Internal; using Microsoft.Framework.Logging; +using Microsoft.Framework.WebEncoders; -namespace Microsoft.AspNet.Security.Infrastructure +namespace Microsoft.AspNet.Authentication { /// /// Base class for the per-request work performed by most authentication middleware. /// public abstract class AuthenticationHandler : IAuthenticationHandler { - private static readonly RNGCryptoServiceProvider CryptoRandom = new RNGCryptoServiceProvider(); + private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create(); private Task _authenticate; private bool _authenticateInitialized; @@ -33,9 +32,9 @@ public abstract class AuthenticationHandler : IAuthenticationHandler private AuthenticationOptions _baseOptions; - protected IChallengeContext ChallengeContext { get; set; } - protected SignInIdentityContext SignInIdentityContext { get; set; } - protected ISignOutContext SignOutContext { get; set; } + protected ChallengeContext ChallengeContext { get; set; } + protected SignInContext SignInContext { get; set; } + protected SignOutContext SignOutContext { get; set; } protected HttpContext Context { get; private set; } @@ -51,18 +50,28 @@ protected HttpResponse Response protected PathString RequestPathBase { get; private set; } + protected ILogger Logger { get; private set; } + + protected IUrlEncoder UrlEncoder { get; private set; } + internal AuthenticationOptions BaseOptions { get { return _baseOptions; } } + internal bool AuthenticateCalled { get; set; } + public IAuthenticationHandler PriorHandler { get; set; } - protected async Task BaseInitializeAsync(AuthenticationOptions options, HttpContext context) + public bool Faulted { get; set; } + + protected async Task BaseInitializeAsync([NotNull] AuthenticationOptions options, [NotNull] HttpContext context, [NotNull] ILogger logger, [NotNull] IUrlEncoder encoder) { _baseOptions = options; Context = context; RequestPathBase = Request.PathBase; + Logger = logger; + UrlEncoder = encoder; RegisterAuthenticationHandler(); @@ -70,12 +79,12 @@ protected async Task BaseInitializeAsync(AuthenticationOptions options, HttpCont await InitializeCoreAsync(); - if (BaseOptions.AuthenticationMode == AuthenticationMode.Active) + if (BaseOptions.AutomaticAuthentication) { - AuthenticationTicket ticket = await AuthenticateAsync(); - if (ticket != null && ticket.Identity != null) + var ticket = await AuthenticateAsync(); + if (ticket?.Principal != null) { - SecurityHelper.AddUserIdentity(Context, ticket.Identity); + SecurityHelper.AddUserPrincipal(Context, ticket.Principal); } } } @@ -92,12 +101,29 @@ protected virtual Task InitializeCoreAsync() } /// - /// Called once per request after Initialize and Invoke. + /// Called once per request after Initialize and Invoke. /// /// async completion internal async Task TeardownAsync() { - await ApplyResponseAsync(); + try + { + await ApplyResponseAsync(); + } + catch (Exception) + { + try + { + await TeardownCoreAsync(); + } + catch (Exception) + { + // Don't mask the original exception + } + UnregisterAuthenticationHandler(); + throw; + } + await TeardownCoreAsync(); UnregisterAuthenticationHandler(); } @@ -117,27 +143,32 @@ protected virtual Task TeardownCoreAsync() /// pipeline. public virtual Task InvokeAsync() { - return Task.FromResult(false); + return Task.FromResult(false); } - public virtual void GetDescriptions(IAuthTypeContext authTypeContext) + public virtual void GetDescriptions(DescribeSchemesContext describeContext) { - authTypeContext.Accept(BaseOptions.Description.Dictionary); + describeContext.Accept(BaseOptions.Description.Items); if (PriorHandler != null) { - PriorHandler.GetDescriptions(authTypeContext); + PriorHandler.GetDescriptions(describeContext); } } - public virtual void Authenticate(IAuthenticateContext context) + public virtual void Authenticate(AuthenticateContext context) { - if (context.AuthenticationTypes.Contains(BaseOptions.AuthenticationType, StringComparer.Ordinal)) + if (ShouldHandleScheme(context.AuthenticationScheme)) { - AuthenticationTicket ticket = Authenticate(); - if (ticket != null && ticket.Identity != null) + var ticket = Authenticate(); + if (ticket?.Principal != null) { - context.Authenticated(ticket.Identity, ticket.Properties.Dictionary, BaseOptions.Description.Dictionary); + AuthenticateCalled = true; + context.Authenticated(ticket.Principal, ticket.Properties.Items, BaseOptions.Description.Items); + } + else + { + context.NotAuthenticated(); } } @@ -147,6 +178,28 @@ public virtual void Authenticate(IAuthenticateContext context) } } + public virtual async Task AuthenticateAsync(AuthenticateContext context) + { + if (ShouldHandleScheme(context.AuthenticationScheme)) + { + var ticket = await AuthenticateAsync(); + if (ticket?.Principal != null) + { + AuthenticateCalled = true; + context.Authenticated(ticket.Principal, ticket.Properties.Items, BaseOptions.Description.Items); + } + else + { + context.NotAuthenticated(); + } + } + + if (PriorHandler != null) + { + await PriorHandler.AuthenticateAsync(context); + } + } + public AuthenticationTicket Authenticate() { return LazyInitializer.EnsureInitialized( @@ -156,28 +209,11 @@ public AuthenticationTicket Authenticate() () => { return Task.FromResult(AuthenticateCore()); - }).Result; + }).GetAwaiter().GetResult(); } protected abstract AuthenticationTicket AuthenticateCore(); - public virtual async Task AuthenticateAsync(IAuthenticateContext context) - { - if (context.AuthenticationTypes.Contains(BaseOptions.AuthenticationType, StringComparer.Ordinal)) - { - AuthenticationTicket ticket = await AuthenticateAsync(); - if (ticket != null && ticket.Identity != null) - { - context.Authenticated(ticket.Identity, ticket.Properties.Dictionary, BaseOptions.Description.Dictionary); - } - } - - if (PriorHandler != null) - { - await PriorHandler.AuthenticateAsync(context); - } - } - /// /// Causes the authentication logic in AuthenticateCore to be performed for the current request /// at most once and returns the results. Calling Authenticate more than once will always return @@ -207,15 +243,29 @@ protected virtual Task AuthenticateCoreAsync() private void ApplyResponse() { - LazyInitializer.EnsureInitialized( - ref _applyResponse, - ref _applyResponseInitialized, - ref _applyResponseSyncLock, - () => + // If ApplyResponse already failed in the OnSendingHeaderCallback or TeardownAsync code path then a + // failed task is cached. If called again the same error will be re-thrown. This breaks error handling + // scenarios like the ability to display the error page or re-execute the request. + try + { + if (!Faulted) { - ApplyResponseCore(); - return Task.FromResult(0); - }).Wait(); // Block if the async version is in progress. + LazyInitializer.EnsureInitialized( + ref _applyResponse, + ref _applyResponseInitialized, + ref _applyResponseSyncLock, + () => + { + ApplyResponseCore(); + return Task.FromResult(0); + }).GetAwaiter().GetResult(); // Block if the async version is in progress. + } + } + catch (Exception) + { + Faulted = true; + throw; + } } protected virtual void ApplyResponseCore() @@ -230,13 +280,27 @@ protected virtual void ApplyResponseCore() /// or later, as the last step when the original async call to the middleware is returning. /// /// - private Task ApplyResponseAsync() + private async Task ApplyResponseAsync() { - return LazyInitializer.EnsureInitialized( - ref _applyResponse, - ref _applyResponseInitialized, - ref _applyResponseSyncLock, - ApplyResponseCoreAsync); + // If ApplyResponse already failed in the OnSendingHeaderCallback or TeardownAsync code path then a + // failed task is cached. If called again the same error will be re-thrown. This breaks error handling + // scenarios like the ability to display the error page or re-execute the request. + try + { + if (!Faulted) + { + await LazyInitializer.EnsureInitialized( + ref _applyResponse, + ref _applyResponseInitialized, + ref _applyResponseSyncLock, + ApplyResponseCoreAsync); + } + } + catch (Exception) + { + Faulted = true; + throw; + } } /// @@ -263,14 +327,13 @@ protected virtual Task ApplyResponseGrantAsync() return Task.FromResult(0); } - public virtual void SignIn(ISignInContext context) + public virtual void SignIn(SignInContext context) { - ClaimsIdentity identity; - if (SecurityHelper.LookupSignIn(context.Identities, BaseOptions.AuthenticationType, out identity)) + if (ShouldHandleScheme(context.AuthenticationScheme)) { - SignInIdentityContext = new SignInIdentityContext(identity, new AuthenticationProperties(context.Properties)); + SignInContext = context; SignOutContext = null; - context.Accept(BaseOptions.AuthenticationType, BaseOptions.Description.Dictionary); + context.Accept(); } if (PriorHandler != null) @@ -279,13 +342,13 @@ public virtual void SignIn(ISignInContext context) } } - public virtual void SignOut(ISignOutContext context) + public virtual void SignOut(SignOutContext context) { - if (SecurityHelper.LookupSignOut(context.AuthenticationTypes, BaseOptions.AuthenticationType, BaseOptions.AuthenticationMode)) + if (ShouldHandleScheme(context.AuthenticationScheme)) { - SignInIdentityContext = null; + SignInContext = null; SignOutContext = context; - context.Accept(BaseOptions.AuthenticationType, BaseOptions.Description.Dictionary); + context.Accept(); } if (PriorHandler != null) @@ -294,12 +357,12 @@ public virtual void SignOut(ISignOutContext context) } } - public virtual void Challenge(IChallengeContext context) + public virtual void Challenge(ChallengeContext context) { - if (SecurityHelper.LookupChallenge(context.AuthenticationTypes, BaseOptions.AuthenticationType, BaseOptions.AuthenticationMode)) + if (ShouldHandleScheme(context.AuthenticationScheme)) { ChallengeContext = context; - context.Accept(BaseOptions.AuthenticationType, BaseOptions.Description.Dictionary); + context.Accept(); } if (PriorHandler != null) @@ -310,6 +373,22 @@ public virtual void Challenge(IChallengeContext context) protected abstract void ApplyResponseChallenge(); + public virtual bool ShouldHandleScheme(string authenticationScheme) + { + return string.Equals(BaseOptions.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) || + (BaseOptions.AutomaticAuthentication && string.IsNullOrWhiteSpace(authenticationScheme)); + } + + public virtual bool ShouldConvertChallengeToForbidden() + { + // Return 403 iff 401 and this handler's authenticate was called + // and the challenge is for the authentication type + return Response.StatusCode == 401 && + AuthenticateCalled && + ChallengeContext != null && + ShouldHandleScheme(ChallengeContext.AuthenticationScheme); + } + /// /// Override this method to deal with 401 challenge concerns, if an authentication scheme in question /// deals an authentication interaction as part of it's request flow. (like adding a response header, or @@ -324,53 +403,54 @@ protected virtual Task ApplyResponseChallengeAsync() protected void GenerateCorrelationId([NotNull] AuthenticationProperties properties) { - string correlationKey = Constants.CorrelationPrefix + BaseOptions.AuthenticationType; + var correlationKey = Constants.CorrelationPrefix + BaseOptions.AuthenticationScheme; var nonceBytes = new byte[32]; CryptoRandom.GetBytes(nonceBytes); - string correlationId = TextEncodings.Base64Url.Encode(nonceBytes); + var correlationId = TextEncodings.Base64Url.Encode(nonceBytes); var cookieOptions = new CookieOptions { HttpOnly = true, - Secure = Request.IsSecure + Secure = Request.IsHttps }; - properties.Dictionary[correlationKey] = correlationId; + properties.Items[correlationKey] = correlationId; Response.Cookies.Append(correlationKey, correlationId, cookieOptions); } - [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", - MessageId = "Microsoft.Owin.Logging.LoggerExtensions.WriteWarning(Microsoft.Owin.Logging.ILogger,System.String,System.String[])", - Justification = "Logging is not Localized")] - protected bool ValidateCorrelationId([NotNull] AuthenticationProperties properties, [NotNull] ILogger logger) + protected bool ValidateCorrelationId([NotNull] AuthenticationProperties properties) { - string correlationKey = Constants.CorrelationPrefix + BaseOptions.AuthenticationType; - - string correlationCookie = Request.Cookies[correlationKey]; + var correlationKey = Constants.CorrelationPrefix + BaseOptions.AuthenticationScheme; + var correlationCookie = Request.Cookies[correlationKey]; if (string.IsNullOrWhiteSpace(correlationCookie)) { - logger.WriteWarning("{0} cookie not found.", correlationKey); + Logger.LogWarning("{0} cookie not found.", correlationKey); return false; } - Response.Cookies.Delete(correlationKey); + var cookieOptions = new CookieOptions + { + HttpOnly = true, + Secure = Request.IsHttps + }; + Response.Cookies.Delete(correlationKey, cookieOptions); string correlationExtra; - if (!properties.Dictionary.TryGetValue( + if (!properties.Items.TryGetValue( correlationKey, out correlationExtra)) { - logger.WriteWarning("{0} state property not found.", correlationKey); + Logger.LogWarning("{0} state property not found.", correlationKey); return false; } - properties.Dictionary.Remove(correlationKey); + properties.Items.Remove(correlationKey); if (!string.Equals(correlationCookie, correlationExtra, StringComparison.Ordinal)) { - logger.WriteWarning("{0} correlation cookie and state property mismatch.", correlationKey); + Logger.LogWarning("{0} correlation cookie and state property mismatch.", correlationKey); return false; } @@ -390,4 +470,4 @@ private void UnregisterAuthenticationHandler() auth.Handler = PriorHandler; } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler`1.cs b/src/Microsoft.AspNet.Authentication/AuthenticationHandler`1.cs similarity index 63% rename from src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler`1.cs rename to src/Microsoft.AspNet.Authentication/AuthenticationHandler`1.cs index 80944261c..edc127e26 100644 --- a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler`1.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationHandler`1.cs @@ -1,9 +1,12 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.Framework.Logging; +using Microsoft.Framework.WebEncoders; -namespace Microsoft.AspNet.Security.Infrastructure +namespace Microsoft.AspNet.Authentication { /// /// Base class for the per-request work performed by most authentication middleware. @@ -18,11 +21,12 @@ public abstract class AuthenticationHandler : AuthenticationHandler wh /// /// The original options passed by the application control behavior /// The utility object to observe the current request and response + /// The logging factory used to create loggers /// async completion - internal Task Initialize(TOptions options, HttpContext context) + public Task Initialize(TOptions options, HttpContext context, ILogger logger, IUrlEncoder encoder) { Options = options; - return BaseInitializeAsync(options, context); + return BaseInitializeAsync(options, context, logger, encoder); } } } diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs new file mode 100644 index 000000000..188f903c4 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs @@ -0,0 +1,84 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.Framework.Internal; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Authentication +{ + public abstract class AuthenticationMiddleware where TOptions : AuthenticationOptions, new() + { + private readonly RequestDelegate _next; + + protected AuthenticationMiddleware( + [NotNull] RequestDelegate next, + [NotNull] IOptions options, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IUrlEncoder encoder, + ConfigureOptions configureOptions) + { + if (configureOptions != null) + { + Options = options.GetNamedOptions(configureOptions.Name); + configureOptions.Configure(Options, configureOptions.Name); + } + else + { + Options = options.Options; + } + Logger = loggerFactory.CreateLogger(this.GetType().FullName); + UrlEncoder = encoder; + + if (string.IsNullOrEmpty(Options.ClaimsIssuer)) + { + // Default to something reasonable + Options.ClaimsIssuer = Options.AuthenticationScheme; + } + + _next = next; + } + + public string AuthenticationScheme { get; set; } + + public TOptions Options { get; set; } + + public ILogger Logger { get; set; } + + public IUrlEncoder UrlEncoder { get; set; } + + public async Task Invoke(HttpContext context) + { + var handler = CreateHandler(); + await handler.Initialize(Options, context, Logger, UrlEncoder); + try + { + if (!await handler.InvokeAsync()) + { + await _next(context); + } + } + catch (Exception) + { + try + { + handler.Faulted = true; + await handler.TeardownAsync(); + } + catch (Exception) + { + // Don't mask the original exception + } + throw; + } + await handler.TeardownAsync(); + } + + protected abstract AuthenticationHandler CreateHandler(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication/AuthenticationOptions.cs new file mode 100644 index 000000000..54f8a7a35 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/AuthenticationOptions.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http.Authentication; + +namespace Microsoft.AspNet.Authentication +{ + /// + /// Base Options for all authentication middleware + /// + public abstract class AuthenticationOptions + { + private string _authenticationScheme; + + /// + /// The AuthenticationScheme in the options corresponds to the logical name for a particular authentication scheme. A different + /// value may be assigned in order to use the same authentication middleware type more than once in a pipeline. + /// + public string AuthenticationScheme + { + get { return _authenticationScheme; } + set + { + _authenticationScheme = value; + Description.AuthenticationScheme = value; + } + } + + /// + /// If true the authentication middleware alter the request user coming in and + /// alter 401 Unauthorized responses going out. If false the authentication middleware will only provide + /// identity and alter responses when explicitly indicated by the AuthenticationScheme. + /// + public bool AutomaticAuthentication { get; set; } + + /// + /// Gets or sets the issuer that should be used for any claims that are created + /// + public string ClaimsIssuer { get; set; } + + /// + /// Additional information about the authentication type which is made available to the application. + /// + public AuthenticationDescription Description { get; set; } = new AuthenticationDescription(); + } +} diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Authentication/AuthenticationServiceCollectionExtensions.cs new file mode 100644 index 000000000..0b6cfdb9f --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/AuthenticationServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNet.Authentication; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.DependencyInjection +{ + public static class AuthenticationServiceCollectionExtensions + { + public static IServiceCollection AddAuthentication([NotNull] this IServiceCollection services) + { + services.AddWebEncoders(); + services.AddDataProtection(); + return services; + } + + public static IServiceCollection ConfigureClaimsTransformation([NotNull] this IServiceCollection services, [NotNull] Action configure) + { + return services.Configure(configure); + } + + public static IServiceCollection ConfigureClaimsTransformation([NotNull] this IServiceCollection services, [NotNull] Func transform) + { + return services.Configure(o => o.Transformation = transform); + } + + } +} diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationTicket.cs b/src/Microsoft.AspNet.Authentication/AuthenticationTicket.cs new file mode 100644 index 000000000..e72385d87 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/AuthenticationTicket.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNet.Http.Authentication; + +namespace Microsoft.AspNet.Authentication +{ + /// + /// Contains user identity information as well as additional authentication state. + /// + public class AuthenticationTicket + { + /// + /// Initializes a new instance of the class + /// + /// additional properties that can be consumed by the user or runtime. + /// the authentication middleware that was responsible for this ticket. + public AuthenticationTicket(AuthenticationProperties properties, string authenticationScheme) : this(null, properties, authenticationScheme) { } + + /// + /// Initializes a new instance of the class + /// + /// the that represents the authenticated user. + /// additional properties that can be consumed by the user or runtime. + /// the authentication middleware that was responsible for this ticket. + public AuthenticationTicket(ClaimsPrincipal principal, AuthenticationProperties properties, string authenticationScheme) + { + AuthenticationScheme = authenticationScheme; + Principal = principal; + Properties = properties ?? new AuthenticationProperties(); + } + + /// + /// Gets the authentication type. + /// + public string AuthenticationScheme { get; private set; } + + /// + /// Gets the claims-principal with authenticated user identities. + /// + public ClaimsPrincipal Principal{ get; private set; } + + /// + /// Additional state values for the authentication session. + /// + public AuthenticationProperties Properties { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenCreateContext.cs b/src/Microsoft.AspNet.Authentication/AuthenticationTokenCreateContext.cs similarity index 75% rename from src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenCreateContext.cs rename to src/Microsoft.AspNet.Authentication/AuthenticationTokenCreateContext.cs index 3a96006c5..ae0548f76 100644 --- a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenCreateContext.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationTokenCreateContext.cs @@ -1,10 +1,13 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. 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.AspNet.Http; -using Microsoft.AspNet.Security.Notifications; +using Microsoft.AspNet.Authentication.Notifications; +using Microsoft.Framework.Internal; -namespace Microsoft.AspNet.Security.Infrastructure +namespace Microsoft.AspNet.Authentication { public class AuthenticationTokenCreateContext : BaseContext { diff --git a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenProvider.cs b/src/Microsoft.AspNet.Authentication/AuthenticationTokenProvider.cs similarity index 91% rename from src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenProvider.cs rename to src/Microsoft.AspNet.Authentication/AuthenticationTokenProvider.cs index c8beb6f0b..9483538c6 100644 --- a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenProvider.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationTokenProvider.cs @@ -1,9 +1,11 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All 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.AspNet.Security.Infrastructure +namespace Microsoft.AspNet.Authentication { public class AuthenticationTokenProvider : IAuthenticationTokenProvider { diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationTokenReceiveContext.cs b/src/Microsoft.AspNet.Authentication/AuthenticationTokenReceiveContext.cs new file mode 100644 index 000000000..ec0c38b20 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/AuthenticationTokenReceiveContext.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Authentication.Notifications; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authentication +{ + public class AuthenticationTokenReceiveContext : BaseContext + { + public AuthenticationTokenReceiveContext( + [NotNull] HttpContext context, + [NotNull] string token) + : base(context) + { + Token = token; + } + + public string Token { get; protected set; } + + public AuthenticationTicket Ticket { get; protected set; } + + public void SetTicket([NotNull] AuthenticationTicket ticket) + { + Ticket = ticket; + } + } +} diff --git a/src/Microsoft.AspNet.Security/CertificateSubjectKeyIdentifierValidator.cs b/src/Microsoft.AspNet.Authentication/CertificateSubjectKeyIdentifierValidator.cs similarity index 92% rename from src/Microsoft.AspNet.Security/CertificateSubjectKeyIdentifierValidator.cs rename to src/Microsoft.AspNet.Authentication/CertificateSubjectKeyIdentifierValidator.cs index d25bb7223..3e19a3e68 100644 --- a/src/Microsoft.AspNet.Security/CertificateSubjectKeyIdentifierValidator.cs +++ b/src/Microsoft.AspNet.Authentication/CertificateSubjectKeyIdentifierValidator.cs @@ -1,11 +1,14 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. -#if NET45 +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if DNX451 using System; using System.Collections.Generic; using System.Net.Security; using System.Security.Cryptography.X509Certificates; +using Microsoft.Framework.Internal; -namespace Microsoft.AspNet.Security +namespace Microsoft.AspNet.Authentication { /// /// Provides pinned certificate validation based on the subject key identifier of the certificate. @@ -75,4 +78,4 @@ private static string GetSubjectKeyIdentifier(X509Certificate2 certificate) } } } -#endif \ No newline at end of file +#endif diff --git a/src/Microsoft.AspNet.Security/CertificateSubjectPublicKeyInfoValidator.cs b/src/Microsoft.AspNet.Authentication/CertificateSubjectPublicKeyInfoValidator.cs similarity index 92% rename from src/Microsoft.AspNet.Security/CertificateSubjectPublicKeyInfoValidator.cs rename to src/Microsoft.AspNet.Authentication/CertificateSubjectPublicKeyInfoValidator.cs index 79645c50d..11e827ae7 100644 --- a/src/Microsoft.AspNet.Security/CertificateSubjectPublicKeyInfoValidator.cs +++ b/src/Microsoft.AspNet.Authentication/CertificateSubjectPublicKeyInfoValidator.cs @@ -1,5 +1,7 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. -#if NET45 +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if DNX451 using System; using System.Collections.Generic; using System.ComponentModel; @@ -8,9 +10,10 @@ using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using Microsoft.Framework.Internal; using Microsoft.Win32; -namespace Microsoft.AspNet.Security +namespace Microsoft.AspNet.Authentication { /// /// Implements a cert pinning validator passed on @@ -33,12 +36,12 @@ public CertificateSubjectPublicKeyInfoValidator([NotNull] IEnumerable va if (_validBase64EncodedSubjectPublicKeyInfoHashes.Count == 0) { - throw new ArgumentOutOfRangeException("validBase64EncodedSubjectPublicKeyInfoHashes"); + throw new ArgumentOutOfRangeException(nameof(validBase64EncodedSubjectPublicKeyInfoHashes)); } if (_algorithm != SubjectPublicKeyInfoAlgorithm.Sha1 && _algorithm != SubjectPublicKeyInfoAlgorithm.Sha256) { - throw new ArgumentOutOfRangeException("algorithm"); + throw new ArgumentOutOfRangeException(nameof(algorithm)); } _algorithm = algorithm; @@ -122,4 +125,4 @@ private HashAlgorithm CreateHashAlgorithm() } } } -#endif \ No newline at end of file +#endif diff --git a/src/Microsoft.AspNet.Security/CertificateThumbprintValidator.cs b/src/Microsoft.AspNet.Authentication/CertificateThumbprintValidator.cs similarity index 88% rename from src/Microsoft.AspNet.Security/CertificateThumbprintValidator.cs rename to src/Microsoft.AspNet.Authentication/CertificateThumbprintValidator.cs index 5661d5591..6188375a1 100644 --- a/src/Microsoft.AspNet.Security/CertificateThumbprintValidator.cs +++ b/src/Microsoft.AspNet.Authentication/CertificateThumbprintValidator.cs @@ -1,11 +1,14 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. -#if NET45 +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if DNX451 using System; using System.Collections.Generic; using System.Net.Security; using System.Security.Cryptography.X509Certificates; +using Microsoft.Framework.Internal; -namespace Microsoft.AspNet.Security +namespace Microsoft.AspNet.Authentication { /// /// Provides pinned certificate validation based on the certificate thumbprint. @@ -24,7 +27,7 @@ public CertificateThumbprintValidator([NotNull] IEnumerable validThumbpr if (_validCertificateThumbprints.Count == 0) { - throw new ArgumentOutOfRangeException("validThumbprints"); + throw new ArgumentOutOfRangeException(nameof(validThumbprints)); } } @@ -68,4 +71,4 @@ public bool Validate(object sender, X509Certificate certificate, [NotNull] X509C } } } -#endif \ No newline at end of file +#endif diff --git a/src/Microsoft.AspNet.Authentication/ClaimsTransformationAppBuilderExtensions.cs b/src/Microsoft.AspNet.Authentication/ClaimsTransformationAppBuilderExtensions.cs new file mode 100644 index 000000000..c18317f83 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/ClaimsTransformationAppBuilderExtensions.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Authentication; +using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods provided by the claims transformation authentication middleware + /// + public static class ClaimsTransformationAppBuilderExtensions + { + /// + /// Adds a claims transformation middleware to your web application pipeline. + /// + /// The IApplicationBuilder passed to your configuration method + /// The original app parameter + public static IApplicationBuilder UseClaimsTransformation(this IApplicationBuilder app) + { + return app.UseClaimsTransformation(configureOptions: o => { }, optionsName: string.Empty); + } + + /// + /// Adds a claims transformation middleware to your web application pipeline. + /// + /// The IApplicationBuilder passed to your configuration method + /// Used to configure the options for the middleware + /// The original app parameter + public static IApplicationBuilder UseClaimsTransformation(this IApplicationBuilder app, [NotNull] Action configureOptions) + { + return app.UseClaimsTransformation(configureOptions: configureOptions, optionsName: string.Empty); + } + + /// + /// Adds a claims transformation middleware to your web application pipeline. + /// + /// The IApplicationBuilder passed to your configuration method + /// Used to configure the options for the middleware + /// The name of the options class that controls the middleware behavior, null will use the default options + /// The original app parameter + public static IApplicationBuilder UseClaimsTransformation(this IApplicationBuilder app, [NotNull] Action configureOptions, [NotNull] string optionsName) + { + return app.UseMiddleware( + new ConfigureOptions(configureOptions) { Name = optionsName }); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/ClaimsTransformationAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication/ClaimsTransformationAuthenticationHandler.cs new file mode 100644 index 000000000..4d896e152 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/ClaimsTransformationAuthenticationHandler.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.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Http.Features.Authentication; + +namespace Microsoft.AspNet.Authentication +{ + /// + /// Handler that applies ClaimsTransformation to authentication + /// + public class ClaimsTransformationAuthenticationHandler : IAuthenticationHandler + { + private readonly Func _transform; + + public ClaimsTransformationAuthenticationHandler(Func transform) + { + _transform = transform; + } + + public IAuthenticationHandler PriorHandler { get; set; } + + private void ApplyTransform(AuthenticateContext context) + { + if (_transform != null) + { + // REVIEW: this cast seems really bad (missing interface way to get the result back out?) + var authContext = context as AuthenticateContext; + if (authContext?.Principal != null) + { + context.Authenticated( + _transform.Invoke(authContext.Principal), + authContext.Properties, + authContext.Description); + } + } + + } + + public void Authenticate(AuthenticateContext context) + { + if (PriorHandler != null) + { + PriorHandler.Authenticate(context); + ApplyTransform(context); + } + } + + public async Task AuthenticateAsync(AuthenticateContext context) + { + if (PriorHandler != null) + { + await PriorHandler.AuthenticateAsync(context); + ApplyTransform(context); + } + } + + public void Challenge(ChallengeContext context) + { + if (PriorHandler != null) + { + PriorHandler.Challenge(context); + } + } + + public void GetDescriptions(DescribeSchemesContext context) + { + if (PriorHandler != null) + { + PriorHandler.GetDescriptions(context); + } + } + + public void SignIn(SignInContext context) + { + if (PriorHandler != null) + { + PriorHandler.SignIn(context); + } + } + + public void SignOut(SignOutContext context) + { + if (PriorHandler != null) + { + PriorHandler.SignOut(context); + } + } + + public void RegisterAuthenticationHandler(IHttpAuthenticationFeature auth) + { + PriorHandler = auth.Handler; + auth.Handler = this; + } + + public void UnregisterAuthenticationHandler(IHttpAuthenticationFeature auth) + { + auth.Handler = PriorHandler; + } + + } +} diff --git a/src/Microsoft.AspNet.Authentication/ClaimsTransformationMiddleware.cs b/src/Microsoft.AspNet.Authentication/ClaimsTransformationMiddleware.cs new file mode 100644 index 000000000..0f785161c --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/ClaimsTransformationMiddleware.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authentication +{ + public class ClaimsTransformationMiddleware + { + private readonly RequestDelegate _next; + + public ClaimsTransformationMiddleware( + [NotNull] RequestDelegate next, + [NotNull] IOptions options, + ConfigureOptions configureOptions) + { + if (configureOptions != null) + { + Options = options.GetNamedOptions(configureOptions.Name); + configureOptions.Configure(Options, configureOptions.Name); + } + else + { + Options = options.Options; + } + _next = next; + } + + public ClaimsTransformationOptions Options { get; set; } + + public async Task Invoke(HttpContext context) + { + var handler = new ClaimsTransformationAuthenticationHandler(Options.Transformation); + handler.RegisterAuthenticationHandler(context.GetAuthentication()); + try { + if (Options.Transformation != null) + { + context.User = Options.Transformation.Invoke(context.User); + } + await _next(context); + } + finally + { + handler.UnregisterAuthenticationHandler(context.GetAuthentication()); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/ClaimsTransformationOptions.cs b/src/Microsoft.AspNet.Authentication/ClaimsTransformationOptions.cs new file mode 100644 index 000000000..1eb76c82a --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/ClaimsTransformationOptions.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; + +namespace Microsoft.AspNet.Authentication +{ + public class ClaimsTransformationOptions + { + public Func Transformation { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication/Constants.cs b/src/Microsoft.AspNet.Authentication/Constants.cs new file mode 100644 index 000000000..e6b0d7b43 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Constants.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +namespace Microsoft.AspNet.Authentication +{ + internal static class Constants + { + public static string SecurityAuthenticate = "security.Authenticate"; + internal const string CorrelationPrefix = ".AspNet.Correlation."; + } +} diff --git a/src/Microsoft.AspNet.Security/DataHandler/Encoder/Base64TextEncoder.cs b/src/Microsoft.AspNet.Authentication/DataHandler/Encoder/Base64TextEncoder.cs similarity index 57% rename from src/Microsoft.AspNet.Security/DataHandler/Encoder/Base64TextEncoder.cs rename to src/Microsoft.AspNet.Authentication/DataHandler/Encoder/Base64TextEncoder.cs index 29acfcb20..073795f26 100644 --- a/src/Microsoft.AspNet.Security/DataHandler/Encoder/Base64TextEncoder.cs +++ b/src/Microsoft.AspNet.Authentication/DataHandler/Encoder/Base64TextEncoder.cs @@ -1,8 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET 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.AspNet.Security.DataHandler.Encoder +namespace Microsoft.AspNet.Authentication.DataHandler.Encoder { public class Base64TextEncoder : ITextEncoder { diff --git a/src/Microsoft.AspNet.Security/DataHandler/Encoder/Base64UrlTextEncoder.cs b/src/Microsoft.AspNet.Authentication/DataHandler/Encoder/Base64UrlTextEncoder.cs similarity index 72% rename from src/Microsoft.AspNet.Security/DataHandler/Encoder/Base64UrlTextEncoder.cs rename to src/Microsoft.AspNet.Authentication/DataHandler/Encoder/Base64UrlTextEncoder.cs index 250ea7019..28d74196f 100644 --- a/src/Microsoft.AspNet.Security/DataHandler/Encoder/Base64UrlTextEncoder.cs +++ b/src/Microsoft.AspNet.Authentication/DataHandler/Encoder/Base64UrlTextEncoder.cs @@ -1,8 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. 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.Framework.Internal; -namespace Microsoft.AspNet.Security.DataHandler.Encoder +namespace Microsoft.AspNet.Authentication.DataHandler.Encoder { public class Base64UrlTextEncoder : ITextEncoder { diff --git a/src/Microsoft.AspNet.Authentication/DataHandler/Encoder/ITextEncoder.cs b/src/Microsoft.AspNet.Authentication/DataHandler/Encoder/ITextEncoder.cs new file mode 100644 index 000000000..953fb6232 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/DataHandler/Encoder/ITextEncoder.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +namespace Microsoft.AspNet.Authentication.DataHandler.Encoder +{ + public interface ITextEncoder + { + string Encode(byte[] data); + byte[] Decode(string text); + } +} diff --git a/src/Microsoft.AspNet.Security/DataHandler/Encoder/TextEncodings.cs b/src/Microsoft.AspNet.Authentication/DataHandler/Encoder/TextEncodings.cs similarity index 66% rename from src/Microsoft.AspNet.Security/DataHandler/Encoder/TextEncodings.cs rename to src/Microsoft.AspNet.Authentication/DataHandler/Encoder/TextEncodings.cs index 45b29107d..5e6bbde93 100644 --- a/src/Microsoft.AspNet.Security/DataHandler/Encoder/TextEncodings.cs +++ b/src/Microsoft.AspNet.Authentication/DataHandler/Encoder/TextEncodings.cs @@ -1,6 +1,8 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (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.AspNet.Security.DataHandler.Encoder + +namespace Microsoft.AspNet.Authentication.DataHandler.Encoder { public static class TextEncodings { diff --git a/src/Microsoft.AspNet.Authentication/DataHandler/ISecureDataFormat.cs b/src/Microsoft.AspNet.Authentication/DataHandler/ISecureDataFormat.cs new file mode 100644 index 000000000..e9587de00 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/DataHandler/ISecureDataFormat.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +namespace Microsoft.AspNet.Authentication +{ + public interface ISecureDataFormat + { + string Protect(TData data); + TData Unprotect(string protectedText); + } +} diff --git a/src/Microsoft.AspNet.Authentication/DataHandler/PropertiesDataFormat.cs b/src/Microsoft.AspNet.Authentication/DataHandler/PropertiesDataFormat.cs new file mode 100644 index 000000000..127554ad8 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/DataHandler/PropertiesDataFormat.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.DataProtection; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Authentication.DataHandler.Encoder; +using Microsoft.AspNet.Authentication.DataHandler.Serializer; + +namespace Microsoft.AspNet.Authentication.DataHandler +{ + public class PropertiesDataFormat : SecureDataFormat + { + public PropertiesDataFormat(IDataProtector protector) + : base(DataSerializers.Properties, protector, TextEncodings.Base64Url) + { + } + } +} diff --git a/src/Microsoft.AspNet.Security/DataHandler/SecureDataFormat.cs b/src/Microsoft.AspNet.Authentication/DataHandler/SecureDataFormat.cs similarity index 82% rename from src/Microsoft.AspNet.Security/DataHandler/SecureDataFormat.cs rename to src/Microsoft.AspNet.Authentication/DataHandler/SecureDataFormat.cs index fa045e780..6221e498a 100644 --- a/src/Microsoft.AspNet.Security/DataHandler/SecureDataFormat.cs +++ b/src/Microsoft.AspNet.Authentication/DataHandler/SecureDataFormat.cs @@ -1,11 +1,13 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNet.Security.DataHandler.Encoder; -using Microsoft.AspNet.Security.DataHandler.Serializer; -using Microsoft.AspNet.Security.DataProtection; +using Microsoft.AspNet.DataProtection; +using Microsoft.AspNet.Authentication.DataHandler.Encoder; +using Microsoft.AspNet.Authentication.DataHandler.Serializer; -namespace Microsoft.AspNet.Security.DataHandler +namespace Microsoft.AspNet.Authentication.DataHandler { public class SecureDataFormat : ISecureDataFormat { @@ -60,4 +62,4 @@ public TData Unprotect(string protectedText) } } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/DataHandler/Serializer/DataSerializers.cs b/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/DataSerializers.cs similarity index 50% rename from src/Microsoft.AspNet.Security/DataHandler/Serializer/DataSerializers.cs rename to src/Microsoft.AspNet.Authentication/DataHandler/Serializer/DataSerializers.cs index faf395aab..0c185a9b2 100644 --- a/src/Microsoft.AspNet.Security/DataHandler/Serializer/DataSerializers.cs +++ b/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/DataSerializers.cs @@ -1,8 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .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.AspNet.Http.Security; -namespace Microsoft.AspNet.Security.DataHandler.Serializer +using Microsoft.AspNet.Http.Authentication; + +namespace Microsoft.AspNet.Authentication.DataHandler.Serializer { public static class DataSerializers { @@ -12,8 +14,8 @@ static DataSerializers() Ticket = new TicketSerializer(); } - public static IDataSerializer Properties { get; set; } + public static IDataSerializer Properties { get; private set; } - public static IDataSerializer Ticket { get; set; } + public static IDataSerializer Ticket { get; private set; } } } diff --git a/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/IDataSerializer.cs b/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/IDataSerializer.cs new file mode 100644 index 000000000..d6cfd18b2 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/IDataSerializer.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +namespace Microsoft.AspNet.Authentication.DataHandler.Serializer +{ + public interface IDataSerializer + { + byte[] Serialize(TModel model); + TModel Deserialize(byte[] data); + } +} diff --git a/src/Microsoft.AspNet.Security/DataHandler/Serializer/PropertiesSerializer.cs b/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/PropertiesSerializer.cs similarity index 83% rename from src/Microsoft.AspNet.Security/DataHandler/Serializer/PropertiesSerializer.cs rename to src/Microsoft.AspNet.Authentication/DataHandler/Serializer/PropertiesSerializer.cs index d8bfc7209..5120219ff 100644 --- a/src/Microsoft.AspNet.Security/DataHandler/Serializer/PropertiesSerializer.cs +++ b/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/PropertiesSerializer.cs @@ -1,12 +1,15 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.Framework.Internal; -namespace Microsoft.AspNet.Security.DataHandler.Serializer +namespace Microsoft.AspNet.Authentication.DataHandler.Serializer { public class PropertiesSerializer : IDataSerializer { @@ -41,8 +44,8 @@ public AuthenticationProperties Deserialize(byte[] data) public static void Write([NotNull] BinaryWriter writer, [NotNull] AuthenticationProperties properties) { writer.Write(FormatVersion); - writer.Write(properties.Dictionary.Count); - foreach (var kv in properties.Dictionary) + writer.Write(properties.Items.Count); + foreach (var kv in properties.Items) { writer.Write(kv.Key); writer.Write(kv.Value); diff --git a/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/TicketSerializer.cs b/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/TicketSerializer.cs new file mode 100644 index 000000000..71c8b5856 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/TicketSerializer.cs @@ -0,0 +1,126 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Security.Claims; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authentication.DataHandler.Serializer +{ + public class TicketSerializer : IDataSerializer + { + private const int FormatVersion = 2; + + public virtual byte[] Serialize(AuthenticationTicket model) + { + using (var memory = new MemoryStream()) + { + using (var writer = new BinaryWriter(memory)) + { + Write(writer, model); + } + return memory.ToArray(); + } + } + + public virtual AuthenticationTicket Deserialize(byte[] data) + { + using (var memory = new MemoryStream(data)) + { + using (var reader = new BinaryReader(memory)) + { + return Read(reader); + } + } + } + + public static void Write([NotNull] BinaryWriter writer, [NotNull] AuthenticationTicket model) + { + writer.Write(FormatVersion); + writer.Write(model.AuthenticationScheme); + var principal = model.Principal; + writer.Write(principal.Identities.Count()); + foreach (var identity in principal.Identities) + { + var authenticationType = string.IsNullOrWhiteSpace(identity.AuthenticationType) ? string.Empty : identity.AuthenticationType; + writer.Write(authenticationType); + WriteWithDefault(writer, identity.NameClaimType, DefaultValues.NameClaimType); + WriteWithDefault(writer, identity.RoleClaimType, DefaultValues.RoleClaimType); + writer.Write(identity.Claims.Count()); + foreach (var claim in identity.Claims) + { + WriteWithDefault(writer, claim.Type, identity.NameClaimType); + writer.Write(claim.Value); + WriteWithDefault(writer, claim.ValueType, DefaultValues.StringValueType); + WriteWithDefault(writer, claim.Issuer, DefaultValues.LocalAuthority); + WriteWithDefault(writer, claim.OriginalIssuer, claim.Issuer); + } + } + PropertiesSerializer.Write(writer, model.Properties); + } + + public static AuthenticationTicket Read([NotNull] BinaryReader reader) + { + if (reader.ReadInt32() != FormatVersion) + { + return null; + } + string authenticationScheme = reader.ReadString(); + int identityCount = reader.ReadInt32(); + var identities = new ClaimsIdentity[identityCount]; + for (int i = 0; i != identityCount; ++i) + { + string authenticationType = reader.ReadString(); + string nameClaimType = ReadWithDefault(reader, DefaultValues.NameClaimType); + string roleClaimType = ReadWithDefault(reader, DefaultValues.RoleClaimType); + int count = reader.ReadInt32(); + var claims = new Claim[count]; + for (int index = 0; index != count; ++index) + { + string type = ReadWithDefault(reader, nameClaimType); + string value = reader.ReadString(); + string valueType = ReadWithDefault(reader, DefaultValues.StringValueType); + string issuer = ReadWithDefault(reader, DefaultValues.LocalAuthority); + string originalIssuer = ReadWithDefault(reader, issuer); + claims[index] = new Claim(type, value, valueType, issuer, originalIssuer); + } + identities[i] = new ClaimsIdentity(claims, authenticationType, nameClaimType, roleClaimType); + } + var properties = PropertiesSerializer.Read(reader); + return new AuthenticationTicket(new ClaimsPrincipal(identities), properties, authenticationScheme); + } + + private static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue) + { + if (string.Equals(value, defaultValue, StringComparison.Ordinal)) + { + writer.Write(DefaultValues.DefaultStringPlaceholder); + } + else + { + writer.Write(value); + } + } + + private static string ReadWithDefault(BinaryReader reader, string defaultValue) + { + string value = reader.ReadString(); + if (string.Equals(value, DefaultValues.DefaultStringPlaceholder, StringComparison.Ordinal)) + { + return defaultValue; + } + return value; + } + + private static class DefaultValues + { + public const string DefaultStringPlaceholder = "\0"; + public const string NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"; + public const string RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"; + public const string LocalAuthority = "LOCAL AUTHORITY"; + public const string StringValueType = "http://www.w3.org/2001/XMLSchema#string"; + } + } +} diff --git a/src/Microsoft.AspNet.Authentication/DataHandler/TicketDataFormat.cs b/src/Microsoft.AspNet.Authentication/DataHandler/TicketDataFormat.cs new file mode 100644 index 000000000..6dd719aec --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/DataHandler/TicketDataFormat.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.DataProtection; +using Microsoft.AspNet.Authentication.DataHandler.Encoder; +using Microsoft.AspNet.Authentication.DataHandler.Serializer; + +namespace Microsoft.AspNet.Authentication.DataHandler +{ + public class TicketDataFormat : SecureDataFormat + { + public TicketDataFormat(IDataProtector protector) : base(DataSerializers.Ticket, protector, TextEncodings.Base64Url) + { + } + } +} diff --git a/src/Microsoft.AspNet.Authentication/ExternalAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication/ExternalAuthenticationOptions.cs new file mode 100644 index 000000000..19bd05fa9 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/ExternalAuthenticationOptions.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +using System; + +namespace Microsoft.AspNet.Authentication +{ + public class ExternalAuthenticationOptions + { + /// + /// Gets or sets the authentication scheme corresponding to the default middleware + /// responsible of persisting user's identity after a successful authentication. + /// This value typically corresponds to a cookie middleware registered in the Startup class. + /// + public string SignInScheme { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security/Infrastructure/HttpContextExtensions.cs b/src/Microsoft.AspNet.Authentication/HttpContextExtensions.cs similarity index 56% rename from src/Microsoft.AspNet.Security/Infrastructure/HttpContextExtensions.cs rename to src/Microsoft.AspNet.Authentication/HttpContextExtensions.cs index 4336544d3..4617b02f4 100644 --- a/src/Microsoft.AspNet.Security/Infrastructure/HttpContextExtensions.cs +++ b/src/Microsoft.AspNet.Authentication/HttpContextExtensions.cs @@ -1,8 +1,11 @@ -using Microsoft.AspNet.Http; -using Microsoft.AspNet.HttpFeature.Security; -using Microsoft.AspNet.PipelineCore.Security; +// Copyright (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.AspNet.Security.Infrastructure +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features.Authentication; +using Microsoft.AspNet.Http.Features.Authentication.Internal; + +namespace Microsoft.AspNet.Authentication { internal static class HttpContextExtensions { diff --git a/src/Microsoft.AspNet.Security/Infrastructure/IAuthenticationTokenProvider.cs b/src/Microsoft.AspNet.Authentication/IAuthenticationTokenProvider.cs similarity index 63% rename from src/Microsoft.AspNet.Security/Infrastructure/IAuthenticationTokenProvider.cs rename to src/Microsoft.AspNet.Authentication/IAuthenticationTokenProvider.cs index 7bda97d3a..3315fa21f 100644 --- a/src/Microsoft.AspNet.Security/Infrastructure/IAuthenticationTokenProvider.cs +++ b/src/Microsoft.AspNet.Authentication/IAuthenticationTokenProvider.cs @@ -1,8 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System.Threading.Tasks; -namespace Microsoft.AspNet.Security.Infrastructure +namespace Microsoft.AspNet.Authentication { public interface IAuthenticationTokenProvider { diff --git a/src/Microsoft.AspNet.Security/ICertificateValidator.cs b/src/Microsoft.AspNet.Authentication/ICertificateValidator.cs similarity index 84% rename from src/Microsoft.AspNet.Security/ICertificateValidator.cs rename to src/Microsoft.AspNet.Authentication/ICertificateValidator.cs index 3b84fb485..4e8c2d41e 100644 --- a/src/Microsoft.AspNet.Security/ICertificateValidator.cs +++ b/src/Microsoft.AspNet.Authentication/ICertificateValidator.cs @@ -1,10 +1,12 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. -#if NET45 +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if DNX451 using System; using System.Net.Security; using System.Security.Cryptography.X509Certificates; -namespace Microsoft.AspNet.Security +namespace Microsoft.AspNet.Authentication { /// /// Interface for providing pinned certificate validation, which checks HTTPS @@ -25,4 +27,4 @@ public interface ICertificateValidator bool Validate(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors); } } -#endif \ No newline at end of file +#endif diff --git a/src/Microsoft.AspNet.Security/Infrastructure/ISystemClock.cs b/src/Microsoft.AspNet.Authentication/ISystemClock.cs similarity index 58% rename from src/Microsoft.AspNet.Security/Infrastructure/ISystemClock.cs rename to src/Microsoft.AspNet.Authentication/ISystemClock.cs index cca20fbcb..dedb6d36a 100644 --- a/src/Microsoft.AspNet.Security/Infrastructure/ISystemClock.cs +++ b/src/Microsoft.AspNet.Authentication/ISystemClock.cs @@ -1,8 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET 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.AspNet.Security.Infrastructure +namespace Microsoft.AspNet.Authentication { /// /// Abstracts the system clock to facilitate testing. diff --git a/src/Microsoft.AspNet.Authentication/Microsoft.AspNet.Authentication.xproj b/src/Microsoft.AspNet.Authentication/Microsoft.AspNet.Authentication.xproj new file mode 100644 index 000000000..fe12613f9 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Microsoft.AspNet.Authentication.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 2286250a-52c8-4126-9f93-b1e45f0ad078 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/Notifications/AuthenticationFailedNotification.cs b/src/Microsoft.AspNet.Authentication/Notifications/AuthenticationFailedNotification.cs new file mode 100644 index 000000000..4f912dc1c --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Notifications/AuthenticationFailedNotification.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Authentication.Notifications +{ + public class AuthenticationFailedNotification : BaseNotification + { + public AuthenticationFailedNotification(HttpContext context, TOptions options) : base(context, options) + { + } + + public Exception Exception { get; set; } + + public TMessage ProtocolMessage { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Notifications/BaseContext.cs b/src/Microsoft.AspNet.Authentication/Notifications/BaseContext.cs similarity index 67% rename from src/Microsoft.AspNet.Security/Notifications/BaseContext.cs rename to src/Microsoft.AspNet.Authentication/Notifications/BaseContext.cs index 2ad464e37..4215020dc 100644 --- a/src/Microsoft.AspNet.Security/Notifications/BaseContext.cs +++ b/src/Microsoft.AspNet.Authentication/Notifications/BaseContext.cs @@ -1,8 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .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.AspNet.Http; -namespace Microsoft.AspNet.Security.Notifications +namespace Microsoft.AspNet.Authentication.Notifications { public abstract class BaseContext { diff --git a/src/Microsoft.AspNet.Security/Notifications/BaseContext`1.cs b/src/Microsoft.AspNet.Authentication/Notifications/BaseContext`1.cs similarity index 74% rename from src/Microsoft.AspNet.Security/Notifications/BaseContext`1.cs rename to src/Microsoft.AspNet.Authentication/Notifications/BaseContext`1.cs index 5073cd183..b124fa717 100644 --- a/src/Microsoft.AspNet.Security/Notifications/BaseContext`1.cs +++ b/src/Microsoft.AspNet.Authentication/Notifications/BaseContext`1.cs @@ -1,8 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .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.AspNet.Http; -namespace Microsoft.AspNet.Security.Notifications +namespace Microsoft.AspNet.Authentication.Notifications { /// /// Base class used for certain event contexts diff --git a/src/Microsoft.AspNet.Authentication/Notifications/BaseNotification.cs b/src/Microsoft.AspNet.Authentication/Notifications/BaseNotification.cs new file mode 100644 index 000000000..50b626d1a --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Notifications/BaseNotification.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Authentication.Notifications +{ + public class BaseNotification : BaseContext + { + protected BaseNotification(HttpContext context, TOptions options) : base(context, options) + { + } + + public NotificationResultState State { get; set; } + + public bool HandledResponse + { + get { return State == NotificationResultState.HandledResponse; } + } + + public bool Skipped + { + get { return State == NotificationResultState.Skipped; } + } + + /// + /// Discontinue all processing for this request and return to the client. + /// The caller is responsible for generating the full response. + /// Set the to trigger SignIn. + /// + public void HandleResponse() + { + State = NotificationResultState.HandledResponse; + } + + /// + /// Discontinue processing the request in the current middleware and pass control to the next one. + /// SignIn will not be called. + /// + public void SkipToNextMiddleware() + { + State = NotificationResultState.Skipped; + } + + /// + /// Gets or set the to return if this notification signals it handled the notification. + /// + public AuthenticationTicket AuthenticationTicket { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Notifications/EndpointContext.cs b/src/Microsoft.AspNet.Authentication/Notifications/EndpointContext.cs similarity index 62% rename from src/Microsoft.AspNet.Security/Notifications/EndpointContext.cs rename to src/Microsoft.AspNet.Authentication/Notifications/EndpointContext.cs index 214a724a4..096de20ee 100644 --- a/src/Microsoft.AspNet.Security/Notifications/EndpointContext.cs +++ b/src/Microsoft.AspNet.Authentication/Notifications/EndpointContext.cs @@ -1,8 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .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.AspNet.Http; -namespace Microsoft.AspNet.Security.Notifications +namespace Microsoft.AspNet.Authentication.Notifications { public abstract class EndpointContext : BaseContext { diff --git a/src/Microsoft.AspNet.Security/Notifications/EndpointContext`1.cs b/src/Microsoft.AspNet.Authentication/Notifications/EndpointContext`1.cs similarity index 80% rename from src/Microsoft.AspNet.Security/Notifications/EndpointContext`1.cs rename to src/Microsoft.AspNet.Authentication/Notifications/EndpointContext`1.cs index 80b5c2c55..f7a9a1e97 100644 --- a/src/Microsoft.AspNet.Security/Notifications/EndpointContext`1.cs +++ b/src/Microsoft.AspNet.Authentication/Notifications/EndpointContext`1.cs @@ -1,8 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .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.AspNet.Http; -namespace Microsoft.AspNet.Security.Notifications +namespace Microsoft.AspNet.Authentication.Notifications { /// /// Base class used for certain event contexts diff --git a/src/Microsoft.AspNet.Authentication/Notifications/MessageReceivedNotification.cs b/src/Microsoft.AspNet.Authentication/Notifications/MessageReceivedNotification.cs new file mode 100644 index 000000000..32df87bab --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Notifications/MessageReceivedNotification.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Authentication.Notifications +{ + public class MessageReceivedNotification : BaseNotification + { + public MessageReceivedNotification(HttpContext context, TOptions options) : base(context, options) + { + } + + public TMessage ProtocolMessage { get; set; } + + /// + /// Bearer Token. This will give application an opportunity to retrieve token from an alternation location. + /// + public string Token { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/Notifications/NotificationResultState.cs b/src/Microsoft.AspNet.Authentication/Notifications/NotificationResultState.cs new file mode 100644 index 000000000..7434ebd82 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Notifications/NotificationResultState.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +using System; + +namespace Microsoft.AspNet.Authentication.Notifications +{ + public enum NotificationResultState + { + /// + /// Continue with normal processing. + /// + Continue, + + /// + /// Discontinue processing the request in the current middleware and pass control to the next one. + /// + Skipped, + + /// + /// Discontinue all processing for this request. + /// + HandledResponse + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/Notifications/RedirectFromIdentityProviderNotification.cs b/src/Microsoft.AspNet.Authentication/Notifications/RedirectFromIdentityProviderNotification.cs new file mode 100644 index 000000000..970d9d718 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Notifications/RedirectFromIdentityProviderNotification.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Authentication.Notifications +{ + public class RedirectFromIdentityProviderNotification : BaseNotification + { + public RedirectFromIdentityProviderNotification(HttpContext context, TOptions options) + : base(context, options) + { + } + + public string SignInScheme { get; set; } + + public bool IsRequestCompleted { get; set; } + + public TMessage ProtocolMessage { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication/Notifications/RedirectToIdentityProviderNotification.cs b/src/Microsoft.AspNet.Authentication/Notifications/RedirectToIdentityProviderNotification.cs new file mode 100644 index 000000000..a4b2d979e --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Notifications/RedirectToIdentityProviderNotification.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Authentication.Notifications +{ + public class RedirectToIdentityProviderNotification : BaseNotification + { + public RedirectToIdentityProviderNotification(HttpContext context, TOptions options) : base(context, options) + { + } + + public TMessage ProtocolMessage { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security/Notifications/ReturnEndpointContext.cs b/src/Microsoft.AspNet.Authentication/Notifications/ReturnEndpointContext.cs similarity index 62% rename from src/Microsoft.AspNet.Security/Notifications/ReturnEndpointContext.cs rename to src/Microsoft.AspNet.Authentication/Notifications/ReturnEndpointContext.cs index ff88098f5..ac4fa7b3c 100644 --- a/src/Microsoft.AspNet.Security/Notifications/ReturnEndpointContext.cs +++ b/src/Microsoft.AspNet.Authentication/Notifications/ReturnEndpointContext.cs @@ -1,11 +1,13 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using Microsoft.AspNet.Http; -using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Http.Authentication; -namespace Microsoft.AspNet.Security.Notifications +namespace Microsoft.AspNet.Authentication.Notifications { public abstract class ReturnEndpointContext : EndpointContext { @@ -16,15 +18,15 @@ protected ReturnEndpointContext( { if (ticket != null) { - Identity = ticket.Identity; + Principal = ticket.Principal; Properties = ticket.Properties; } } - public ClaimsIdentity Identity { get; set; } + public ClaimsPrincipal Principal { get; set; } public AuthenticationProperties Properties { get; set; } - public string SignInAsAuthenticationType { get; set; } + public string SignInScheme { get; set; } [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "By design")] public string RedirectUri { get; set; } diff --git a/src/Microsoft.AspNet.Authentication/Notifications/SecurityTokenReceivedNotification.cs b/src/Microsoft.AspNet.Authentication/Notifications/SecurityTokenReceivedNotification.cs new file mode 100644 index 000000000..a0208b8b3 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Notifications/SecurityTokenReceivedNotification.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Authentication.Notifications +{ + public class SecurityTokenReceivedNotification : BaseNotification + { + public SecurityTokenReceivedNotification(HttpContext context, TOptions options) : base(context, options) + { + } + + public string SecurityToken { get; set; } + + public TMessage ProtocolMessage { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication/Notifications/SecurityTokenValidatedNotification.cs b/src/Microsoft.AspNet.Authentication/Notifications/SecurityTokenValidatedNotification.cs new file mode 100644 index 000000000..82c641ca1 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Notifications/SecurityTokenValidatedNotification.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Authentication.Notifications +{ + public class SecurityTokenValidatedNotification : BaseNotification + { + public SecurityTokenValidatedNotification(HttpContext context, TOptions options) : base(context, options) + { + } + + public TMessage ProtocolMessage { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authentication/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Authentication/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..025a94598 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +[assembly: AssemblyMetadata("Serviceable", "True")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Authentication/Properties/Resources.Designer.cs new file mode 100644 index 000000000..b1dc46b06 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/Properties/Resources.Designer.cs @@ -0,0 +1,78 @@ +// +namespace Microsoft.AspNet.Authentication +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Authentication.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The default data protection provider may only be used when the IApplicationBuilder.Properties contains an appropriate 'host.AppName' key. + /// + internal static string Exception_DefaultDpapiRequiresAppNameKey + { + get { return GetString("Exception_DefaultDpapiRequiresAppNameKey"); } + } + + /// + /// The default data protection provider may only be used when the IApplicationBuilder.Properties contains an appropriate 'host.AppName' key. + /// + internal static string FormatException_DefaultDpapiRequiresAppNameKey() + { + return GetString("Exception_DefaultDpapiRequiresAppNameKey"); + } + + /// + /// The state passed to UnhookAuthentication may only be the return value from HookAuthentication. + /// + internal static string Exception_UnhookAuthenticationStateType + { + get { return GetString("Exception_UnhookAuthenticationStateType"); } + } + + /// + /// The state passed to UnhookAuthentication may only be the return value from HookAuthentication. + /// + internal static string FormatException_UnhookAuthenticationStateType() + { + return GetString("Exception_UnhookAuthenticationStateType"); + } + + /// + /// The AuthenticationTokenProvider's required synchronous events have not been registered. + /// + internal static string Exception_AuthenticationTokenDoesNotProvideSyncMethods + { + get { return GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods"); } + } + + /// + /// The AuthenticationTokenProvider's required synchronous events have not been registered. + /// + internal static string FormatException_AuthenticationTokenDoesNotProvideSyncMethods() + { + return GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods"); + } + + 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.AspNet.Security/Resources.resx b/src/Microsoft.AspNet.Authentication/Resources.resx similarity index 94% rename from src/Microsoft.AspNet.Security/Resources.resx rename to src/Microsoft.AspNet.Authentication/Resources.resx index fdd098066..77060045e 100644 --- a/src/Microsoft.AspNet.Security/Resources.resx +++ b/src/Microsoft.AspNet.Authentication/Resources.resx @@ -118,14 +118,11 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - The default data protection provider may only be used when the IAppBuilder.Properties contains an appropriate 'host.AppName' key. + The default data protection provider may only be used when the IApplicationBuilder.Properties contains an appropriate 'host.AppName' key. The state passed to UnhookAuthentication may only be the return value from HookAuthentication. - - A default value for SignInAsAuthenticationType was not found in IAppBuilder Properties. This can happen if your authentication middleware are added in the wrong order, or if one is missing. - The AuthenticationTokenProvider's required synchronous events have not been registered. diff --git a/src/Microsoft.AspNet.Authentication/SecurityHelper.cs b/src/Microsoft.AspNet.Authentication/SecurityHelper.cs new file mode 100644 index 000000000..5f5c765b3 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/SecurityHelper.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNet.Http; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authentication +{ + /// + /// Helper code used when implementing authentication middleware + /// + public static class SecurityHelper + { + /// + /// Add all ClaimsIdenities from an additional ClaimPrincipal to the ClaimsPrincipal + /// Merges a new claims principal, placing all new identities first, and eliminating + /// any empty unauthenticated identities from context.User + /// + /// + public static void AddUserPrincipal([NotNull] HttpContext context, [NotNull] ClaimsPrincipal principal) + { + var newPrincipal = new ClaimsPrincipal(); + // New principal identities go first + newPrincipal.AddIdentities(principal.Identities); + + // Then add any existing non empty or authenticated identities + var existingPrincipal = context.User; + if (existingPrincipal != null) + { + newPrincipal.AddIdentities(existingPrincipal.Identities.Where(i => i.IsAuthenticated || i.Claims.Count() > 0)); + } + context.User = newPrincipal; + } + } +} diff --git a/src/Microsoft.AspNet.Security/SubjectPublicKeyInfoAlgorithm.cs b/src/Microsoft.AspNet.Authentication/SubjectPublicKeyInfoAlgorithm.cs similarity index 72% rename from src/Microsoft.AspNet.Security/SubjectPublicKeyInfoAlgorithm.cs rename to src/Microsoft.AspNet.Authentication/SubjectPublicKeyInfoAlgorithm.cs index 2987f6874..fa5704f34 100644 --- a/src/Microsoft.AspNet.Security/SubjectPublicKeyInfoAlgorithm.cs +++ b/src/Microsoft.AspNet.Authentication/SubjectPublicKeyInfoAlgorithm.cs @@ -1,6 +1,8 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (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.AspNet.Security + +namespace Microsoft.AspNet.Authentication { /// /// The algorithm used to generate the subject public key information blob hashes. diff --git a/src/Microsoft.AspNet.Security/Infrastructure/SystemClock.cs b/src/Microsoft.AspNet.Authentication/SystemClock.cs similarity index 76% rename from src/Microsoft.AspNet.Security/Infrastructure/SystemClock.cs rename to src/Microsoft.AspNet.Authentication/SystemClock.cs index 2dcbca0fe..405b8afa2 100644 --- a/src/Microsoft.AspNet.Security/Infrastructure/SystemClock.cs +++ b/src/Microsoft.AspNet.Authentication/SystemClock.cs @@ -1,8 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET 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.AspNet.Security.Infrastructure +namespace Microsoft.AspNet.Authentication { /// /// Provides access to the normal system clock. diff --git a/src/Microsoft.AspNet.Security/Win32.cs b/src/Microsoft.AspNet.Authentication/Win32.cs similarity index 95% rename from src/Microsoft.AspNet.Security/Win32.cs rename to src/Microsoft.AspNet.Authentication/Win32.cs index fe26feb7b..0e757e42a 100644 --- a/src/Microsoft.AspNet.Security/Win32.cs +++ b/src/Microsoft.AspNet.Authentication/Win32.cs @@ -1,4 +1,6 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using System.ComponentModel; diff --git a/src/Microsoft.AspNet.Authentication/project.json b/src/Microsoft.AspNet.Authentication/project.json new file mode 100644 index 000000000..bdf51537f --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/project.json @@ -0,0 +1,21 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET 5 common types used by the various authentication middleware.", + "dependencies": { + "Microsoft.AspNet.DataProtection": "1.0.0-*", + "Microsoft.AspNet.Http": "1.0.0-*", + "Microsoft.AspNet.Http.Extensions": "1.0.0-*", + "Microsoft.Framework.Logging.Abstractions": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" }, + "Microsoft.Framework.OptionsModel": "1.0.0-*", + "Microsoft.Framework.WebEncoders": "1.0.0-*" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { + "dependencies": { + "System.Security.Cryptography.RandomNumberGenerator": "4.0.0-beta-*" + } + } + } +} diff --git a/src/Microsoft.AspNet.Authorization/AuthorizationContext.cs b/src/Microsoft.AspNet.Authorization/AuthorizationContext.cs new file mode 100644 index 000000000..b117f8851 --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/AuthorizationContext.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authorization +{ + /// + /// Contains authorization information used by . + /// + public class AuthorizationContext + { + private HashSet _pendingRequirements; + private bool _failCalled; + private bool _succeedCalled; + + public AuthorizationContext( + [NotNull] IEnumerable requirements, + ClaimsPrincipal user, + object resource) + { + Requirements = requirements; + User = user; + Resource = resource; + _pendingRequirements = new HashSet(requirements); + } + + public IEnumerable Requirements { get; private set; } + public ClaimsPrincipal User { get; private set; } + public object Resource { get; private set; } + + public IEnumerable PendingRequirements { get { return _pendingRequirements; } } + + public bool HasFailed { get { return _failCalled; } } + + public bool HasSucceeded { + get + { + return !_failCalled && _succeedCalled && !PendingRequirements.Any(); + } + } + + public void Fail() + { + _failCalled = true; + } + + public void Succeed(IAuthorizationRequirement requirement) + { + _succeedCalled = true; + _pendingRequirements.Remove(requirement); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/AuthorizationHandler.cs b/src/Microsoft.AspNet.Authorization/AuthorizationHandler.cs new file mode 100644 index 000000000..b682e43ff --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/AuthorizationHandler.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Authorization +{ + public abstract class AuthorizationHandler : IAuthorizationHandler + where TRequirement : IAuthorizationRequirement + { + public void Handle(AuthorizationContext context) + { + foreach (var req in context.Requirements.OfType()) + { + Handle(context, req); + } + } + + public virtual Task HandleAsync(AuthorizationContext context) + { + Handle(context); + return Task.FromResult(0); + } + + // REVIEW: do we need an async hook too? + public abstract void Handle(AuthorizationContext context, TRequirement requirement); + } + + public abstract class AuthorizationHandler : IAuthorizationHandler + where TResource : class + where TRequirement : IAuthorizationRequirement + { + public virtual async Task HandleAsync(AuthorizationContext context) + { + var resource = context.Resource as TResource; + // REVIEW: should we allow null resources? + if (resource != null) + { + foreach (var req in context.Requirements.OfType()) + { + await HandleAsync(context, req, resource); + } + } + } + + public virtual Task HandleAsync(AuthorizationContext context, TRequirement requirement, TResource resource) + { + Handle(context, requirement, resource); + return Task.FromResult(0); + } + + public virtual void Handle(AuthorizationContext context) + { + var resource = context.Resource as TResource; + // REVIEW: should we allow null resources? + if (resource != null) + { + foreach (var req in context.Requirements.OfType()) + { + Handle(context, req, resource); + } + } + } + + public abstract void Handle(AuthorizationContext context, TRequirement requirement, TResource resource); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/AuthorizationOptions.cs b/src/Microsoft.AspNet.Authorization/AuthorizationOptions.cs new file mode 100644 index 000000000..17d879329 --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/AuthorizationOptions.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authorization +{ + public class AuthorizationOptions + { + // TODO: make this case insensitive + private IDictionary PolicyMap { get; } = new Dictionary(); + + public void AddPolicy([NotNull] string name, [NotNull] AuthorizationPolicy policy) + { + PolicyMap[name] = policy; + } + + public void AddPolicy([NotNull] string name, [NotNull] Action configurePolicy) + { + var policyBuilder = new AuthorizationPolicyBuilder(); + configurePolicy(policyBuilder); + PolicyMap[name] = policyBuilder.Build(); + } + + public AuthorizationPolicy GetPolicy([NotNull] string name) + { + return PolicyMap.ContainsKey(name) ? PolicyMap[name] : null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/AuthorizationPolicy.cs b/src/Microsoft.AspNet.Authorization/AuthorizationPolicy.cs new file mode 100644 index 000000000..379926450 --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/AuthorizationPolicy.cs @@ -0,0 +1,82 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authorization +{ + public class AuthorizationPolicy + { + public AuthorizationPolicy([NotNull] IEnumerable requirements, [NotNull] IEnumerable activeAuthenticationSchemes) + { + if (requirements.Count() == 0) + { + throw new InvalidOperationException(Resources.Exception_AuthorizationPolicyEmpty); + } + Requirements = new List(requirements).AsReadOnly(); + ActiveAuthenticationSchemes = new List(activeAuthenticationSchemes).AsReadOnly(); + } + + public IReadOnlyList Requirements { get; private set; } + public IReadOnlyList ActiveAuthenticationSchemes { get; private set; } + + public static AuthorizationPolicy Combine([NotNull] params AuthorizationPolicy[] policies) + { + return Combine((IEnumerable)policies); + } + + // TODO: Add unit tests + public static AuthorizationPolicy Combine([NotNull] IEnumerable policies) + { + var builder = new AuthorizationPolicyBuilder(); + foreach (var policy in policies) + { + builder.Combine(policy); + } + return builder.Build(); + } + + public static AuthorizationPolicy Combine([NotNull] AuthorizationOptions options, [NotNull] IEnumerable attributes) + { + var policyBuilder = new AuthorizationPolicyBuilder(); + var any = false; + foreach (var authorizeAttribute in attributes.OfType()) + { + any = true; + var requireAnyAuthenticated = true; + if (!string.IsNullOrWhiteSpace(authorizeAttribute.Policy)) + { + var policy = options.GetPolicy(authorizeAttribute.Policy); + if (policy == null) + { + throw new InvalidOperationException(Resources.FormatException_AuthorizationPolicyNotFound(authorizeAttribute.Policy)); + } + policyBuilder.Combine(policy); + requireAnyAuthenticated = false; + } + var rolesSplit = authorizeAttribute.Roles?.Split(','); + if (rolesSplit != null && rolesSplit.Any()) + { + policyBuilder.RequireRole(rolesSplit); + requireAnyAuthenticated = false; + } + var authTypesSplit = authorizeAttribute.ActiveAuthenticationSchemes?.Split(','); + if (authTypesSplit != null && authTypesSplit.Any()) + { + foreach (var authType in authTypesSplit) + { + policyBuilder.ActiveAuthenticationSchemes.Add(authType); + } + } + if (requireAnyAuthenticated) + { + policyBuilder.RequireAuthenticatedUser(); + } + } + return any ? policyBuilder.Build() : null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/AuthorizationPolicyBuilder.cs b/src/Microsoft.AspNet.Authorization/AuthorizationPolicyBuilder.cs new file mode 100644 index 000000000..f41fe7369 --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/AuthorizationPolicyBuilder.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authorization +{ + public class AuthorizationPolicyBuilder + { + public AuthorizationPolicyBuilder(params string[] activeAuthenticationSchemes) + { + AddAuthenticationSchemes(activeAuthenticationSchemes); + } + + public AuthorizationPolicyBuilder(AuthorizationPolicy policy) + { + Combine(policy); + } + + public IList Requirements { get; set; } = new List(); + public IList ActiveAuthenticationSchemes { get; set; } = new List(); + + public AuthorizationPolicyBuilder AddAuthenticationSchemes(params string[] activeAuthTypes) + { + foreach (var authType in activeAuthTypes) + { + ActiveAuthenticationSchemes.Add(authType); + } + return this; + } + + public AuthorizationPolicyBuilder AddRequirements(params IAuthorizationRequirement[] requirements) + { + foreach (var req in requirements) + { + Requirements.Add(req); + } + return this; + } + + public AuthorizationPolicyBuilder Combine([NotNull] AuthorizationPolicy policy) + { + AddAuthenticationSchemes(policy.ActiveAuthenticationSchemes.ToArray()); + AddRequirements(policy.Requirements.ToArray()); + return this; + } + + public AuthorizationPolicyBuilder RequireClaim([NotNull] string claimType, params string[] requiredValues) + { + return RequireClaim(claimType, (IEnumerable)requiredValues); + } + + public AuthorizationPolicyBuilder RequireClaim([NotNull] string claimType, IEnumerable requiredValues) + { + Requirements.Add(new ClaimsAuthorizationRequirement(claimType, requiredValues)); + return this; + } + + public AuthorizationPolicyBuilder RequireClaim([NotNull] string claimType) + { + Requirements.Add(new ClaimsAuthorizationRequirement(claimType, allowedValues: null)); + return this; + } + + public AuthorizationPolicyBuilder RequireRole([NotNull] params string[] roles) + { + return RequireRole((IEnumerable)roles); + } + + public AuthorizationPolicyBuilder RequireRole([NotNull] IEnumerable roles) + { + Requirements.Add(new RolesAuthorizationRequirement(roles)); + return this; + } + + public AuthorizationPolicyBuilder RequireUserName([NotNull] string userName) + { + Requirements.Add(new NameAuthorizationRequirement(userName)); + return this; + } + + public AuthorizationPolicyBuilder RequireAuthenticatedUser() + { + Requirements.Add(new DenyAnonymousAuthorizationRequirement()); + return this; + } + + public AuthorizationPolicy Build() + { + return new AuthorizationPolicy(Requirements, ActiveAuthenticationSchemes.Distinct()); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/AuthorizationServiceExtensions.cs b/src/Microsoft.AspNet.Authorization/AuthorizationServiceExtensions.cs new file mode 100644 index 000000000..14951fa07 --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/AuthorizationServiceExtensions.cs @@ -0,0 +1,112 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authorization +{ + public static class AuthorizationServiceExtensions + { + /// + /// Checks if a user meets a specific requirement for the specified resource + /// + /// + /// + /// + /// + public static Task AuthorizeAsync([NotNull] this IAuthorizationService service, ClaimsPrincipal user, object resource, [NotNull] IAuthorizationRequirement requirement) + { + return service.AuthorizeAsync(user, resource, new IAuthorizationRequirement[] { requirement }); + } + + /// + /// Checks if a user meets a specific authorization policy + /// + /// The authorization service. + /// The user to check the policy against. + /// The resource the policy should be checked with. + /// The policy to check against a specific context. + /// true when the user fulfills the policy, false otherwise. + public static Task AuthorizeAsync([NotNull] this IAuthorizationService service, ClaimsPrincipal user, object resource, [NotNull] AuthorizationPolicy policy) + { + return service.AuthorizeAsync(user, resource, policy.Requirements.ToArray()); + } + + /// + /// Checks if a user meets a specific authorization policy + /// + /// The authorization service. + /// The user to check the policy against. + /// The policy to check against a specific context. + /// true when the user fulfills the policy, false otherwise. + public static Task AuthorizeAsync([NotNull] this IAuthorizationService service, ClaimsPrincipal user, [NotNull] AuthorizationPolicy policy) + { + return service.AuthorizeAsync(user, resource: null, policy: policy); + } + + /// + /// Checks if a user meets a specific authorization policy + /// + /// The authorization service. + /// The user to check the policy against. + /// The name of the policy to check against a specific context. + /// true when the user fulfills the policy, false otherwise. + public static Task AuthorizeAsync([NotNull] this IAuthorizationService service, ClaimsPrincipal user, [NotNull] string policyName) + { + return service.AuthorizeAsync(user, resource: null, policyName: policyName); + } + + /// + /// Checks if a user meets a specific requirement for the specified resource + /// + /// + /// + /// + /// + public static bool Authorize([NotNull] this IAuthorizationService service, ClaimsPrincipal user, object resource, [NotNull] IAuthorizationRequirement requirement) + { + return service.Authorize(user, resource, new IAuthorizationRequirement[] { requirement }); + } + + /// + /// Checks if a user meets a specific authorization policy + /// + /// The authorization service. + /// The user to check the policy against. + /// The resource the policy should be checked with. + /// The policy to check against a specific context. + /// true when the user fulfills the policy, false otherwise. + public static bool Authorize([NotNull] this IAuthorizationService service, ClaimsPrincipal user, object resource, [NotNull] AuthorizationPolicy policy) + { + return service.Authorize(user, resource, policy.Requirements.ToArray()); + } + + /// + /// Checks if a user meets a specific authorization policy + /// + /// The authorization service. + /// The user to check the policy against. + /// The policy to check against a specific context. + /// true when the user fulfills the policy, false otherwise. + public static bool Authorize([NotNull] this IAuthorizationService service, ClaimsPrincipal user, [NotNull] AuthorizationPolicy policy) + { + return service.Authorize(user, resource: null, requirements: policy.Requirements.ToArray()); + } + + /// + /// Checks if a user meets a specific authorization policy + /// + /// The authorization service. + /// The user to check the policy against. + /// The name of the policy to check against a specific context. + /// true when the user fulfills the policy, false otherwise. + public static bool Authorize([NotNull] this IAuthorizationService service, ClaimsPrincipal user, [NotNull] string policyName) + { + return service.Authorize(user, resource: null, policyName: policyName); + } + + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/AuthorizeAttribute.cs b/src/Microsoft.AspNet.Authorization/AuthorizeAttribute.cs new file mode 100644 index 000000000..42d757d8e --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/AuthorizeAttribute.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Authorization +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class AuthorizeAttribute : Attribute + { + public AuthorizeAttribute() { } + + public AuthorizeAttribute(string policy) + { + Policy = policy; + } + + public string Policy { get; set; } + + // REVIEW: can we get rid of the , deliminated in Roles/AuthTypes + public string Roles { get; set; } + + public string ActiveAuthenticationSchemes { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authorization/ClaimsAuthorizationRequirement.cs b/src/Microsoft.AspNet.Authorization/ClaimsAuthorizationRequirement.cs new file mode 100644 index 000000000..c5af9449b --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/ClaimsAuthorizationRequirement.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authorization +{ + // Must contain a claim with the specified name, and at least one of the required values + // If AllowedValues is null or empty, that means any claim is valid + public class ClaimsAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement + { + public ClaimsAuthorizationRequirement([NotNull] string claimType, IEnumerable allowedValues) + { + ClaimType = claimType; + AllowedValues = allowedValues; + } + + public string ClaimType { get; } + public IEnumerable AllowedValues { get; } + + public override void Handle(AuthorizationContext context, ClaimsAuthorizationRequirement requirement) + { + if (context.User != null) + { + var found = false; + if (requirement.AllowedValues == null || !requirement.AllowedValues.Any()) + { + found = context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase)); + } + else + { + found = context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase) + && requirement.AllowedValues.Contains(c.Value, StringComparer.Ordinal)); + } + if (found) + { + context.Succeed(requirement); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Authorization/DefaultAuthorizationService.cs b/src/Microsoft.AspNet.Authorization/DefaultAuthorizationService.cs new file mode 100644 index 000000000..e66721a5c --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/DefaultAuthorizationService.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Authorization +{ + public class DefaultAuthorizationService : IAuthorizationService + { + private readonly IList _handlers; + private readonly AuthorizationOptions _options; + + public DefaultAuthorizationService(IOptions options, IEnumerable handlers) + { + _handlers = handlers.ToArray(); + _options = options.Options; + } + + public bool Authorize(ClaimsPrincipal user, object resource, string policyName) + { + var policy = _options.GetPolicy(policyName); + return (policy == null) + ? false + : this.Authorize(user, resource, policy); + } + + public bool Authorize(ClaimsPrincipal user, object resource, [NotNull] IEnumerable requirements) + { + var authContext = new AuthorizationContext(requirements, user, resource); + foreach (var handler in _handlers) + { + handler.Handle(authContext); + } + return authContext.HasSucceeded; + } + + public async Task AuthorizeAsync(ClaimsPrincipal user, object resource, [NotNull] IEnumerable requirements) + { + var authContext = new AuthorizationContext(requirements, user, resource); + foreach (var handler in _handlers) + { + await handler.HandleAsync(authContext); + } + return authContext.HasSucceeded; + } + + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) + { + var policy = _options.GetPolicy(policyName); + return (policy == null) + ? Task.FromResult(false) + : this.AuthorizeAsync(user, resource, policy); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/DenyAnonymousAuthorizationRequirement.cs b/src/Microsoft.AspNet.Authorization/DenyAnonymousAuthorizationRequirement.cs new file mode 100644 index 000000000..92f48c1fe --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/DenyAnonymousAuthorizationRequirement.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; + +namespace Microsoft.AspNet.Authorization +{ + public class DenyAnonymousAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement + { + public override void Handle(AuthorizationContext context, DenyAnonymousAuthorizationRequirement requirement) + { + var user = context.User; + var userIsAnonymous = + user?.Identity == null || + !user.Identities.Any(i => i.IsAuthenticated); + if (!userIsAnonymous) + { + context.Succeed(requirement); + } + } + } +} diff --git a/src/Microsoft.AspNet.Authorization/IAuthorizationHandler.cs b/src/Microsoft.AspNet.Authorization/IAuthorizationHandler.cs new file mode 100644 index 000000000..b4366a6f5 --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/IAuthorizationHandler.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Authorization +{ + public interface IAuthorizationHandler + { + Task HandleAsync(AuthorizationContext context); + void Handle(AuthorizationContext context); + } +} diff --git a/src/Microsoft.AspNet.Authorization/IAuthorizationRequirement.cs b/src/Microsoft.AspNet.Authorization/IAuthorizationRequirement.cs new file mode 100644 index 000000000..19857b618 --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/IAuthorizationRequirement.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Authorization +{ + public interface IAuthorizationRequirement + { + } +} diff --git a/src/Microsoft.AspNet.Authorization/IAuthorizationService.cs b/src/Microsoft.AspNet.Authorization/IAuthorizationService.cs new file mode 100644 index 000000000..876fd7601 --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/IAuthorizationService.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authorization +{ + /// + /// Checks policy based permissions for a user + /// + public interface IAuthorizationService + { + /// + /// Checks if a user meets a specific set of requirements for the specified resource + /// + /// + /// + /// + /// + Task AuthorizeAsync(ClaimsPrincipal user, object resource, [NotNull] IEnumerable requirements); + + /// + /// Checks if a user meets a specific set of requirements for the specified resource + /// + /// + /// + /// + /// + bool Authorize(ClaimsPrincipal user, object resource, [NotNull] IEnumerable requirements); + + /// + /// Checks if a user meets a specific authorization policy + /// + /// The user to check the policy against. + /// The resource the policy should be checked with. + /// The name of the policy to check against a specific context. + /// true when the user fulfills the policy, false otherwise. + Task AuthorizeAsync(ClaimsPrincipal user, object resource, [NotNull] string policyName); + + /// + /// Checks if a user meets a specific authorization policy + /// + /// The user to check the policy against. + /// The resource the policy should be checked with. + /// The name of the policy to check against a specific context. + /// true when the user fulfills the policy, false otherwise. + bool Authorize(ClaimsPrincipal user, object resource, [NotNull] string policyName); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/Microsoft.AspNet.Authorization.xproj b/src/Microsoft.AspNet.Authorization/Microsoft.AspNet.Authorization.xproj new file mode 100644 index 000000000..be8ba0e0e --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/Microsoft.AspNet.Authorization.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 6ab3e514-5894-4131-9399-dc7d5284addb + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/NameAuthorizationRequirement.cs b/src/Microsoft.AspNet.Authorization/NameAuthorizationRequirement.cs new file mode 100644 index 000000000..7d64f1b71 --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/NameAuthorizationRequirement.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authorization +{ + /// + /// Requirement that ensures a specific Name + /// + public class NameAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement + { + public NameAuthorizationRequirement([NotNull] string requiredName) + { + RequiredName = requiredName; + } + + public string RequiredName { get; } + + public override void Handle(AuthorizationContext context, NameAuthorizationRequirement requirement) + { + if (context.User != null) + { + // REVIEW: Do we need to do normalization? casing/loc? + if (context.User.Identities.Any(i => string.Equals(i.Name, requirement.RequiredName))) + { + context.Succeed(requirement); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Authorization/OperationAuthorizationRequirement.cs b/src/Microsoft.AspNet.Authorization/OperationAuthorizationRequirement.cs new file mode 100644 index 000000000..13732d019 --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/OperationAuthorizationRequirement.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Authorization +{ + public class OperationAuthorizationRequirement : IAuthorizationRequirement + { + public string Name { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Authorization/PassThroughAuthorizationHandler.cs b/src/Microsoft.AspNet.Authorization/PassThroughAuthorizationHandler.cs new file mode 100644 index 000000000..93107cc6b --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/PassThroughAuthorizationHandler.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Authorization +{ + public class PassThroughAuthorizationHandler : IAuthorizationHandler + { + public async Task HandleAsync(AuthorizationContext context) + { + foreach (var handler in context.Requirements.OfType()) + { + await handler.HandleAsync(context); + } + } + + public void Handle(AuthorizationContext context) + { + foreach (var handler in context.Requirements.OfType()) + { + handler.Handle(context); + } + } + } +} diff --git a/src/Microsoft.AspNet.Authorization/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Authorization/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..025a94598 --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +[assembly: AssemblyMetadata("Serviceable", "True")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Authorization/Properties/Resources.Designer.cs new file mode 100644 index 000000000..29d82385f --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/Properties/Resources.Designer.cs @@ -0,0 +1,78 @@ +// +namespace Microsoft.AspNet.Authorization +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Authorization.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The AuthorizationPolicy named: '{0}' was not found. + /// + internal static string Exception_AuthorizationPolicyNotFound + { + get { return GetString("Exception_AuthorizationPolicyNotFound"); } + } + + /// + /// The AuthorizationPolicy named: '{0}' was not found. + /// + internal static string FormatException_AuthorizationPolicyNotFound(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Exception_AuthorizationPolicyNotFound"), p0); + } + + /// + /// AuthorizationPolicy must have at least one requirement. + /// + internal static string Exception_AuthorizationPolicyEmpty + { + get { return GetString("Exception_AuthorizationPolicyEmpty"); } + } + + /// + /// AuthorizationPolicy must have at least one requirement. + /// + internal static string FormatException_AuthorizationPolicyEmpty(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Exception_AuthorizationPolicyEmpty"), p0); + } + + /// + /// At least one role must be specified. + /// + internal static string Exception_RoleRequirementEmpty + { + get { return GetString("Exception_RoleRequirementEmpty"); } + } + + /// + /// At least one role must be specified. + /// + internal static string FormatException_RoleRequirementEmpty() + { + return GetString("Exception_RoleRequirementEmpty"); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNet.Authorization/Resources.resx b/src/Microsoft.AspNet.Authorization/Resources.resx new file mode 100644 index 000000000..a36e55d6b --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/Resources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + AuthorizationPolicy must have at least one requirement. + + + The AuthorizationPolicy named: '{0}' was not found. + + + At least one role must be specified. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/RolesAuthorizationRequirement.cs b/src/Microsoft.AspNet.Authorization/RolesAuthorizationRequirement.cs new file mode 100644 index 000000000..f3336237e --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/RolesAuthorizationRequirement.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authorization +{ + // Must belong to with one of specified roles + // If AllowedRoles is null or empty, that means any role is valid + public class RolesAuthorizationRequirement : AuthorizationHandler, IAuthorizationRequirement + { + public RolesAuthorizationRequirement([NotNull] IEnumerable allowedRoles) + { + if (allowedRoles.Count() == 0) + { + throw new InvalidOperationException(Resources.Exception_RoleRequirementEmpty); + } + AllowedRoles = allowedRoles; + } + + public IEnumerable AllowedRoles { get; } + + public override void Handle(AuthorizationContext context, RolesAuthorizationRequirement requirement) + { + if (context.User != null) + { + bool found = false; + if (requirement.AllowedRoles == null || !requirement.AllowedRoles.Any()) + { + // Review: What do we want to do here? No roles requested is auto success? + } + else + { + found = requirement.AllowedRoles.Any(r => context.User.IsInRole(r)); + } + if (found) + { + context.Succeed(requirement); + } + } + } + + } +} diff --git a/src/Microsoft.AspNet.Authorization/ServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Authorization/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..d77512a64 --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/ServiceCollectionExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Authorization; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection ConfigureAuthorization([NotNull] this IServiceCollection services, [NotNull] Action configure) + { + return services.Configure(configure); + } + + public static IServiceCollection AddAuthorization([NotNull] this IServiceCollection services) + { + return services.AddAuthorization(configureOptions: null); + } + + public static IServiceCollection AddAuthorization([NotNull] this IServiceCollection services, Action configureOptions) + { + services.AddOptions(); + services.TryAdd(ServiceDescriptor.Transient()); + services.AddTransient(); + return services; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/project.json b/src/Microsoft.AspNet.Authorization/project.json new file mode 100644 index 000000000..a0b9beb5d --- /dev/null +++ b/src/Microsoft.AspNet.Authorization/project.json @@ -0,0 +1,14 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET 5 authorization classes.", + "dependencies": { + "Microsoft.AspNet.Http.Features": "1.0.0-*", + "Microsoft.Framework.Logging.Abstractions": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" }, + "Microsoft.Framework.OptionsModel": "1.0.0-*" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + } +} diff --git a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationExtensions.cs deleted file mode 100644 index 5df523161..000000000 --- a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Security.Cookies; -using Microsoft.AspNet.Security.DataProtection; -using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.Logging; - -namespace Microsoft.AspNet.Builder -{ - /// - /// Extension methods provided by the cookies authentication middleware - /// - public static class CookieAuthenticationExtensions - { - /// - /// Adds a cookie-based authentication middleware to your web application pipeline. - /// - /// The IAppBuilder passed to your configuration method - /// An options class that controls the middleware behavior - /// The original app parameter - public static IBuilder UseCookieAuthentication([NotNull] this IBuilder app, [NotNull] CookieAuthenticationOptions options) - { - // TODO: Use UseMiddleware to inject dependencies once it can discover Invoke from a base class. - var dataProtectionProvider = app.ApplicationServices.GetService(); - var loggerFactory = app.ApplicationServices.GetService(); - return app.Use(next => new CookieAuthenticationMiddleware(next, dataProtectionProvider, loggerFactory, options).Invoke); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs deleted file mode 100644 index 03ad97927..000000000 --- a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs +++ /dev/null @@ -1,247 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Threading.Tasks; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Security.Infrastructure; -using Microsoft.Framework.Logging; - -namespace Microsoft.AspNet.Security.Cookies -{ - internal class CookieAuthenticationHandler : AuthenticationHandler - { - private const string HeaderNameCacheControl = "Cache-Control"; - private const string HeaderNamePragma = "Pragma"; - private const string HeaderNameExpires = "Expires"; - private const string HeaderValueNoCache = "no-cache"; - private const string HeaderValueMinusOne = "-1"; - - private readonly ILogger _logger; - - private bool _shouldRenew; - private DateTimeOffset _renewIssuedUtc; - private DateTimeOffset _renewExpiresUtc; - - public CookieAuthenticationHandler([NotNull] ILogger logger) - { - _logger = logger; - } - - protected override AuthenticationTicket AuthenticateCore() - { - return AuthenticateCoreAsync().Result; - } - - protected override async Task AuthenticateCoreAsync() - { - IReadableStringCollection cookies = Request.Cookies; - string cookie = cookies[Options.CookieName]; - if (string.IsNullOrWhiteSpace(cookie)) - { - return null; - } - - AuthenticationTicket ticket = Options.TicketDataFormat.Unprotect(cookie); - - if (ticket == null) - { - _logger.WriteWarning(@"Unprotect ticket failed"); - return null; - } - - DateTimeOffset currentUtc = Options.SystemClock.UtcNow; - DateTimeOffset? issuedUtc = ticket.Properties.IssuedUtc; - DateTimeOffset? expiresUtc = ticket.Properties.ExpiresUtc; - - if (expiresUtc != null && expiresUtc.Value < currentUtc) - { - return null; - } - - if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration) - { - TimeSpan timeElapsed = currentUtc.Subtract(issuedUtc.Value); - TimeSpan timeRemaining = expiresUtc.Value.Subtract(currentUtc); - - if (timeRemaining < timeElapsed) - { - _shouldRenew = true; - _renewIssuedUtc = currentUtc; - TimeSpan timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); - _renewExpiresUtc = currentUtc.Add(timeSpan); - } - } - - var context = new CookieValidateIdentityContext(Context, ticket, Options); - - await Options.Notifications.ValidateIdentity(context); - - return new AuthenticationTicket(context.Identity, context.Properties); - } - - protected override void ApplyResponseGrant() - { - ApplyResponseGrantAsync().Wait(); - } - - protected override async Task ApplyResponseGrantAsync() - { - var signin = SignInIdentityContext; - bool shouldSignin = signin != null; - var signout = SignOutContext; - bool shouldSignout = signout != null; - - if (shouldSignin || shouldSignout || _shouldRenew) - { - var cookieOptions = new CookieOptions - { - Domain = Options.CookieDomain, - HttpOnly = Options.CookieHttpOnly, - Path = Options.CookiePath ?? "/", - }; - if (Options.CookieSecure == CookieSecureOption.SameAsRequest) - { - cookieOptions.Secure = Request.IsSecure; - } - else - { - cookieOptions.Secure = Options.CookieSecure == CookieSecureOption.Always; - } - - if (shouldSignin) - { - var context = new CookieResponseSignInContext( - Context, - Options, - Options.AuthenticationType, - signin.Identity, - signin.Properties, - cookieOptions); - - DateTimeOffset issuedUtc = Options.SystemClock.UtcNow; - DateTimeOffset expiresUtc = issuedUtc.Add(Options.ExpireTimeSpan); - - context.Properties.IssuedUtc = issuedUtc; - context.Properties.ExpiresUtc = expiresUtc; - - Options.Notifications.ResponseSignIn(context); - - if (context.Properties.IsPersistent) - { - cookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; - } - - var model = new AuthenticationTicket(context.Identity, context.Properties); - string cookieValue = Options.TicketDataFormat.Protect(model); - - Response.Cookies.Append( - Options.CookieName, - cookieValue, - cookieOptions); - } - else if (shouldSignout) - { - var context = new CookieResponseSignOutContext( - Context, - Options, - cookieOptions); - - Options.Notifications.ResponseSignOut(context); - - Response.Cookies.Delete( - Options.CookieName, - cookieOptions); - } - else if (_shouldRenew) - { - AuthenticationTicket model = await AuthenticateAsync(); - - model.Properties.IssuedUtc = _renewIssuedUtc; - model.Properties.ExpiresUtc = _renewExpiresUtc; - - string cookieValue = Options.TicketDataFormat.Protect(model); - - if (model.Properties.IsPersistent) - { - cookieOptions.Expires = _renewExpiresUtc.ToUniversalTime().DateTime; - } - - Response.Cookies.Append( - Options.CookieName, - cookieValue, - cookieOptions); - } - - Response.Headers.Set( - HeaderNameCacheControl, - HeaderValueNoCache); - - Response.Headers.Set( - HeaderNamePragma, - HeaderValueNoCache); - - Response.Headers.Set( - HeaderNameExpires, - HeaderValueMinusOne); - - bool shouldLoginRedirect = shouldSignin && Options.LoginPath.HasValue && Request.Path == Options.LoginPath; - bool shouldLogoutRedirect = shouldSignout && Options.LogoutPath.HasValue && Request.Path == Options.LogoutPath; - - if ((shouldLoginRedirect || shouldLogoutRedirect) && Response.StatusCode == 200) - { - IReadableStringCollection query = Request.Query; - string redirectUri = query.Get(Options.ReturnUrlParameter); - if (!string.IsNullOrWhiteSpace(redirectUri) - && IsHostRelative(redirectUri)) - { - var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri); - Options.Notifications.ApplyRedirect(redirectContext); - } - } - } - } - - private static bool IsHostRelative(string path) - { - if (string.IsNullOrEmpty(path)) - { - return false; - } - if (path.Length == 1) - { - return path[0] == '/'; - } - return path[0] == '/' && path[1] != '/' && path[1] != '\\'; - } - - protected override void ApplyResponseChallenge() - { - if (Response.StatusCode != 401 || !Options.LoginPath.HasValue ) - { - return; - } - - // Active middleware should redirect on 401 even if there wasn't an explicit challenge. - if (ChallengeContext == null && Options.AuthenticationMode == AuthenticationMode.Passive) - { - return; - } - - string currentUri = - Request.PathBase + - Request.Path + - Request.QueryString; - - string loginUri = - Request.Scheme + - "://" + - Request.Host + - Request.PathBase + - Options.LoginPath + - new QueryString(Options.ReturnUrlParameter, currentUri); - - var redirectContext = new CookieApplyRedirectContext(Context, Options, loginUri); - Options.Notifications.ApplyRedirect(redirectContext); - } - } -} diff --git a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationMiddleware.cs deleted file mode 100644 index 9fde9e7d8..000000000 --- a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationMiddleware.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNet.Builder; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Security.DataHandler; -using Microsoft.AspNet.Security.DataProtection; -using Microsoft.AspNet.Security.Infrastructure; -using Microsoft.Framework.Logging; - -namespace Microsoft.AspNet.Security.Cookies -{ - internal class CookieAuthenticationMiddleware : AuthenticationMiddleware - { - private readonly ILogger _logger; - - public CookieAuthenticationMiddleware(RequestDelegate next, IDataProtectionProvider dataProtectionProvider, ILoggerFactory loggerFactory, CookieAuthenticationOptions options) - : base(next, options) - { - if (Options.Notifications == null) - { - Options.Notifications = new CookieAuthenticationNotifications(); - } - if (String.IsNullOrEmpty(Options.CookieName)) - { - Options.CookieName = CookieAuthenticationDefaults.CookiePrefix + Options.AuthenticationType; - } - if (options.TicketDataFormat == null) - { - IDataProtector dataProtector = DataProtectionHelpers.CreateDataProtector(dataProtectionProvider, - typeof(CookieAuthenticationMiddleware).FullName, options.AuthenticationType, "v1"); - options.TicketDataFormat = new TicketDataFormat(dataProtector); - } - - _logger = loggerFactory.Create(typeof(CookieAuthenticationMiddleware).FullName); - } - - protected override AuthenticationHandler CreateHandler() - { - return new CookieAuthenticationHandler(_logger); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Cookies/Microsoft.AspNet.Security.Cookies.kproj b/src/Microsoft.AspNet.Security.Cookies/Microsoft.AspNet.Security.Cookies.kproj deleted file mode 100644 index b5ae96ed0..000000000 --- a/src/Microsoft.AspNet.Security.Cookies/Microsoft.AspNet.Security.Cookies.kproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - 12.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 15f1211b-b695-4a1c-b730-1ac58fc91090 - Library - - - - - - - 2.0 - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Cookies/NotNullAttribute.cs b/src/Microsoft.AspNet.Security.Cookies/NotNullAttribute.cs deleted file mode 100644 index b3b1edcbd..000000000 --- a/src/Microsoft.AspNet.Security.Cookies/NotNullAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Microsoft.AspNet.Security.Cookies -{ - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] - internal sealed class NotNullAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieValidateIdentityContext.cs b/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieValidateIdentityContext.cs deleted file mode 100644 index 3996a9585..000000000 --- a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieValidateIdentityContext.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Security.Claims; -using System.Security.Principal; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Http.Security; -using Microsoft.AspNet.HttpFeature.Security; -using Microsoft.AspNet.Security.Infrastructure; -using Microsoft.AspNet.Security.Notifications; - -namespace Microsoft.AspNet.Security.Cookies -{ - /// - /// Context object passed to the ICookieAuthenticationProvider method ValidateIdentity. - /// - public class CookieValidateIdentityContext : BaseContext - { - /// - /// Creates a new instance of the context object. - /// - /// - /// Contains the initial values for identity and extra data - /// - public CookieValidateIdentityContext([NotNull] HttpContext context, [NotNull] AuthenticationTicket ticket, [NotNull] CookieAuthenticationOptions options) - : base(context, options) - { - Identity = ticket.Identity; - Properties = ticket.Properties; - } - - /// - /// Contains the claims identity arriving with the request. May be altered to change the - /// details of the authenticated user. - /// - public ClaimsIdentity Identity { get; private set; } - - /// - /// Contains the extra meta-data arriving with the request ticket. May be altered. - /// - public AuthenticationProperties Properties { get; private set; } - - /// - /// Called to replace the claims identity. The supplied identity will replace the value of the - /// Identity property, which determines the identity of the authenticated request. - /// - /// The identity used as the replacement - public void ReplaceIdentity(IIdentity identity) - { - Identity = new ClaimsIdentity(identity); - } - - /// - /// Called to reject the incoming identity. This may be done if the application has determined the - /// account is no longer active, and the request should be treated as if it was anonymous. - /// - public void RejectIdentity() - { - Identity = null; - } - } -} diff --git a/src/Microsoft.AspNet.Security.Cookies/project.json b/src/Microsoft.AspNet.Security.Cookies/project.json deleted file mode 100644 index 98cfc52a3..000000000 --- a/src/Microsoft.AspNet.Security.Cookies/project.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "version": "0.1-alpha-*", - "dependencies": { - "Newtonsoft.Json": "5.0.8", - "Microsoft.AspNet.Security": "0.1-alpha-*", - "Microsoft.AspNet.PipelineCore": "0.1-alpha-*", - "Microsoft.AspNet.Http": "0.1-alpha-*", - "Microsoft.AspNet.FeatureModel": "0.1-alpha-*", - "Microsoft.AspNet.HttpFeature": "0.1-alpha-*", - "Microsoft.AspNet.Security.DataProtection": "0.1-alpha-*", - "Microsoft.Framework.DependencyInjection": "0.1-alpha-*", - "Microsoft.Framework.Logging": "0.1-alpha-*" - }, - "configurations": { - "net45": {}, - "k10": { - "dependencies": { - "System.Collections": "4.0.0.0", - "System.Console": "4.0.0.0", - "System.ComponentModel": "4.0.0.0", - "System.Diagnostics.Debug": "4.0.10.0", - "System.Diagnostics.Tools": "4.0.0.0", - "System.Globalization": "4.0.10.0", - "System.IO": "4.0.0.0", - "System.IO.Compression": "4.0.0.0", - "System.Linq": "4.0.0.0", - "System.Reflection": "4.0.10.0", - "System.Resources.ResourceManager": "4.0.0.0", - "System.Runtime": "4.0.20.0", - "System.Runtime.Extensions": "4.0.10.0", - "System.Runtime.InteropServices": "4.0.20.0", - "System.Security.Claims": "0.1-alpha-*", - "System.Security.Principal" : "4.0.0.0", - "System.Threading": "4.0.0.0", - "System.Threading.Tasks": "4.0.10.0" - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/AppBuilderSecurityExtensions.cs b/src/Microsoft.AspNet.Security/AppBuilderSecurityExtensions.cs deleted file mode 100644 index 077cdbd53..000000000 --- a/src/Microsoft.AspNet.Security/AppBuilderSecurityExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. -/* TODO: -using System; -using Owin; - -namespace Microsoft.AspNet.Security -{ - /// - /// Provides extensions methods for app.Property values that are only needed by implementations of authentication middleware. - /// - public static class AppBuilderSecurityExtensions - { - /// - /// Returns the previously set AuthenticationType that external sign in middleware should use when the - /// browser navigates back to their return url. - /// - /// App builder passed to the application startup code - /// - public static string GetDefaultSignInAsAuthenticationType([NotNull] this IAppBuilder app) - { - object value; - if (app.Properties.TryGetValue(Constants.DefaultSignInAsAuthenticationType, out value)) - { - var authenticationType = value as string; - if (!string.IsNullOrEmpty(authenticationType)) - { - return authenticationType; - } - } - throw new InvalidOperationException(Resources.Exception_MissingDefaultSignInAsAuthenticationType); - } - - /// - /// Called by middleware to change the name of the AuthenticationType that external middleware should use - /// when the browser navigates back to their return url. - /// - /// App builder passed to the application startup code - /// AuthenticationType that external middleware should sign in as. - public static void SetDefaultSignInAsAuthenticationType([NotNull] this IAppBuilder app, [NotNull] string authenticationType) - { - app.Properties[Constants.DefaultSignInAsAuthenticationType] = authenticationType; - } - } -} -*/ \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/AuthenticationMode.cs b/src/Microsoft.AspNet.Security/AuthenticationMode.cs deleted file mode 100644 index ba7aff904..000000000 --- a/src/Microsoft.AspNet.Security/AuthenticationMode.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Security -{ - /// - /// Controls the behavior of authentication middleware - /// - public enum AuthenticationMode - { - /// - /// In Active mode the authentication middleware will alter the user identity as the request arrives, and - /// will also alter a plain 401 as the response leaves. - /// - Active, - - /// - /// In Passive mode the authentication middleware will only provide user identity when asked, and will only - /// alter 401 responses where the authentication type named in the extra challenge data. - /// - Passive - } -} diff --git a/src/Microsoft.AspNet.Security/AuthenticationOptions.cs b/src/Microsoft.AspNet.Security/AuthenticationOptions.cs deleted file mode 100644 index 719132212..000000000 --- a/src/Microsoft.AspNet.Security/AuthenticationOptions.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using Microsoft.AspNet.Http.Security; -using Microsoft.AspNet.HttpFeature.Security; -using Microsoft.AspNet.PipelineCore.Security; - -namespace Microsoft.AspNet.Security -{ - /// - /// Base Options for all authentication middleware - /// - public abstract class AuthenticationOptions - { - private string _authenticationType; - - /// - /// Initialize properties of AuthenticationOptions base class - /// - /// Assigned to the AuthenticationType property - protected AuthenticationOptions(string authenticationType) - { - Description = new AuthenticationDescription(); - AuthenticationType = authenticationType; - AuthenticationMode = AuthenticationMode.Active; - } - - /// - /// The AuthenticationType in the options corresponds to the IIdentity AuthenticationType property. A different - /// value may be assigned in order to use the same authentication middleware type more than once in a pipeline. - /// - public string AuthenticationType - { - get { return _authenticationType; } - set - { - _authenticationType = value; - Description.AuthenticationType = value; - } - } - - /// - /// If Active the authentication middleware alter the request user coming in and - /// alter 401 Unauthorized responses going out. If Passive the authentication middleware will only provide - /// identity and alter responses when explicitly indicated by the AuthenticationType. - /// - public AuthenticationMode AuthenticationMode { get; set; } - - /// - /// Additional information about the authentication type which is made available to the application. - /// - public AuthenticationDescription Description { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Security/AuthenticationTicket.cs b/src/Microsoft.AspNet.Security/AuthenticationTicket.cs deleted file mode 100644 index 5adb20b7e..000000000 --- a/src/Microsoft.AspNet.Security/AuthenticationTicket.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Security.Claims; -using Microsoft.AspNet.Http.Security; -using Microsoft.AspNet.HttpFeature.Security; -using Microsoft.AspNet.PipelineCore.Security; -using Microsoft.AspNet.Security.Infrastructure; - -namespace Microsoft.AspNet.Security -{ - /// - /// Contains user identity information as well as additional authentication state. - /// - public class AuthenticationTicket - { - /// - /// Initializes a new instance of the class - /// - /// - /// - public AuthenticationTicket(ClaimsIdentity identity, AuthenticationProperties properties) - { - Identity = identity; - Properties = properties ?? new AuthenticationProperties(); - } - - /// - /// Gets the authenticated user identity. - /// - public ClaimsIdentity Identity { get; private set; } - - /// - /// Additional state values for the authentication session. - /// - public AuthenticationProperties Properties { get; private set; } - } -} diff --git a/src/Microsoft.AspNet.Security/AuthorizationPolicy.cs b/src/Microsoft.AspNet.Security/AuthorizationPolicy.cs deleted file mode 100644 index 157b71047..000000000 --- a/src/Microsoft.AspNet.Security/AuthorizationPolicy.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. - -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Security -{ - /// - /// This class provides a base implementation for - /// - public abstract class AuthorizationPolicy : IAuthorizationPolicy - { - public int Order { get; set; } - - public virtual Task ApplyingAsync(AuthorizationPolicyContext context) - { - return Task.FromResult(0); - } - - public virtual Task ApplyAsync(AuthorizationPolicyContext context) - { - return Task.FromResult(0); - } - - public virtual Task AppliedAsync(AuthorizationPolicyContext context) - { - return Task.FromResult(0); - } - } -} diff --git a/src/Microsoft.AspNet.Security/AuthorizationPolicyContext.cs b/src/Microsoft.AspNet.Security/AuthorizationPolicyContext.cs deleted file mode 100644 index 4055fddbd..000000000 --- a/src/Microsoft.AspNet.Security/AuthorizationPolicyContext.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. - -using System.Collections.Generic; -using System.Security.Claims; -using System.Linq; - -namespace Microsoft.AspNet.Security -{ - /// - /// Contains authorization information used by . - /// - public class AuthorizationPolicyContext - { - public AuthorizationPolicyContext(IEnumerable claims, ClaimsPrincipal user, object resource ) - { - Claims = (claims ?? Enumerable.Empty()).ToList(); - User = user; - Resource = resource; - - // user claims are copied to a new and mutable list - UserClaims = user != null - ? user.Claims.ToList() - : new List(); - } - - /// - /// The list of claims the is checking. - /// - public IList Claims { get; private set; } - - /// - /// The user to check the claims against. - /// - public ClaimsPrincipal User { get; private set; } - - /// - /// The claims of the user. - /// - /// - /// This list can be modified by policies for retries. - /// - public IList UserClaims { get; private set; } - - /// - /// An optional resource associated to the check. - /// - public object Resource { get; private set; } - - /// - /// Gets or set whether the permission will be granted to the user. - /// - public bool Authorized { get; set; } - - /// - /// When set to true, the authorization check will be processed again. - /// - public bool Retry { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Security/AuthorizationServiceExtensions.cs b/src/Microsoft.AspNet.Security/AuthorizationServiceExtensions.cs deleted file mode 100644 index 158314a15..000000000 --- a/src/Microsoft.AspNet.Security/AuthorizationServiceExtensions.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Security -{ - public static class AuthorizationServiceExtensions - { - /// - /// Checks if a user has specific claims. - /// - /// The claim to check against a specific user. - /// The user to check claims against. - /// true when the user fulfills one of the claims, false otherwise. - public static Task AuthorizeAsync(this IAuthorizationService service, Claim claim, ClaimsPrincipal user) - { - return service.AuthorizeAsync(new Claim[] { claim }, user); - } - - /// - /// Checks if a user has specific claims. - /// - /// The claim to check against a specific user. - /// The user to check claims against. - /// true when the user fulfills one of the claims, false otherwise. - public static bool Authorize(this IAuthorizationService service, Claim claim, ClaimsPrincipal user) - { - return service.Authorize(new Claim[] { claim }, user); - } - - /// - /// Checks if a user has specific claims for a specific context obj. - /// - /// The claim to check against a specific user. - /// The user to check claims against. - /// The resource the claims should be check with. - /// true when the user fulfills one of the claims, false otherwise. - public static Task AuthorizeAsync(this IAuthorizationService service, Claim claim, ClaimsPrincipal user, object resource) - { - return service.AuthorizeAsync(new Claim[] { claim }, user, resource); - } - - /// - /// Checks if a user has specific claims for a specific context obj. - /// - /// The claimsto check against a specific user. - /// The user to check claims against. - /// The resource the claims should be check with. - /// true when the user fulfills one of the claims, false otherwise. - public static bool Authorize(this IAuthorizationService service, Claim claim, ClaimsPrincipal user, object resource) - { - return service.Authorize(new Claim[] { claim }, user, resource); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Constants.cs b/src/Microsoft.AspNet.Security/Constants.cs deleted file mode 100644 index 50d4dfe0a..000000000 --- a/src/Microsoft.AspNet.Security/Constants.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Security -{ - /// - /// String constants used only by the Security assembly - /// - internal static class Constants - { - /// - /// Used by middleware extension methods to coordinate the default value Options property SignInAsAuthenticationType - /// - public const string DefaultSignInAsAuthenticationType = "Microsoft.AspNet.Security.Constants.DefaultSignInAsAuthenticationType"; - } -} diff --git a/src/Microsoft.AspNet.Security/DataHandler/Encoder/ITextEncoder.cs b/src/Microsoft.AspNet.Security/DataHandler/Encoder/ITextEncoder.cs deleted file mode 100644 index ce61e8fb9..000000000 --- a/src/Microsoft.AspNet.Security/DataHandler/Encoder/ITextEncoder.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Security.DataHandler.Encoder -{ - public interface ITextEncoder - { - string Encode(byte[] data); - byte[] Decode(string text); - } -} diff --git a/src/Microsoft.AspNet.Security/DataHandler/ISecureDataFormat.cs b/src/Microsoft.AspNet.Security/DataHandler/ISecureDataFormat.cs deleted file mode 100644 index f239150ba..000000000 --- a/src/Microsoft.AspNet.Security/DataHandler/ISecureDataFormat.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Security -{ - public interface ISecureDataFormat - { - string Protect(TData data); - TData Unprotect(string protectedText); - } -} diff --git a/src/Microsoft.AspNet.Security/DataHandler/PropertiesDataFormat.cs b/src/Microsoft.AspNet.Security/DataHandler/PropertiesDataFormat.cs deleted file mode 100644 index 4c4122350..000000000 --- a/src/Microsoft.AspNet.Security/DataHandler/PropertiesDataFormat.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.AspNet.Http.Security; -using Microsoft.AspNet.Security.DataHandler.Encoder; -using Microsoft.AspNet.Security.DataHandler.Serializer; -using Microsoft.AspNet.Security.DataProtection; - -namespace Microsoft.AspNet.Security.DataHandler -{ - public class PropertiesDataFormat : SecureDataFormat - { - public PropertiesDataFormat(IDataProtector protector) - : base(DataSerializers.Properties, protector, TextEncodings.Base64Url) - { - } - } -} diff --git a/src/Microsoft.AspNet.Security/DataHandler/Serializer/IDataSerializer.cs b/src/Microsoft.AspNet.Security/DataHandler/Serializer/IDataSerializer.cs deleted file mode 100644 index 8b9b77ae0..000000000 --- a/src/Microsoft.AspNet.Security/DataHandler/Serializer/IDataSerializer.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Security.DataHandler.Serializer -{ - public interface IDataSerializer - { - byte[] Serialize(TModel model); - TModel Deserialize(byte[] data); - } -} diff --git a/src/Microsoft.AspNet.Security/DataHandler/Serializer/TicketSerializer.cs b/src/Microsoft.AspNet.Security/DataHandler/Serializer/TicketSerializer.cs deleted file mode 100644 index 70406d1d9..000000000 --- a/src/Microsoft.AspNet.Security/DataHandler/Serializer/TicketSerializer.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Security.Claims; - -namespace Microsoft.AspNet.Security.DataHandler.Serializer -{ - public class TicketSerializer : IDataSerializer - { - private const int FormatVersion = 2; - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Dispose is idempotent")] - public virtual byte[] Serialize(AuthenticationTicket model) - { - using (var memory = new MemoryStream()) - { - using (var compression = new GZipStream(memory, CompressionLevel.Optimal)) - { - using (var writer = new BinaryWriter(compression)) - { - Write(writer, model); - } - } - return memory.ToArray(); - } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Dispose is idempotent")] - public virtual AuthenticationTicket Deserialize(byte[] data) - { - using (var memory = new MemoryStream(data)) - { - using (var compression = new GZipStream(memory, CompressionMode.Decompress)) - { - using (var reader = new BinaryReader(compression)) - { - return Read(reader); - } - } - } - } - - public static void Write([NotNull] BinaryWriter writer, [NotNull] AuthenticationTicket model) - { - writer.Write(FormatVersion); - ClaimsIdentity identity = model.Identity; - writer.Write(identity.AuthenticationType); - WriteWithDefault(writer, identity.NameClaimType, DefaultValues.NameClaimType); - WriteWithDefault(writer, identity.RoleClaimType, DefaultValues.RoleClaimType); - writer.Write(identity.Claims.Count()); - foreach (var claim in identity.Claims) - { - WriteWithDefault(writer, claim.Type, identity.NameClaimType); - writer.Write(claim.Value); - WriteWithDefault(writer, claim.ValueType, DefaultValues.StringValueType); - WriteWithDefault(writer, claim.Issuer, DefaultValues.LocalAuthority); - WriteWithDefault(writer, claim.OriginalIssuer, claim.Issuer); - } - PropertiesSerializer.Write(writer, model.Properties); - } - - public static AuthenticationTicket Read([NotNull] BinaryReader reader) - { - if (reader.ReadInt32() != FormatVersion) - { - return null; - } - - string authenticationType = reader.ReadString(); - string nameClaimType = ReadWithDefault(reader, DefaultValues.NameClaimType); - string roleClaimType = ReadWithDefault(reader, DefaultValues.RoleClaimType); - int count = reader.ReadInt32(); - var claims = new Claim[count]; - for (int index = 0; index != count; ++index) - { - string type = ReadWithDefault(reader, nameClaimType); - string value = reader.ReadString(); - string valueType = ReadWithDefault(reader, DefaultValues.StringValueType); - string issuer = ReadWithDefault(reader, DefaultValues.LocalAuthority); - string originalIssuer = ReadWithDefault(reader, issuer); - claims[index] = new Claim(type, value, valueType, issuer, originalIssuer); - } - var identity = new ClaimsIdentity(claims, authenticationType, nameClaimType, roleClaimType); - var properties = PropertiesSerializer.Read(reader); - return new AuthenticationTicket(identity, properties); - } - - private static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue) - { - if (string.Equals(value, defaultValue, StringComparison.Ordinal)) - { - writer.Write(DefaultValues.DefaultStringPlaceholder); - } - else - { - writer.Write(value); - } - } - - private static string ReadWithDefault(BinaryReader reader, string defaultValue) - { - string value = reader.ReadString(); - if (string.Equals(value, DefaultValues.DefaultStringPlaceholder, StringComparison.Ordinal)) - { - return defaultValue; - } - return value; - } - - private static class DefaultValues - { - public const string DefaultStringPlaceholder = "\0"; - public const string NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"; - public const string RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"; - public const string LocalAuthority = "LOCAL AUTHORITY"; - public const string StringValueType = "http://www.w3.org/2001/XMLSchema#string"; - } - } -} diff --git a/src/Microsoft.AspNet.Security/DataHandler/TicketDataFormat.cs b/src/Microsoft.AspNet.Security/DataHandler/TicketDataFormat.cs deleted file mode 100644 index 8d0d0c69f..000000000 --- a/src/Microsoft.AspNet.Security/DataHandler/TicketDataFormat.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.AspNet.Security.DataHandler.Encoder; -using Microsoft.AspNet.Security.DataHandler.Serializer; -using Microsoft.AspNet.Security.DataProtection; - -namespace Microsoft.AspNet.Security.DataHandler -{ - public class TicketDataFormat : SecureDataFormat - { - public TicketDataFormat(IDataProtector protector) : base(DataSerializers.Ticket, protector, TextEncodings.Base64Url) - { - } - } -} diff --git a/src/Microsoft.AspNet.Security/DataProtection/DataProtectionHelpers.cs b/src/Microsoft.AspNet.Security/DataProtection/DataProtectionHelpers.cs deleted file mode 100644 index 96b151e81..000000000 --- a/src/Microsoft.AspNet.Security/DataProtection/DataProtectionHelpers.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNet.Http; - -namespace Microsoft.AspNet.Security.DataProtection -{ - public static class DataProtectionHelpers - { - public static IDataProtector CreateDataProtector(IDataProtectionProvider dataProtectionProvider, params string[] purposes) - { - if (dataProtectionProvider == null) - { - dataProtectionProvider = DataProtectionProvider.CreateFromDpapi(); - } - - return dataProtectionProvider.CreateProtector(string.Join(";", purposes)); - } - } -} diff --git a/src/Microsoft.AspNet.Security/DefaultAuthorizationService.cs b/src/Microsoft.AspNet.Security/DefaultAuthorizationService.cs deleted file mode 100644 index 129290152..000000000 --- a/src/Microsoft.AspNet.Security/DefaultAuthorizationService.cs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Security -{ - public class DefaultAuthorizationService : IAuthorizationService - { - private readonly IList _policies; - public int MaxRetries = 99; - - public DefaultAuthorizationService(IEnumerable policies) - { - if (policies == null) - { - _policies = Enumerable.Empty().ToArray(); - } - else - { - _policies = policies.OrderBy(x => x.Order).ToArray(); - } - } - - public async Task AuthorizeAsync(IEnumerable claims, ClaimsPrincipal user) - { - return await AuthorizeAsync(claims, user, null); - } - - public bool Authorize(IEnumerable claims, ClaimsPrincipal user) - { - return AuthorizeAsync(claims, user, null).Result; - } - - public async Task AuthorizeAsync(IEnumerable claims, ClaimsPrincipal user, object resource) - { - var context = new AuthorizationPolicyContext(claims, user, resource); - - foreach (var policy in _policies) - { - await policy.ApplyingAsync(context); - } - - // we only apply the policies for a limited number of times to prevent - // infinite loops - - int retries; - for (retries = 0; retries < MaxRetries; retries++) - { - // we don't need to check for owned claims if the permission is already granted - if (!context.Authorized) - { - if (context.User != null) - { - if (ClaimsMatch(context.Claims, context.UserClaims)) - { - context.Authorized = true; - } - } - } - - // reset the retry flag - context.Retry = false; - - // give a chance for policies to change claims or the grant - foreach (var policy in _policies) - { - await policy.ApplyAsync(context); - } - - // if no policies have changed the context, stop checking - if (!context.Retry) - { - break; - } - } - - if (retries == MaxRetries) - { - throw new InvalidOperationException("Too many authorization retries."); - } - - foreach (var policy in _policies) - { - await policy.AppliedAsync(context); - } - - return context.Authorized; - } - - public bool Authorize(IEnumerable claims, ClaimsPrincipal user, object resource) - { - return AuthorizeAsync(claims, user, resource).Result; - } - - private bool ClaimsMatch([NotNull] IEnumerable x, [NotNull] IEnumerable y) - { - return x.Any(claim => - y.Any(userClaim => - string.Equals(claim.Type, userClaim.Type, StringComparison.OrdinalIgnoreCase) && - string.Equals(claim.Value, userClaim.Value, StringComparison.Ordinal) - ) - ); - - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/IAuthorizationPolicy.cs b/src/Microsoft.AspNet.Security/IAuthorizationPolicy.cs deleted file mode 100644 index 395549065..000000000 --- a/src/Microsoft.AspNet.Security/IAuthorizationPolicy.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. - -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Security -{ - public interface IAuthorizationPolicy - { - int Order { get; set; } - Task ApplyingAsync(AuthorizationPolicyContext context); - Task ApplyAsync(AuthorizationPolicyContext context); - Task AppliedAsync(AuthorizationPolicyContext context); - } -} diff --git a/src/Microsoft.AspNet.Security/IAuthorizationService.cs b/src/Microsoft.AspNet.Security/IAuthorizationService.cs deleted file mode 100644 index cbd705c8f..000000000 --- a/src/Microsoft.AspNet.Security/IAuthorizationService.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. - -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Security -{ - /// - /// Checks claims based permissions for a user. - /// - public interface IAuthorizationService - { - /// - /// Checks if a user has specific claims. - /// - /// The claims to check against a specific user. - /// The user to check claims against. - /// true when the user fulfills one of the claims, false otherwise. - Task AuthorizeAsync(IEnumerable claims, ClaimsPrincipal user); - - /// - /// Checks if a user has specific claims. - /// - /// The claims to check against a specific user. - /// The user to check claims against. - /// true when the user fulfills one of the claims, false otherwise. - bool Authorize(IEnumerable claims, ClaimsPrincipal user); - - /// - /// Checks if a user has specific claims for a specific context obj. - /// - /// The claims to check against a specific user. - /// The user to check claims against. - /// The resource the claims should be check with. - /// true when the user fulfills one of the claims, false otherwise. - Task AuthorizeAsync(IEnumerable claims, ClaimsPrincipal user, object resource); - - /// - /// Checks if a user has specific claims for a specific context obj. - /// - /// The claims to check against a specific user. - /// The user to check claims against. - /// The resource the claims should be check with. - /// true when the user fulfills one of the claims, false otherwise. - bool Authorize(IEnumerable claims, ClaimsPrincipal user, object resource); - - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationMiddleware.cs deleted file mode 100644 index 726028c6d..000000000 --- a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationMiddleware.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Threading.Tasks; -using Microsoft.AspNet.Builder; -using Microsoft.AspNet.Http; - -namespace Microsoft.AspNet.Security.Infrastructure -{ - public abstract class AuthenticationMiddleware where TOptions : AuthenticationOptions - { - private readonly RequestDelegate _next; - - protected AuthenticationMiddleware([NotNull] RequestDelegate next, [NotNull] TOptions options) - { - Options = options; - _next = next; - } - - public TOptions Options { get; set; } - - public async Task Invoke(HttpContext context) - { - AuthenticationHandler handler = CreateHandler(); - await handler.Initialize(Options, context); - if (!await handler.InvokeAsync()) - { - await _next(context); - } - await handler.TeardownAsync(); - } - - protected abstract AuthenticationHandler CreateHandler(); - } -} diff --git a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenReceiveContext.cs b/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenReceiveContext.cs deleted file mode 100644 index 0f6d6bce0..000000000 --- a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenReceiveContext.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Security.Notifications; - -namespace Microsoft.AspNet.Security.Infrastructure -{ - public class AuthenticationTokenReceiveContext : BaseContext - { - private readonly ISecureDataFormat _secureDataFormat; - - public AuthenticationTokenReceiveContext( - [NotNull] HttpContext context, - [NotNull] ISecureDataFormat secureDataFormat, - [NotNull] string token) - : base(context) - { - _secureDataFormat = secureDataFormat; - Token = token; - } - - public string Token { get; protected set; } - - public AuthenticationTicket Ticket { get; protected set; } - - public void DeserializeTicket(string protectedData) - { - Ticket = _secureDataFormat.Unprotect(protectedData); - } - - public void SetTicket([NotNull] AuthenticationTicket ticket) - { - Ticket = ticket; - } - } -} diff --git a/src/Microsoft.AspNet.Security/Infrastructure/Constants.cs b/src/Microsoft.AspNet.Security/Infrastructure/Constants.cs deleted file mode 100644 index 73a8f7958..000000000 --- a/src/Microsoft.AspNet.Security/Infrastructure/Constants.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Security.Infrastructure -{ - internal static class Constants - { - public static string SecurityAuthenticate = "security.Authenticate"; - internal const string CorrelationPrefix = ".AspNet.Correlation."; - } -} diff --git a/src/Microsoft.AspNet.Security/Infrastructure/NotNullAttribute.cs b/src/Microsoft.AspNet.Security/Infrastructure/NotNullAttribute.cs deleted file mode 100644 index 131cec58e..000000000 --- a/src/Microsoft.AspNet.Security/Infrastructure/NotNullAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Microsoft.AspNet.Security -{ - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] - internal sealed class NotNullAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Infrastructure/SecurityHelper.cs b/src/Microsoft.AspNet.Security/Infrastructure/SecurityHelper.cs deleted file mode 100644 index 436101fa9..000000000 --- a/src/Microsoft.AspNet.Security/Infrastructure/SecurityHelper.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 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.Security.Principal; -using Microsoft.AspNet.Http; - -namespace Microsoft.AspNet.Security.Infrastructure -{ - /// - /// Helper code used when implementing authentication middleware - /// - public static class SecurityHelper - { - /// - /// Add an additional ClaimsIdentity to the ClaimsPrincipal - /// - /// - public static void AddUserIdentity([NotNull] HttpContext context, [NotNull] IIdentity identity) - { - var newClaimsPrincipal = new ClaimsPrincipal(identity); - - ClaimsPrincipal existingPrincipal = context.User; - if (existingPrincipal != null) - { - foreach (var existingClaimsIdentity in existingPrincipal.Identities) - { - if (existingClaimsIdentity.IsAuthenticated) - { - newClaimsPrincipal.AddIdentity(existingClaimsIdentity); - } - } - } - context.User = newClaimsPrincipal; - } - - public static bool LookupChallenge(IList authenticationTypes, string authenticationType, AuthenticationMode authenticationMode) - { - bool challengeHasAuthenticationTypes = authenticationTypes != null && authenticationTypes.Any(); - if (!challengeHasAuthenticationTypes) - { - return authenticationMode == AuthenticationMode.Active; - } - return authenticationTypes.Contains(authenticationType, StringComparer.Ordinal); - } - - /// - /// Find response sign-in details for a specific authentication middleware - /// - /// The authentication type to look for - public static bool LookupSignIn(IList identities, string authenticationType, out ClaimsIdentity identity) - { - identity = null; - foreach (var claimsIdentity in identities) - { - if (string.Equals(authenticationType, claimsIdentity.AuthenticationType, StringComparison.Ordinal)) - { - identity = claimsIdentity; - return true; - } - } - return false; - } - - /// - /// Find response sign-out details for a specific authentication middleware - /// - /// The authentication type to look for - /// The authentication mode the middleware is running under - public static bool LookupSignOut(IList authenticationTypes, string authenticationType, AuthenticationMode authenticationMode) - { - bool singOutHasAuthenticationTypes = authenticationTypes != null && authenticationTypes.Any(); - if (!singOutHasAuthenticationTypes) - { - return authenticationMode == AuthenticationMode.Active; - } - return authenticationTypes.Contains(authenticationType, StringComparer.Ordinal); - } - } -} diff --git a/src/Microsoft.AspNet.Security/Infrastructure/SignInIdentityContext.cs b/src/Microsoft.AspNet.Security/Infrastructure/SignInIdentityContext.cs deleted file mode 100644 index 52c2ddff1..000000000 --- a/src/Microsoft.AspNet.Security/Infrastructure/SignInIdentityContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Security.Claims; -using Microsoft.AspNet.Http.Security; - -namespace Microsoft.AspNet.Security.Infrastructure -{ - public class SignInIdentityContext - { - public SignInIdentityContext(ClaimsIdentity identity, AuthenticationProperties properties) - { - Identity = identity; - Properties = properties; - } - - public ClaimsIdentity Identity { get; private set; } - public AuthenticationProperties Properties { get; private set; } - } -} diff --git a/src/Microsoft.AspNet.Security/Microsoft.AspNet.Security.kproj b/src/Microsoft.AspNet.Security/Microsoft.AspNet.Security.kproj deleted file mode 100644 index fa845e638..000000000 --- a/src/Microsoft.AspNet.Security/Microsoft.AspNet.Security.kproj +++ /dev/null @@ -1,82 +0,0 @@ - - - - 12.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 0f174c63-1898-4024-9a3c-3fdf5cae5c68 - Library - - - - - - - 2.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs b/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs deleted file mode 100644 index 77c193bb4..000000000 --- a/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.AspNet.Security.Notifications -{ - public class AuthenticationFailedNotification - { - public AuthenticationFailedNotification() - { - } - - public bool Cancel { get; set; } - public Exception Exception { get; set; } - public TMessage ProtocolMessage { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Notifications/MessageReceivedNotification.cs b/src/Microsoft.AspNet.Security/Notifications/MessageReceivedNotification.cs deleted file mode 100644 index af81a2dbf..000000000 --- a/src/Microsoft.AspNet.Security/Notifications/MessageReceivedNotification.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Security.Notifications -{ - public class MessageReceivedNotification - { - public MessageReceivedNotification() - { - } - - public bool Cancel { get; set; } - public TMessage ProtocolMessage { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Notifications/RedirectFromIdentityProviderNotification.cs b/src/Microsoft.AspNet.Security/Notifications/RedirectFromIdentityProviderNotification.cs deleted file mode 100644 index a8a1dcfd5..000000000 --- a/src/Microsoft.AspNet.Security/Notifications/RedirectFromIdentityProviderNotification.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Security.Notifications -{ - public class RedirectFromIdentityProviderNotification - { - public AuthenticationTicket AuthenticationTicket { get; set; } - - public string SignInAsAuthenticationType { get; set; } - - public bool Cancel { get; set; } - - public bool IsRequestCompleted { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Security/Notifications/RedirectToIdentityProviderNotification.cs b/src/Microsoft.AspNet.Security/Notifications/RedirectToIdentityProviderNotification.cs deleted file mode 100644 index 91dfb8793..000000000 --- a/src/Microsoft.AspNet.Security/Notifications/RedirectToIdentityProviderNotification.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Security.Notifications -{ - public class RedirectToIdentityProviderNotification - { - public RedirectToIdentityProviderNotification() - { - } - - public bool Cancel { get; set; } - public TMessage ProtocolMessage { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Notifications/SecurityTokenReceivedNotification.cs b/src/Microsoft.AspNet.Security/Notifications/SecurityTokenReceivedNotification.cs deleted file mode 100644 index 2ac5c3368..000000000 --- a/src/Microsoft.AspNet.Security/Notifications/SecurityTokenReceivedNotification.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Security.Notifications -{ - public class SecurityTokenReceivedNotification - { - public SecurityTokenReceivedNotification() - { - } - - public bool Cancel { get; set; } - public string SecurityToken { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Notifications/SecurityTokenValidatedNotification.cs b/src/Microsoft.AspNet.Security/Notifications/SecurityTokenValidatedNotification.cs deleted file mode 100644 index 963266e93..000000000 --- a/src/Microsoft.AspNet.Security/Notifications/SecurityTokenValidatedNotification.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Security.Notifications -{ - public class SecurityTokenValidatedNotification - { - public SecurityTokenValidatedNotification() - { - } - - public AuthenticationTicket AuthenticationTicket { get; set; } - public bool Cancel { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/project.json b/src/Microsoft.AspNet.Security/project.json deleted file mode 100644 index 6501b3e77..000000000 --- a/src/Microsoft.AspNet.Security/project.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "version": "0.1-alpha-*", - "dependencies": { - "Microsoft.AspNet.PipelineCore": "0.1-alpha-*", - "Microsoft.AspNet.Http": "0.1-alpha-*", - "Microsoft.AspNet.FeatureModel": "0.1-alpha-*", - "Microsoft.AspNet.HttpFeature": "0.1-alpha-*", - "Microsoft.AspNet.Security.DataProtection": "0.1-alpha-*", - "Microsoft.Framework.DependencyInjection": "0.1-alpha-*", - "Microsoft.Framework.Logging": "0.1-alpha-*" - }, - "configurations": { - "net45": {}, - "k10": { - "dependencies": { - "System.Collections": "4.0.0.0", - "System.Console": "4.0.0.0", - "System.ComponentModel": "4.0.0.0", - "System.Diagnostics.Debug": "4.0.10.0", - "System.Diagnostics.Tools": "4.0.0.0", - "System.Globalization": "4.0.10.0", - "System.IO": "4.0.0.0", - "System.IO.Compression": "4.0.0.0", - "System.Linq": "4.0.0.0", - "System.Reflection": "4.0.10.0", - "System.Resources.ResourceManager": "4.0.0.0", - "System.Runtime": "4.0.20.0", - "System.Runtime.Extensions": "4.0.10.0", - "System.Runtime.InteropServices": "4.0.20.0", - "System.Security.Claims": "0.1-alpha-*", - "System.Security.Cryptography.RandomNumberGenerator" : "4.0.0.0", - "System.Security.Principal" : "4.0.0.0", - "System.Threading": "4.0.0.0", - "System.Threading.Tasks": "4.0.10.0" - } - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs b/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs new file mode 100644 index 000000000..c531c17ca --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs @@ -0,0 +1,113 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Http.Internal; +using Microsoft.Framework.Logging; +using Xunit; + +namespace Microsoft.AspNet.Authentication +{ + public class AuthenticationHandlerFacts + { + [Fact] + public void ShouldHandleSchemeAreDeterminedOnlyByMatchingAuthenticationScheme() + { + var handler = new TestHandler("Alpha"); + var passiveNoMatch = handler.ShouldHandleScheme("Beta"); + + handler = new TestHandler("Alpha"); + var passiveWithMatch = handler.ShouldHandleScheme("Alpha"); + + Assert.False(passiveNoMatch); + Assert.True(passiveWithMatch); + } + + [Fact] + public void AutomaticHandlerInAutomaticModeHandlesEmptyChallenges() + { + var handler = new TestAutoHandler("ignored", true); + Assert.True(handler.ShouldHandleScheme("")); + } + + [Fact] + public void AutomaticHandlerShouldHandleSchemeWhenSchemeMatches() + { + var handler = new TestAutoHandler("Alpha", true); + Assert.True(handler.ShouldHandleScheme("Alpha")); + } + + [Fact] + public void AutomaticHandlerShouldNotHandleChallengeWhenSchemeDoesNotMatches() + { + var handler = new TestAutoHandler("Dog", true); + Assert.False(handler.ShouldHandleScheme("Alpha")); + } + + [Fact] + public void AutomaticHandlerShouldNotHandleChallengeWhenSchemesNotEmpty() + { + var handler = new TestAutoHandler(null, true); + Assert.False(handler.ShouldHandleScheme("Alpha")); + } + + private class TestHandler : AuthenticationHandler + { + public TestHandler(string scheme) + { + Initialize(new TestOptions(), new DefaultHttpContext(), new LoggerFactory().CreateLogger("TestHandler"), Framework.WebEncoders.UrlEncoder.Default); + Options.AuthenticationScheme = scheme; + } + + protected override void ApplyResponseChallenge() + { + throw new NotImplementedException(); + } + + protected override void ApplyResponseGrant() + { + throw new NotImplementedException(); + } + + protected override AuthenticationTicket AuthenticateCore() + { + throw new NotImplementedException(); + } + } + + private class TestOptions : AuthenticationOptions { } + + private class TestAutoOptions : AuthenticationOptions + { + public TestAutoOptions() + { + AutomaticAuthentication = true; + } + } + + private class TestAutoHandler : AuthenticationHandler + { + public TestAutoHandler(string scheme, bool auto) + { + Initialize(new TestAutoOptions(), new DefaultHttpContext(), new LoggerFactory().CreateLogger("TestHandler"), Framework.WebEncoders.UrlEncoder.Default); + Options.AuthenticationScheme = scheme; + Options.AutomaticAuthentication = auto; + } + + protected override void ApplyResponseChallenge() + { + throw new NotImplementedException(); + } + + protected override void ApplyResponseGrant() + { + throw new NotImplementedException(); + } + + protected override AuthenticationTicket AuthenticateCore() + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/CertificateSubjectKeyIdentifierValidatorTests.cs b/test/Microsoft.AspNet.Authentication.Test/CertificateSubjectKeyIdentifierValidatorTests.cs new file mode 100644 index 000000000..a0df4a419 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/CertificateSubjectKeyIdentifierValidatorTests.cs @@ -0,0 +1,123 @@ +// Copyright (c) .NET Foundation. All 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.Security; +using System.Security.Cryptography.X509Certificates; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Authentication +{ + public class CertificateSubjectKeyIdentifierValidatorTests + { + private static readonly X509Certificate2 SelfSigned = new X509Certificate2("selfSigned.cer"); + private static readonly X509Certificate2 Chained = new X509Certificate2("katanatest.redmond.corp.microsoft.com.cer"); + + // The Katana test cert has a valid full chain + // katanatest.redmond.corp.microsoft.com -> MSIT Machine Auth CA2 -> Microsoft Internet Authority -> Baltimore CyberTrustRoot + + private const string KatanaTestKeyIdentifier = "d964b2941aaf3e62761041b1f3db098edfa3270a"; + private const string MicrosoftInternetAuthorityKeyIdentifier = "2a4d97955d347e9db6e633be9c27c1707e67dbc1"; + + [Fact] + public void ConstructorShouldNotThrowWithValidValues() + { + var instance = new CertificateSubjectKeyIdentifierValidator(new[] { string.Empty }); + + instance.ShouldNotBe(null); + } + + [Fact] + public void ConstructorShouldThrownWhenTheValidHashEnumerableIsNull() + { + Should.Throw(() => + new CertificateSubjectKeyIdentifierValidator(null)); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenSslPolicyErrorsIsRemoteCertificateChainErrors() + { + var instance = new CertificateSubjectKeyIdentifierValidator(new[] { string.Empty }); + bool result = instance.Validate(null, null, null, SslPolicyErrors.RemoteCertificateChainErrors); + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenSslPolicyErrorsIsRemoteCertificateNameMismatch() + { + var instance = new CertificateSubjectKeyIdentifierValidator(new[] { string.Empty }); + bool result = instance.Validate(null, null, null, SslPolicyErrors.RemoteCertificateNameMismatch); + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenSslPolicyErrorsIsRemoteCertificateNotAvailable() + { + var instance = new CertificateSubjectKeyIdentifierValidator(new[] { string.Empty }); + bool result = instance.Validate(null, null, null, SslPolicyErrors.RemoteCertificateNotAvailable); + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenPassedASelfSignedCertificate() + { + var instance = new CertificateSubjectKeyIdentifierValidator(new[] { string.Empty }); + var certificateChain = new X509Chain(); + certificateChain.Build(SelfSigned); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, SelfSigned, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenPassedATrustedCertificateWhichDoesNotHaveAWhitelistedSubjectKeyIdentifier() + { + var instance = new CertificateSubjectKeyIdentifierValidator(new[] { string.Empty }); + var certificateChain = new X509Chain(); + certificateChain.Build(Chained); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, Chained, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnTrueWhenPassedATrustedCertificateWhichHasItsSubjectKeyIdentifierWhiteListed() + { + var instance = new CertificateSubjectKeyIdentifierValidator( + new[] + { + KatanaTestKeyIdentifier + }); + + var certificateChain = new X509Chain(); + certificateChain.Build(Chained); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, Chained, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(true); + } + + [Fact] + public void ValidatorShouldReturnTrueWhenPassedATrustedCertificateWhichHasAChainElementSubjectKeyIdentifierWhiteListed() + { + var instance = new CertificateSubjectKeyIdentifierValidator( + new[] + { + MicrosoftInternetAuthorityKeyIdentifier + }); + var certificateChain = new X509Chain(); + certificateChain.Build(Chained); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, Chained, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(true); + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/CertificateSubjectPublicKeyInfoValidatorTests.cs b/test/Microsoft.AspNet.Authentication.Test/CertificateSubjectPublicKeyInfoValidatorTests.cs new file mode 100644 index 000000000..366e2f3b5 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/CertificateSubjectPublicKeyInfoValidatorTests.cs @@ -0,0 +1,173 @@ +// Copyright (c) .NET Foundation. All 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.Security; +using System.Security.Cryptography.X509Certificates; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Authentication +{ + public class CertificateSubjectPublicKeyInfoValidatorTests + { + private static readonly X509Certificate2 SelfSigned = new X509Certificate2("selfSigned.cer"); + private static readonly X509Certificate2 Chained = new X509Certificate2("katanatest.redmond.corp.microsoft.com.cer"); + + // The Katana test cert has a valid full chain + // katanatest.redmond.corp.microsoft.com -> MSIT Machine Auth CA2 -> Microsoft Internet Authority -> Baltimore CyberTrustRoot + + // The following fingerprints were generated using the go program in appendix A of the Public Key Pinning Extension for HTTP + // draft-ietf-websec-key-pinning-05 + + private const string KatanaTestSha1Hash = "xvNsCWwxvL3qsCYChZLiwNm1D6o="; + private const string KatanaTestSha256Hash = "AhR1Y/xhxK2uD7YJ0xKUPq8tYrWm4+F7DgO2wUOqB+4="; + + private const string MicrosoftInternetAuthoritySha1Hash = "Z3HnseSVDEPu5hZoj05/bBSnT/s="; + private const string MicrosoftInternetAuthoritySha256Hash = "UQTPeq/Tlg/vLt2ijtl7qlMFBFkbGG9aAWJbQMOMWFg="; + + [Fact] + public void ConstructorShouldNotThrowWithValidValues() + { + var instance = new CertificateSubjectPublicKeyInfoValidator(new string[1], SubjectPublicKeyInfoAlgorithm.Sha1); + + instance.ShouldNotBe(null); + } + + [Fact] + public void ConstructorShouldThrownWhenTheValidHashEnumerableIsNull() + { + Should.Throw(() => + new CertificateSubjectPublicKeyInfoValidator(null, SubjectPublicKeyInfoAlgorithm.Sha1)); + } + + [Fact] + public void ConstructorShouldThrowWhenTheHashEnumerableContainsNoHashes() + { + Should.Throw(() => + new CertificateSubjectPublicKeyInfoValidator(new string[0], SubjectPublicKeyInfoAlgorithm.Sha1)); + } + + [Fact] + public void ConstructorShouldThrowIfAnInvalidAlgorithmIsPassed() + { + Should.Throw(() => + new CertificateSubjectPublicKeyInfoValidator(new string[0], (SubjectPublicKeyInfoAlgorithm)2)); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenSslPolicyErrorsIsRemoteCertificateChainErrors() + { + var instance = new CertificateSubjectPublicKeyInfoValidator(new string[1], SubjectPublicKeyInfoAlgorithm.Sha1); + bool result = instance.Validate(null, null, null, SslPolicyErrors.RemoteCertificateChainErrors); + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenSslPolicyErrorsIsRemoteCertificateNameMismatch() + { + var instance = new CertificateSubjectPublicKeyInfoValidator(new string[1], SubjectPublicKeyInfoAlgorithm.Sha1); + bool result = instance.Validate(null, null, null, SslPolicyErrors.RemoteCertificateNameMismatch); + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenSslPolicyErrorsIsRemoteCertificateNotAvailable() + { + var instance = new CertificateSubjectPublicKeyInfoValidator(new string[1], SubjectPublicKeyInfoAlgorithm.Sha1); + bool result = instance.Validate(null, null, null, SslPolicyErrors.RemoteCertificateNotAvailable); + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenPassedASelfSignedCertificate() + { + var instance = new CertificateSubjectPublicKeyInfoValidator(new string[1], SubjectPublicKeyInfoAlgorithm.Sha1); + var certificateChain = new X509Chain(); + certificateChain.Build(SelfSigned); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, SelfSigned, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenPassedATrustedCertificateWhichDoesNotHaveAWhitelistedSha1Spki() + { + var instance = new CertificateSubjectPublicKeyInfoValidator(new string[1], SubjectPublicKeyInfoAlgorithm.Sha1); + var certificateChain = new X509Chain(); + certificateChain.Build(Chained); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, Chained, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnTrueWhenPassedATrustedCertificateWhichHasItsSha1SpkiWhiteListed() + { + var instance = new CertificateSubjectPublicKeyInfoValidator(new[] { KatanaTestSha1Hash }, SubjectPublicKeyInfoAlgorithm.Sha1); + var certificateChain = new X509Chain(); + certificateChain.Build(Chained); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, Chained, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(true); + } + + [Fact] + public void ValidatorShouldReturnTrueWhenPassedATrustedCertificateWhichHasAChainElementSha1SpkiWhiteListed() + { + var instance = new CertificateSubjectPublicKeyInfoValidator(new[] { MicrosoftInternetAuthoritySha1Hash }, SubjectPublicKeyInfoAlgorithm.Sha1); + var certificateChain = new X509Chain(); + certificateChain.Build(Chained); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, Chained, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(true); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenPassedATrustedCertificateWhichDoesNotHaveAWhitelistedSha256Spki() + { + var instance = new CertificateSubjectPublicKeyInfoValidator(new string[1], SubjectPublicKeyInfoAlgorithm.Sha256); + var certificateChain = new X509Chain(); + certificateChain.Build(Chained); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, Chained, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnTrueWhenPassedATrustedCertificateWhichHasItsSha256SpkiWhiteListed() + { + var instance = new CertificateSubjectPublicKeyInfoValidator(new[] { KatanaTestSha256Hash }, SubjectPublicKeyInfoAlgorithm.Sha256); + var certificateChain = new X509Chain(); + certificateChain.Build(Chained); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, Chained, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(true); + } + + [Fact] + public void ValidatorShouldReturnTrueWhenPassedATrustedCertificateWhichHasAChainElementSha256SpkiWhiteListed() + { + var instance = new CertificateSubjectPublicKeyInfoValidator(new[] { MicrosoftInternetAuthoritySha256Hash }, SubjectPublicKeyInfoAlgorithm.Sha256); + var certificateChain = new X509Chain(); + certificateChain.Build(Chained); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, Chained, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(true); + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/CertificateThumbprintValidatorTests.cs b/test/Microsoft.AspNet.Authentication.Test/CertificateThumbprintValidatorTests.cs new file mode 100644 index 000000000..30537c51d --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/CertificateThumbprintValidatorTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Authentication +{ + public class CertificateThumbprintValidatorTests + { + private static readonly X509Certificate2 SelfSigned = new X509Certificate2("selfSigned.cer"); + private static readonly X509Certificate2 Chained = new X509Certificate2("katanatest.redmond.corp.microsoft.com.cer"); + + // The Katana test cert has a valid full chain + // katanatest.redmond.corp.microsoft.com -> MSIT Machine Auth CA2 -> Microsoft Internet Authority -> Baltimore CyberTrustRoot + + private const string KatanaTestThumbprint = "a9894c464b260cac3f5b91cece33b3c55e82e61c"; + private const string MicrosoftInternetAuthorityThumbprint = "992ad44d7dce298de17e6f2f56a7b9caa41db93f"; + + [Fact] + public void ConstructorShouldNotThrowWithValidValues() + { + var instance = new CertificateThumbprintValidator(new string[1]); + + instance.ShouldNotBe(null); + } + + [Fact] + public void ConstructorShouldThrownWhenTheValidHashEnumerableIsNull() + { + Should.Throw(() => + new CertificateThumbprintValidator(null)); + } + + [Fact] + public void ConstructorShouldThrowWhenTheHashEnumerableContainsNoHashes() + { + Should.Throw(() => + new CertificateThumbprintValidator(new string[0])); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenSslPolicyErrorsIsRemoteCertificateChainErrors() + { + var instance = new CertificateThumbprintValidator(new string[1]); + bool result = instance.Validate(null, null, null, SslPolicyErrors.RemoteCertificateChainErrors); + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenSslPolicyErrorsIsRemoteCertificateNameMismatch() + { + var instance = new CertificateThumbprintValidator(new string[1]); + bool result = instance.Validate(null, null, null, SslPolicyErrors.RemoteCertificateNameMismatch); + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenSslPolicyErrorsIsRemoteCertificateNotAvailable() + { + var instance = new CertificateThumbprintValidator(new string[1]); + bool result = instance.Validate(null, null, null, SslPolicyErrors.RemoteCertificateNotAvailable); + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenPassedASelfSignedCertificate() + { + var instance = new CertificateThumbprintValidator(new string[1]); + var certificateChain = new X509Chain(); + certificateChain.Build(SelfSigned); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, SelfSigned, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnFalseWhenPassedATrustedCertificateWhichDoesNotHaveAWhitelistedThumbprint() + { + var instance = new CertificateThumbprintValidator(new string[1]); + var certificateChain = new X509Chain(); + certificateChain.Build(Chained); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, Chained, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(false); + } + + [Fact] + public void ValidatorShouldReturnTrueWhenPassedATrustedCertificateWhichHasItsThumbprintWhiteListed() + { + var instance = new CertificateThumbprintValidator(new[] { KatanaTestThumbprint }); + var certificateChain = new X509Chain(); + certificateChain.Build(Chained); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, Chained, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(true); + } + + [Fact] + public void ValidatorShouldReturnTrueWhenPassedATrustedCertificateWhichHasAChainElementThumbprintWhiteListed() + { + var instance = new CertificateThumbprintValidator(new[] { MicrosoftInternetAuthorityThumbprint }); + var certificateChain = new X509Chain(); + certificateChain.Build(Chained); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + bool result = instance.Validate(null, Chained, certificateChain, SslPolicyErrors.None); + + result.ShouldBe(true); + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs new file mode 100644 index 000000000..c36836532 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs @@ -0,0 +1,655 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Authentication.Cookies +{ + public class CookieMiddlewareTests + { + [Fact] + public async Task NormalRequestPassesThrough() + { + var server = CreateServer(options => + { + }); + var response = await server.CreateClient().GetAsync("http://example.com/normal"); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ProtectedRequestShouldRedirectToLoginOnlyWhenAutomatic(bool auto) + { + var server = CreateServer(options => + { + options.LoginPath = new PathString("/login"); + options.AutomaticAuthentication = auto; + }); + + var transaction = await SendAsync(server, "http://example.com/protected"); + + transaction.Response.StatusCode.ShouldBe(auto ? HttpStatusCode.Redirect : HttpStatusCode.Unauthorized); + if (auto) + { + Uri location = transaction.Response.Headers.Location; + location.LocalPath.ShouldBe("/login"); + location.Query.ShouldBe("?ReturnUrl=%2Fprotected"); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ProtectedCustomRequestShouldRedirectToCustomLogin(bool auto) + { + var server = CreateServer(options => + { + options.LoginPath = new PathString("/login"); + options.AutomaticAuthentication = auto; + }); + + var transaction = await SendAsync(server, "http://example.com/protected/CustomRedirect"); + + transaction.Response.StatusCode.ShouldBe(auto ? HttpStatusCode.Redirect : HttpStatusCode.Unauthorized); + if (auto) + { + Uri location = transaction.Response.Headers.Location; + location.ToString().ShouldBe("/CustomRedirect"); + } + } + + private Task SignInAsAlice(HttpContext context) + { + context.Authentication.SignIn("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), + new AuthenticationProperties()); + return Task.FromResult(null); + } + + private Task SignInAsWrong(HttpContext context) + { + context.Authentication.SignIn("Oops", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), + new AuthenticationProperties()); + return Task.FromResult(null); + } + + private Task SignOutAsWrong(HttpContext context) + { + context.Authentication.SignOut("Oops"); + return Task.FromResult(null); + } + + [Fact] + public async Task SignInCausesDefaultCookieToBeCreated() + { + var server = CreateServer(options => + { + options.LoginPath = new PathString("/login"); + options.CookieName = "TestCookie"; + }, SignInAsAlice); + + var transaction = await SendAsync(server, "http://example.com/testpath"); + + var setCookie = transaction.SetCookie; + setCookie.ShouldStartWith("TestCookie="); + setCookie.ShouldContain("; path=/"); + setCookie.ShouldContain("; HttpOnly"); + setCookie.ShouldNotContain("; expires="); + setCookie.ShouldNotContain("; domain="); + setCookie.ShouldNotContain("; secure"); + } + + [Fact] + public async Task SignInWrongAuthTypeThrows() + { + var server = CreateServer(options => + { + options.LoginPath = new PathString("/login"); + options.CookieName = "TestCookie"; + }, SignInAsWrong); + + await Assert.ThrowsAsync(async () => await SendAsync(server, "http://example.com/testpath")); + } + + [Fact] + public async Task SignOutWrongAuthTypeThrows() + { + var server = CreateServer(options => + { + options.LoginPath = new PathString("/login"); + options.CookieName = "TestCookie"; + }, SignOutAsWrong); + + await Assert.ThrowsAsync(async () => await SendAsync(server, "http://example.com/testpath")); + } + + [Theory] + [InlineData(CookieSecureOption.Always, "http://example.com/testpath", true)] + [InlineData(CookieSecureOption.Always, "https://example.com/testpath", true)] + [InlineData(CookieSecureOption.Never, "http://example.com/testpath", false)] + [InlineData(CookieSecureOption.Never, "https://example.com/testpath", false)] + [InlineData(CookieSecureOption.SameAsRequest, "http://example.com/testpath", false)] + [InlineData(CookieSecureOption.SameAsRequest, "https://example.com/testpath", true)] + public async Task SecureSignInCausesSecureOnlyCookieByDefault( + CookieSecureOption cookieSecureOption, + string requestUri, + bool shouldBeSecureOnly) + { + var server = CreateServer(options => + { + options.LoginPath = new PathString("/login"); + options.CookieName = "TestCookie"; + options.CookieSecure = cookieSecureOption; + }, SignInAsAlice); + + var transaction = await SendAsync(server, requestUri); + var setCookie = transaction.SetCookie; + + if (shouldBeSecureOnly) + { + setCookie.ShouldContain("; secure"); + } + else + { + setCookie.ShouldNotContain("; secure"); + } + } + + [Fact] + public async Task CookieOptionsAlterSetCookieHeader() + { + TestServer server1 = CreateServer(options => + { + options.CookieName = "TestCookie"; + options.CookiePath = "/foo"; + options.CookieDomain = "another.com"; + options.CookieSecure = CookieSecureOption.Always; + options.CookieHttpOnly = true; + }, SignInAsAlice, new Uri("http://example.com/base")); + + var transaction1 = await SendAsync(server1, "http://example.com/base/testpath"); + + var setCookie1 = transaction1.SetCookie; + + setCookie1.ShouldContain("TestCookie="); + setCookie1.ShouldContain(" path=/foo"); + setCookie1.ShouldContain(" domain=another.com"); + setCookie1.ShouldContain(" secure"); + setCookie1.ShouldContain(" HttpOnly"); + + var server2 = CreateServer(options => + { + options.CookieName = "SecondCookie"; + options.CookieSecure = CookieSecureOption.Never; + options.CookieHttpOnly = false; + }, SignInAsAlice, new Uri("http://example.com/base")); + + var transaction2 = await SendAsync(server2, "http://example.com/base/testpath"); + + var setCookie2 = transaction2.SetCookie; + + setCookie2.ShouldContain("SecondCookie="); + setCookie2.ShouldContain(" path=/base"); + setCookie2.ShouldNotContain(" domain="); + setCookie2.ShouldNotContain(" secure"); + setCookie2.ShouldNotContain(" HttpOnly"); + } + + [Fact] + public async Task CookieContainsIdentity() + { + var clock = new TestClock(); + var server = CreateServer(options => + { + options.SystemClock = clock; + }, SignInAsAlice); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice"); + } + + [Fact] + public async Task CookieAppliesClaimsTransform() + { + var clock = new TestClock(); + var server = CreateServer(options => + { + options.SystemClock = clock; + }, + SignInAsAlice, + baseAddress: null, + claimsTransform: o => o.Transformation = (p => + { + if (!p.Identities.Any(i => i.AuthenticationType == "xform")) + { + // REVIEW: Xform runs twice, once on Authenticate, and then once from the middleware + var id = new ClaimsIdentity("xform"); + id.AddClaim(new Claim("xform", "yup")); + p.AddIdentity(id); + } + return p; + })); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice"); + FindClaimValue(transaction2, "xform").ShouldBe("yup"); + + } + + [Fact] + public async Task CookieStopsWorkingAfterExpiration() + { + var clock = new TestClock(); + var server = CreateServer(options => + { + options.SystemClock = clock; + options.ExpireTimeSpan = TimeSpan.FromMinutes(10); + options.SlidingExpiration = false; + }, SignInAsAlice); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + clock.Add(TimeSpan.FromMinutes(7)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + clock.Add(TimeSpan.FromMinutes(7)); + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + transaction2.SetCookie.ShouldBe(null); + FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice"); + transaction3.SetCookie.ShouldBe(null); + FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice"); + transaction4.SetCookie.ShouldBe(null); + FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe(null); + } + + [Fact] + public async Task CookieExpirationCanBeOverridenInSignin() + { + var clock = new TestClock(); + var server = CreateServer(options => + { + options.SystemClock = clock; + options.ExpireTimeSpan = TimeSpan.FromMinutes(10); + options.SlidingExpiration = false; + }, + context => + { + context.Authentication.SignIn("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), + new AuthenticationProperties() { ExpiresUtc = clock.UtcNow.Add(TimeSpan.FromMinutes(5)) }); + return Task.FromResult(null); + }); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + clock.Add(TimeSpan.FromMinutes(3)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + clock.Add(TimeSpan.FromMinutes(3)); + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + transaction2.SetCookie.ShouldBe(null); + FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice"); + transaction3.SetCookie.ShouldBe(null); + FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice"); + transaction4.SetCookie.ShouldBe(null); + FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe(null); + } + + [Fact] + public async Task CookieExpirationCanBeOverridenInEvent() + { + var clock = new TestClock(); + var server = CreateServer(options => + { + options.SystemClock = clock; + options.ExpireTimeSpan = TimeSpan.FromMinutes(10); + options.SlidingExpiration = false; + options.Notifications = new CookieAuthenticationNotifications() + { + OnResponseSignIn = context => + { + context.Properties.ExpiresUtc = clock.UtcNow.Add(TimeSpan.FromMinutes(5)); + } + }; + }, SignInAsAlice); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + clock.Add(TimeSpan.FromMinutes(3)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + clock.Add(TimeSpan.FromMinutes(3)); + + var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + transaction2.SetCookie.ShouldBe(null); + FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice"); + transaction3.SetCookie.ShouldBe(null); + FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice"); + transaction4.SetCookie.ShouldBe(null); + FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe(null); + } + + [Fact] + public async Task CookieIsRenewedWithSlidingExpiration() + { + var clock = new TestClock(); + var server = CreateServer(options => + { + options.SystemClock = clock; + options.ExpireTimeSpan = TimeSpan.FromMinutes(10); + options.SlidingExpiration = true; + }, SignInAsAlice); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + clock.Add(TimeSpan.FromMinutes(4)); + + var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + 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); + + clock.Add(TimeSpan.FromMinutes(4)); + + Transaction transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); + + transaction2.SetCookie.ShouldBe(null); + FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice"); + transaction3.SetCookie.ShouldBe(null); + FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice"); + transaction4.SetCookie.ShouldNotBe(null); + FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe("Alice"); + transaction5.SetCookie.ShouldBe(null); + FindClaimValue(transaction5, ClaimTypes.Name).ShouldBe("Alice"); + } + + [Fact] + public async Task AjaxRedirectsAsExtraHeaderOnTwoHundred() + { + var server = CreateServer(options => + { + options.LoginPath = new PathString("/login"); + options.AutomaticAuthentication = true; + }); + + var transaction = await SendAsync(server, "http://example.com/protected", ajaxRequest: true); + + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + var responded = transaction.Response.Headers.GetValues("X-Responded-JSON"); + + responded.Count().ShouldBe(1); + responded.Single().ShouldContain("\"location\""); + } + + [Fact] + public async Task CookieUsesPathBaseByDefault() + { + var clock = new TestClock(); + var server = CreateServer(options => { }, + context => + { + Assert.Equal(new PathString("/base"), context.Request.PathBase); + context.Authentication.SignIn("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))); + return Task.FromResult(null); + }, + new Uri("http://example.com/base")); + + var transaction1 = await SendAsync(server, "http://example.com/base/testpath"); + Assert.True(transaction1.SetCookie.Contains("path=/base")); + } + + [Fact] + public async Task CookieTurns401To403IfAuthenticated() + { + var clock = new TestClock(); + var server = CreateServer(options => + { + options.SystemClock = clock; + }, + SignInAsAlice); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/unauthorized", transaction1.CookieNameValue); + + transaction2.Response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task CookieTurns401ToAccessDeniedWhenSetAndIfAuthenticated() + { + var clock = new TestClock(); + var server = CreateServer(options => + { + options.SystemClock = clock; + options.AccessDeniedPath = new PathString("/accessdenied"); + }, + SignInAsAlice); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/unauthorized", transaction1.CookieNameValue); + + transaction2.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + + var location = transaction2.Response.Headers.Location; + location.LocalPath.ShouldBe("/accessdenied"); + } + + [Fact] + public async Task CookieDoesNothingTo401IfNotAuthenticated() + { + var clock = new TestClock(); + var server = CreateServer(options => + { + options.SystemClock = clock; + }); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/unauthorized", transaction1.CookieNameValue); + + transaction2.Response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + } + + private static string FindClaimValue(Transaction transaction, string claimType) + { + var claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + + private static async Task GetAuthData(TestServer server, string url, string cookie) + { + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("Cookie", cookie); + + var response2 = await server.CreateClient().SendAsync(request); + var text = await response2.Content.ReadAsStringAsync(); + var me = XElement.Parse(text); + return me; + } + + private static TestServer CreateServer(Action configureOptions, Func testpath = null, Uri baseAddress = null, Action claimsTransform = null) + { + var server = TestServer.Create(app => + { + app.UseCookieAuthentication(configureOptions); + + if (claimsTransform != null) + { + app.UseClaimsTransformation(); + } + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + PathString remainder; + if (req.Path == new PathString("/normal")) + { + res.StatusCode = 200; + } + else if (req.Path == new PathString("/protected")) + { + res.StatusCode = 401; + } + else if (req.Path == new PathString("/unauthorized")) + { + // Simulate Authorization failure + var result = await context.Authentication.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); + context.Authentication.Challenge(CookieAuthenticationDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString("/protected/CustomRedirect")) + { + context.Authentication.Challenge(new AuthenticationProperties() { RedirectUri = "/CustomRedirect" }); + } + else if (req.Path == new PathString("/me")) + { + Describe(res, new AuthenticationResult(context.User, new AuthenticationProperties(), new AuthenticationDescription())); + } + else if (req.Path.StartsWithSegments(new PathString("/me"), out remainder)) + { + var result = await context.Authentication.AuthenticateAsync(remainder.Value.Substring(1)); + Describe(res, result); + } + else if (req.Path == new PathString("/testpath") && testpath != null) + { + await testpath(context); + } + else + { + await next(); + } + }); + }, + services => + { + services.AddAuthentication(); + if (claimsTransform != null) + { + services.ConfigureClaimsTransformation(claimsTransform); + } + }); + server.BaseAddress = baseAddress; + return server; + } + + private static void Describe(HttpResponse res, AuthenticationResult result) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (result != null && result.Principal != null) + { + xml.Add(result.Principal.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value)))); + } + if (result != null && result.Properties != null) + { + xml.Add(result.Properties.Items.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value)))); + } + using (var memory = new MemoryStream()) + { + using (var writer = new XmlTextWriter(memory, Encoding.UTF8)) + { + xml.WriteTo(writer); + } + res.Body.Write(memory.ToArray(), 0, memory.ToArray().Length); + } + } + + private static async Task SendAsync(TestServer server, string uri, string cookieHeader = null, bool ajaxRequest = false) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(cookieHeader)) + { + request.Headers.Add("Cookie", cookieHeader); + } + if (ajaxRequest) + { + request.Headers.Add("X-Requested-With", "XMLHttpRequest"); + } + var transaction = new Transaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").SingleOrDefault(); + } + if (!string.IsNullOrEmpty(transaction.SetCookie)) + { + transaction.CookieNameValue = transaction.SetCookie.Split(new[] { ';' }, 2).First(); + } + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + return transaction; + } + + private class Transaction + { + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage Response { get; set; } + + public string SetCookie { get; set; } + public string CookieNameValue { get; set; } + + public string ResponseText { get; set; } + public XElement ResponseElement { get; set; } + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/Cookies/Infrastructure/CookieChunkingTests.cs b/test/Microsoft.AspNet.Authentication.Test/Cookies/Infrastructure/CookieChunkingTests.cs new file mode 100644 index 000000000..425fa1742 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/Cookies/Infrastructure/CookieChunkingTests.cs @@ -0,0 +1,168 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Internal; +using Xunit; + +namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure +{ + public class CookieChunkingTests + { + [Fact] + public void AppendLargeCookie_Appended() + { + HttpContext context = new DefaultHttpContext(); + + string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + new ChunkingCookieManager(null) { ChunkSize = null }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions()); + IList values = context.Response.Headers.GetValues("Set-Cookie"); + Assert.Equal(1, values.Count); + Assert.Equal("TestCookie=" + testString + "; path=/", values[0]); + } + + [Fact] + public void AppendLargeCookieWithLimit_Chunked() + { + HttpContext context = new DefaultHttpContext(); + + string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + new ChunkingCookieManager(null) { ChunkSize = 30 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions()); + IList values = context.Response.Headers.GetValues("Set-Cookie"); + Assert.Equal(9, values.Count); + Assert.Equal(new[] + { + "TestCookie=chunks:8; path=/", + "TestCookieC1=abcdefgh; path=/", + "TestCookieC2=ijklmnop; path=/", + "TestCookieC3=qrstuvwx; path=/", + "TestCookieC4=yz012345; path=/", + "TestCookieC5=6789ABCD; path=/", + "TestCookieC6=EFGHIJKL; path=/", + "TestCookieC7=MNOPQRST; path=/", + "TestCookieC8=UVWXYZ; path=/", + }, values); + } + + [Fact] + public void AppendLargeQuotedCookieWithLimit_QuotedChunked() + { + HttpContext context = new DefaultHttpContext(); + + string testString = "\"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\""; + new ChunkingCookieManager(null) { ChunkSize = 32 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions()); + IList values = context.Response.Headers.GetValues("Set-Cookie"); + Assert.Equal(9, values.Count); + Assert.Equal(new[] + { + "TestCookie=chunks:8; path=/", + "TestCookieC1=\"abcdefgh\"; path=/", + "TestCookieC2=\"ijklmnop\"; path=/", + "TestCookieC3=\"qrstuvwx\"; path=/", + "TestCookieC4=\"yz012345\"; path=/", + "TestCookieC5=\"6789ABCD\"; path=/", + "TestCookieC6=\"EFGHIJKL\"; path=/", + "TestCookieC7=\"MNOPQRST\"; path=/", + "TestCookieC8=\"UVWXYZ\"; path=/", + }, values); + } + + [Fact] + public void GetLargeChunkedCookie_Reassembled() + { + HttpContext context = new DefaultHttpContext(); + context.Request.Headers.AppendValues("Cookie", + "TestCookie=chunks:7", + "TestCookieC1=abcdefghi", + "TestCookieC2=jklmnopqr", + "TestCookieC3=stuvwxyz0", + "TestCookieC4=123456789", + "TestCookieC5=ABCDEFGHI", + "TestCookieC6=JKLMNOPQR", + "TestCookieC7=STUVWXYZ"); + + string result = new ChunkingCookieManager(null).GetRequestCookie(context, "TestCookie"); + string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + Assert.Equal(testString, result); + } + + [Fact] + public void GetLargeChunkedCookieWithQuotes_Reassembled() + { + HttpContext context = new DefaultHttpContext(); + context.Request.Headers.AppendValues("Cookie", + "TestCookie=chunks:7", + "TestCookieC1=\"abcdefghi\"", + "TestCookieC2=\"jklmnopqr\"", + "TestCookieC3=\"stuvwxyz0\"", + "TestCookieC4=\"123456789\"", + "TestCookieC5=\"ABCDEFGHI\"", + "TestCookieC6=\"JKLMNOPQR\"", + "TestCookieC7=\"STUVWXYZ\""); + + string result = new ChunkingCookieManager(null).GetRequestCookie(context, "TestCookie"); + string testString = "\"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\""; + Assert.Equal(testString, result); + } + + [Fact] + public void GetLargeChunkedCookieWithMissingChunk_ThrowingEnabled_Throws() + { + HttpContext context = new DefaultHttpContext(); + context.Request.Headers.AppendValues("Cookie", + "TestCookie=chunks:7", + "TestCookieC1=abcdefghi", + // Missing chunk "TestCookieC2=jklmnopqr", + "TestCookieC3=stuvwxyz0", + "TestCookieC4=123456789", + "TestCookieC5=ABCDEFGHI", + "TestCookieC6=JKLMNOPQR", + "TestCookieC7=STUVWXYZ"); + + Assert.Throws(() => new ChunkingCookieManager(null).GetRequestCookie(context, "TestCookie")); + } + + [Fact] + public void GetLargeChunkedCookieWithMissingChunk_ThrowingDisabled_NotReassembled() + { + HttpContext context = new DefaultHttpContext(); + context.Request.Headers.AppendValues("Cookie", + "TestCookie=chunks:7", + "TestCookieC1=abcdefghi", + // Missing chunk "TestCookieC2=jklmnopqr", + "TestCookieC3=stuvwxyz0", + "TestCookieC4=123456789", + "TestCookieC5=ABCDEFGHI", + "TestCookieC6=JKLMNOPQR", + "TestCookieC7=STUVWXYZ"); + + string result = new ChunkingCookieManager(null) { ThrowForPartialCookies = false }.GetRequestCookie(context, "TestCookie"); + string testString = "chunks:7"; + Assert.Equal(testString, result); + } + + [Fact] + public void DeleteChunkedCookieWithOptions_AllDeleted() + { + HttpContext context = new DefaultHttpContext(); + context.Request.Headers.AppendValues("Cookie", "TestCookie=chunks:7"); + + new ChunkingCookieManager(null).DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com" }); + var cookies = context.Response.Headers.GetValues("Set-Cookie"); + Assert.Equal(8, cookies.Count); + Assert.Equal(new[] + { + "TestCookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", + "TestCookieC1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", + "TestCookieC2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", + "TestCookieC3=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", + "TestCookieC4=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", + "TestCookieC5=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", + "TestCookieC6=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", + "TestCookieC7=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/", + }, cookies); + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/DataHandler/Encoder/Base64UrlTextEncoderTests.cs b/test/Microsoft.AspNet.Authentication.Test/DataHandler/Encoder/Base64UrlTextEncoderTests.cs new file mode 100644 index 000000000..a345241d2 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/DataHandler/Encoder/Base64UrlTextEncoderTests.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Authentication.DataHandler.Encoder +{ + public class Base64UrlTextEncoderTests + { + [Fact] + public void DataOfVariousLengthRoundTripCorrectly() + { + var encoder = new Base64UrlTextEncoder(); + for (int length = 0; length != 256; ++length) + { + var data = new byte[length]; + for (int index = 0; index != length; ++index) + { + data[index] = (byte)(5 + length + (index * 23)); + } + string text = encoder.Encode(data); + byte[] result = encoder.Decode(text); + + for (int index = 0; index != length; ++index) + { + result[index].ShouldBe(data[index]); + } + } + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs new file mode 100644 index 000000000..6279c0a8e --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs @@ -0,0 +1,124 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Authentication.Facebook +{ + public class FacebookMiddlewareTests + { + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var server = CreateServer( + app => + { + app.UseFacebookAuthentication(); + app.UseCookieAuthentication(); + }, + services => + { + services.AddAuthentication(); + services.ConfigureFacebookAuthentication(options => + { + options.AppId = "Test App Id"; + options.AppSecret = "Test App Secret"; + options.Notifications = new FacebookAuthenticationNotifications + { + OnApplyRedirect = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + } + }; + }); + services.ConfigureCookieAuthentication(options => + { + options.AuthenticationScheme = "External"; + options.AutomaticAuthentication = true; + }); + services.Configure(options => + { + options.SignInScheme = "External"; + }); + }, + context => + { + context.Authentication.Challenge("Facebook"); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var query = transaction.Response.Headers.Location.Query; + query.ShouldContain("custom=test"); + } + + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer( + app => + { + app.UseFacebookAuthentication(); + app.UseCookieAuthentication(); + }, + services => + { + services.AddAuthentication(); + services.ConfigureFacebookAuthentication(options => + { + options.AppId = "Test App Id"; + options.AppSecret = "Test App Secret"; + }); + services.ConfigureCookieAuthentication(options => + { + options.AuthenticationScheme = "External"; + }); + services.Configure(options => + { + options.SignInScheme = "External"; + }); + }, + context => + { + context.Authentication.Challenge("Facebook"); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var location = transaction.Response.Headers.Location.AbsoluteUri; + location.ShouldContain("https://www.facebook.com/v2.2/dialog/oauth"); + location.ShouldContain("response_type=code"); + location.ShouldContain("client_id="); + location.ShouldContain("redirect_uri="); + location.ShouldContain("scope="); + location.ShouldContain("state="); + } + + private static TestServer CreateServer(Action configure, Action configureServices, Func handler) + { + return TestServer.Create(app => + { + if (configure != null) + { + configure(app); + } + app.Use(async (context, next) => + { + if (handler == null || !handler(context)) + { + await next(); + } + }); + }, + configureServices); + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs new file mode 100644 index 000000000..0b02b78e9 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs @@ -0,0 +1,524 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Authentication.DataHandler; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.DataProtection; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.WebEncoders; +using Newtonsoft.Json; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Authentication.Google +{ + public class GoogleMiddlewareTests + { + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var location = transaction.Response.Headers.Location.ToString(); + location.ShouldContain("https://accounts.google.com/o/oauth2/auth?response_type=code"); + location.ShouldContain("&client_id="); + location.ShouldContain("&redirect_uri="); + location.ShouldContain("&scope="); + location.ShouldContain("&state="); + + location.ShouldNotContain("access_type="); + location.ShouldNotContain("approval_prompt="); + location.ShouldNotContain("login_hint="); + } + + [Fact] + public async Task Challenge401WillTriggerRedirection() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + options.AutomaticAuthentication = true; + }); + var transaction = await server.SendAsync("https://example.com/401"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var location = transaction.Response.Headers.Location.ToString(); + location.ShouldContain("https://accounts.google.com/o/oauth2/auth?response_type=code"); + location.ShouldContain("&client_id="); + location.ShouldContain("&redirect_uri="); + location.ShouldContain("&scope="); + location.ShouldContain("&state="); + } + + [Fact] + public async Task ChallengeWillSetCorrelationCookie() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + Console.WriteLine(transaction.SetCookie); + transaction.SetCookie.Single().ShouldContain(".AspNet.Correlation.Google="); + } + + [Fact] + public async Task Challenge401WillSetCorrelationCookie() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + options.AutomaticAuthentication = true; + }); + var transaction = await server.SendAsync("https://example.com/401"); + Console.WriteLine(transaction.SetCookie); + transaction.SetCookie.Single().ShouldContain(".AspNet.Correlation.Google="); + } + + [Fact] + public async Task ChallengeWillSetDefaultScope() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var query = transaction.Response.Headers.Location.Query; + query.ShouldContain("&scope=" + UrlEncoder.Default.UrlEncode("openid profile email")); + } + + [Fact] + public async Task Challenge401WillSetDefaultScope() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + options.AutomaticAuthentication = true; + }); + var transaction = await server.SendAsync("https://example.com/401"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var query = transaction.Response.Headers.Location.Query; + query.ShouldContain("&scope=" + UrlEncoder.Default.UrlEncode("openid profile email")); + } + + [Fact] + public async Task ChallengeWillUseAuthenticationPropertiesAsParameters() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + options.AutomaticAuthentication = true; + }, + context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge2")) + { + context.Authentication.Challenge("Google", new AuthenticationProperties( + new Dictionary() + { + { "scope", "https://www.googleapis.com/auth/plus.login" }, + { "access_type", "offline" }, + { "approval_prompt", "force" }, + { "login_hint", "test@example.com" } + })); + res.StatusCode = 401; + } + + return Task.FromResult(null); + }); + var transaction = await server.SendAsync("https://example.com/challenge2"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var query = transaction.Response.Headers.Location.Query; + query.ShouldContain("scope=" + UrlEncoder.Default.UrlEncode("https://www.googleapis.com/auth/plus.login")); + query.ShouldContain("access_type=offline"); + query.ShouldContain("approval_prompt=force"); + query.ShouldContain("login_hint=" + UrlEncoder.Default.UrlEncode("test@example.com")); + } + + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + options.Notifications = new GoogleAuthenticationNotifications + { + OnApplyRedirect = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + } + }; + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var query = transaction.Response.Headers.Location.Query; + query.ShouldContain("custom=test"); + } + + // TODO: Fix these tests to path (Need some test logic for Authenticate("Google") to return a ticket still + //[Fact] + //public async Task GoogleTurns401To403WhenAuthenticated() + //{ + // TestServer server = CreateServer(options => + // { + // options.ClientId = "Test Id"; + // options.ClientSecret = "Test Secret"; + // }); + + // Transaction transaction1 = await SendAsync(server, "http://example.com/unauthorized"); + // transaction1.Response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + //} + + //[Fact] + //public async Task GoogleTurns401To403WhenAutomatic() + //{ + // TestServer server = CreateServer(options => + // { + // options.ClientId = "Test Id"; + // options.ClientSecret = "Test Secret"; + // options.AutomaticAuthentication = true; + // }); + + // Debugger.Launch(); + // Transaction transaction1 = await SendAsync(server, "http://example.com/unauthorizedAuto"); + // transaction1.Response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + //} + + [Fact] + public async Task ReplyPathWithoutStateQueryStringWillBeRejected() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + }); + var transaction = await server.SendAsync("https://example.com/signin-google?code=TestCode"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError); + } + + + + [Theory] + [InlineData(null)] + [InlineData("CustomIssuer")] + public async Task ReplyPathWillAuthenticateValidAuthorizeCodeAndState(string claimsIssuer) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + options.StateDataFormat = stateFormat; + options.ClaimsIssuer = claimsIssuer; + options.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://accounts.google.com/o/oauth2/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expire_in = 3600, + token_type = "Bearer" + }); + } + else if (req.RequestUri.GetLeftPart(UriPartial.Path) == "https://www.googleapis.com/plus/v1/people/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + name = new + { + familyName = "Test Family Name", + givenName = "Test Given Name" + }, + url = "Profile link", + emails = new[] + { + new + { + value = "Test email", + type = "account" + } + } + }); + } + + return null; + } + }; + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".AspNet.Correlation.Google"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.UrlEncode(state), + correlationKey + "=" + correlationValue); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + transaction.Response.Headers.Location.ToString().ShouldBe("/me"); + transaction.SetCookie.Count.ShouldBe(2); + transaction.SetCookie[0].ShouldContain(correlationKey); + transaction.SetCookie[1].ShouldContain(".AspNet." + TestExtensions.CookieAuthenticationScheme); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/me", authCookie); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + var expectedIssuer = claimsIssuer ?? GoogleAuthenticationDefaults.AuthenticationScheme; + transaction.FindClaimValue(ClaimTypes.Name, expectedIssuer).ShouldBe("Test Name"); + transaction.FindClaimValue(ClaimTypes.NameIdentifier, expectedIssuer).ShouldBe("Test User ID"); + transaction.FindClaimValue(ClaimTypes.GivenName, expectedIssuer).ShouldBe("Test Given Name"); + transaction.FindClaimValue(ClaimTypes.Surname, expectedIssuer).ShouldBe("Test Family Name"); + transaction.FindClaimValue(ClaimTypes.Email, expectedIssuer).ShouldBe("Test email"); + + // Ensure claims transformation + transaction.FindClaimValue("xform").ShouldBe("yup"); + } + + [Fact] + public async Task ReplyPathWillRejectIfCodeIsInvalid() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + options.StateDataFormat = stateFormat; + options.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + return new HttpResponseMessage(HttpStatusCode.BadRequest); + } + }; + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".AspNet.Correlation.Google"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.UrlEncode(state), + correlationKey + "=" + correlationValue); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + transaction.Response.Headers.Location.ToString().ShouldContain("error=access_denied"); + } + + [Fact] + public async Task ReplyPathWillRejectIfAccessTokenIsMissing() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + options.StateDataFormat = stateFormat; + options.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + return ReturnJsonResponse(new object()); + } + }; + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".AspNet.Correlation.Google"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.UrlEncode(state), + correlationKey + "=" + correlationValue); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + transaction.Response.Headers.Location.ToString().ShouldContain("error=access_denied"); + } + + [Fact] + public async Task AuthenticatedEventCanGetRefreshToken() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + options.StateDataFormat = stateFormat; + options.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://accounts.google.com/o/oauth2/token") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expire_in = 3600, + token_type = "Bearer", + refresh_token = "Test Refresh Token" + }); + } + else if (req.RequestUri.GetLeftPart(UriPartial.Path) == "https://www.googleapis.com/plus/v1/people/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + displayName = "Test Name", + name = new + { + familyName = "Test Family Name", + givenName = "Test Given Name" + }, + url = "Profile link", + emails = new[] + { + new + { + value = "Test email", + type = "account" + } + } + }); + } + + return null; + } + }; + options.Notifications = new GoogleAuthenticationNotifications() + { + OnAuthenticated = context => + { + var refreshToken = context.RefreshToken; + context.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "Google") }, "Google")); + return Task.FromResult(null); + } + }; + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".AspNet.Correlation.Google"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.UrlEncode(state), + correlationKey + "=" + correlationValue); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + transaction.Response.Headers.Location.ToString().ShouldBe("/me"); + transaction.SetCookie.Count.ShouldBe(2); + transaction.SetCookie[0].ShouldContain(correlationKey); + transaction.SetCookie[1].ShouldContain(".AspNet." + TestExtensions.CookieAuthenticationScheme); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/me", authCookie); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + transaction.FindClaimValue("RefreshToken").ShouldBe("Test Refresh Token"); + } + + private static HttpResponseMessage ReturnJsonResponse(object content) + { + var res = new HttpResponseMessage(HttpStatusCode.OK); + var text = JsonConvert.SerializeObject(content); + res.Content = new StringContent(text, Encoding.UTF8, "application/json"); + return res; + } + + private static TestServer CreateServer(Action configureOptions, Func testpath = null) + { + return TestServer.Create(app => + { + app.UseCookieAuthentication(options => + { + options.AuthenticationScheme = TestExtensions.CookieAuthenticationScheme; + options.AutomaticAuthentication = true; + }); + app.UseGoogleAuthentication(configureOptions); + app.UseClaimsTransformation(o => + { + o.Transformation = p => + { + var id = new ClaimsIdentity("xform"); + id.AddClaim(new Claim("xform", "yup")); + p.AddIdentity(id); + return p; + }; + }); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge")) + { + context.Authentication.Challenge("Google"); + res.StatusCode = 401; + } + else if (req.Path == new PathString("/me")) + { + res.Describe(context.User); + } + else if (req.Path == new PathString("/unauthorized")) + { + // Simulate Authorization failure + var result = await context.Authentication.AuthenticateAsync("Google"); + context.Authentication.Challenge("Google"); + } + else if (req.Path == new PathString("/unauthorizedAuto")) + { + var result = await context.Authentication.AuthenticateAsync("Google"); + res.StatusCode = 401; + context.Authentication.Challenge(); + } + else if (req.Path == new PathString("/401")) + { + res.StatusCode = 401; + } + else if (testpath != null) + { + await testpath(context); + } + else + { + await next(); + } + }); + }, + services => + { + services.AddAuthentication(); + services.Configure(options => + { + options.SignInScheme = TestExtensions.CookieAuthenticationScheme; + }); + }); + } + + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/Microsoft.AspNet.Authentication.Test.xproj b/test/Microsoft.AspNet.Authentication.Test/Microsoft.AspNet.Authentication.Test.xproj new file mode 100644 index 000000000..aaf23b99e --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/Microsoft.AspNet.Authentication.Test.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 8da26cd1-1302-4cfd-9270-9fa1b7c6138b + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs new file mode 100644 index 000000000..2d6baba35 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs @@ -0,0 +1,189 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.DataHandler; +using Microsoft.AspNet.Authentication.MicrosoftAccount; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.DataProtection; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.WebEncoders; +using Newtonsoft.Json; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Authentication.Tests.MicrosoftAccount +{ + public class MicrosoftAccountMiddlewareTests + { + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var server = CreateServer( + options => + { + options.ClientId = "Test Client Id"; + options.ClientSecret = "Test Client Secret"; + options.Notifications = new MicrosoftAccountAuthenticationNotifications + { + OnApplyRedirect = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + } + }; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var query = transaction.Response.Headers.Location.Query; + query.ShouldContain("custom=test"); + } + + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer( + options => + { + options.ClientId = "Test Client Id"; + options.ClientSecret = "Test Client Secret"; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var location = transaction.Response.Headers.Location.AbsoluteUri; + location.ShouldContain("https://login.live.com/oauth20_authorize.srf"); + location.ShouldContain("response_type=code"); + location.ShouldContain("client_id="); + location.ShouldContain("redirect_uri="); + location.ShouldContain("scope="); + location.ShouldContain("state="); + } + + [Fact] + public async Task AuthenticatedEventCanGetRefreshToken() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("MsftTest")); + var server = CreateServer( + options => + { + options.ClientId = "Test Client Id"; + options.ClientSecret = "Test Client Secret"; + options.StateDataFormat = stateFormat; + options.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://login.live.com/oauth20_token.srf") + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expire_in = 3600, + token_type = "Bearer", + refresh_token = "Test Refresh Token" + }); + } + else if (req.RequestUri.GetLeftPart(UriPartial.Path) == "https://apis.live.net/v5.0/me") + { + return ReturnJsonResponse(new + { + id = "Test User ID", + name = "Test Name", + first_name = "Test Given Name", + last_name = "Test Family Name", + emails = new + { + preferred = "Test email" + } + }); + } + + return null; + } + }; + options.Notifications = new MicrosoftAccountAuthenticationNotifications + { + OnAuthenticated = context => + { + var refreshToken = context.RefreshToken; + context.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "Microsoft") }, "Microsoft")); + return Task.FromResult(null); + } + }; + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".AspNet.Correlation.Microsoft"; + var correlationValue = "TestCorrelationId"; + properties.Items.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + "https://example.com/signin-microsoft?code=TestCode&state=" + UrlEncoder.Default.UrlEncode(state), + correlationKey + "=" + correlationValue); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + transaction.Response.Headers.Location.ToString().ShouldBe("/me"); + transaction.SetCookie.Count.ShouldBe(2); + transaction.SetCookie[0].ShouldContain(correlationKey); + transaction.SetCookie[1].ShouldContain(".AspNet." + TestExtensions.CookieAuthenticationScheme); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/me", authCookie); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + transaction.FindClaimValue("RefreshToken").ShouldBe("Test Refresh Token"); + } + + private static TestServer CreateServer(Action configureOptions) + { + return TestServer.Create(app => + { + app.UseCookieAuthentication(options => + { + options.AuthenticationScheme = TestExtensions.CookieAuthenticationScheme; + options.AutomaticAuthentication = true; + }); + app.UseMicrosoftAccountAuthentication(configureOptions); + + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge")) + { + context.Authentication.Challenge("Microsoft"); + res.StatusCode = 401; + } + else if (req.Path == new PathString("/me")) + { + res.Describe(context.User); + } + else + { + await next(); + } + }); + }, + services => + { + services.AddAuthentication(); + services.Configure(options => + { + options.SignInScheme = TestExtensions.CookieAuthenticationScheme; + }); + }); + } + + private static HttpResponseMessage ReturnJsonResponse(object content) + { + var res = new HttpResponseMessage(HttpStatusCode.OK); + var text = JsonConvert.SerializeObject(content); + res.Content = new StringContent(text, Encoding.UTF8, "application/json"); + return res; + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs new file mode 100644 index 000000000..f21b00f79 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs @@ -0,0 +1,368 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IdentityModel.Tokens; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Authentication.OAuthBearer +{ + public class OAuthBearerMiddlewareTests + { + [Fact] + public async Task BearerTokenValidation() + { + var server = CreateServer(options => + { + options.AutomaticAuthentication = true; + + options.Authority = "https://login.windows.net/tushartest.onmicrosoft.com"; + options.Audience = "https://TusharTest.onmicrosoft.com/TodoListService-ManualJwt"; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateLifetime = false + }; + }); + + var newBearerToken = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImtyaU1QZG1Cdng2OHNrVDgtbVBBQjNCc2VlQSJ9.eyJhdWQiOiJodHRwczovL1R1c2hhclRlc3Qub25taWNyb3NvZnQuY29tL1RvZG9MaXN0U2VydmljZS1NYW51YWxKd3QiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9hZmJlY2UwMy1hZWFhLTRmM2YtODVlNy1jZTA4ZGQyMGNlNTAvIiwiaWF0IjoxNDE4MzMwNjE0LCJuYmYiOjE0MTgzMzA2MTQsImV4cCI6MTQxODMzNDUxNCwidmVyIjoiMS4wIiwidGlkIjoiYWZiZWNlMDMtYWVhYS00ZjNmLTg1ZTctY2UwOGRkMjBjZTUwIiwiYW1yIjpbInB3ZCJdLCJvaWQiOiI1Mzk3OTdjMi00MDE5LTQ2NTktOWRiNS03MmM0Yzc3NzhhMzMiLCJ1cG4iOiJWaWN0b3JAVHVzaGFyVGVzdC5vbm1pY3Jvc29mdC5jb20iLCJ1bmlxdWVfbmFtZSI6IlZpY3RvckBUdXNoYXJUZXN0Lm9ubWljcm9zb2Z0LmNvbSIsInN1YiI6IkQyMm9aMW9VTzEzTUFiQXZrdnFyd2REVE80WXZJdjlzMV9GNWlVOVUwYnciLCJmYW1pbHlfbmFtZSI6Ikd1cHRhIiwiZ2l2ZW5fbmFtZSI6IlZpY3RvciIsImFwcGlkIjoiNjEzYjVhZjgtZjJjMy00MWI2LWExZGMtNDE2Yzk3ODAzMGI3IiwiYXBwaWRhY3IiOiIwIiwic2NwIjoidXNlcl9pbXBlcnNvbmF0aW9uIiwiYWNyIjoiMSJ9.N_Kw1EhoVGrHbE6hOcm7ERdZ7paBQiNdObvp2c6T6n5CE8p0fZqmUd-ya_EqwElcD6SiKSiP7gj0gpNUnOJcBl_H2X8GseaeeMxBrZdsnDL8qecc6_ygHruwlPltnLTdka67s1Ow4fDSHaqhVTEk6lzGmNEcbNAyb0CxQxU6o7Fh0yHRiWoLsT8yqYk8nKzsHXfZBNby4aRo3_hXaa4i0SZLYfDGGYPdttG4vT_u54QGGd4Wzbonv2gjDlllOVGOwoJS6kfl1h8mk0qxdiIaT_ChbDWgkWvTB7bTvBE-EgHgV0XmAo0WtJeSxgjsG3KhhEPsONmqrSjhIUV4IVnF2w"; + var response = await SendAsync(server, "http://example.com/oauth", newBearerToken); + response.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task CustomHeaderReceived() + { + var server = CreateServer(options => + { + options.AutomaticAuthentication = true; + + options.Notifications.MessageReceived = notification => + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob") + }; + + notification.AuthenticationTicket = new AuthenticationTicket( + new ClaimsPrincipal(new ClaimsIdentity(claims, notification.Options.AuthenticationScheme)), + new AuthenticationProperties(), notification.Options.AuthenticationScheme); + + notification.HandleResponse(); + + return Task.FromResult(null); + }; + }); + + var response = await SendAsync(server, "http://example.com/oauth", "someHeader someblob"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.ResponseText.ShouldBe("Bob le Magnifique"); + } + + [Fact] + public async Task NoHeaderReceived() + { + var server = CreateServer(options => { }); + var response = await SendAsync(server, "http://example.com/oauth"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task HeaderWithoutBearerReceived() + { + var server = CreateServer(options => { }); + var response = await SendAsync(server, "http://example.com/oauth","Token"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task CustomTokenReceived() + { + var server = CreateServer(options => + { + options.AutomaticAuthentication = true; + + options.Notifications.SecurityTokenReceived = notification => + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob") + }; + + notification.AuthenticationTicket = new AuthenticationTicket( + new ClaimsPrincipal(new ClaimsIdentity(claims, notification.Options.AuthenticationScheme)), + new AuthenticationProperties(), notification.Options.AuthenticationScheme); + + notification.HandleResponse(); + + return Task.FromResult(null); + }; + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.ResponseText.ShouldBe("Bob le Magnifique"); + } + + [Fact] + public async Task CustomTokenValidated() + { + var server = CreateServer(options => + { + options.AutomaticAuthentication = true; + + options.Notifications.SecurityTokenValidated = notification => + { + // Retrieve the NameIdentifier claim from the identity + // returned by the custom security token validator. + var identity = (ClaimsIdentity) notification.AuthenticationTicket.Principal.Identity; + var identifier = identity.FindFirst(ClaimTypes.NameIdentifier); + + identifier.Value.ShouldBe("Bob le Tout Puissant"); + + // Remove the existing NameIdentifier claim and replace it + // with a new one containing a different value. + identity.RemoveClaim(identifier); + // Make sure to use a different name identifier + // than the one defined by BlobTokenValidator. + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique")); + + return Task.FromResult(null); + }; + + options.SecurityTokenValidators = new[] { new BlobTokenValidator(options.AuthenticationScheme) }; + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.ResponseText.ShouldBe("Bob le Magnifique"); + } + + [Fact] + public async Task RetrievingTokenFromAlternateLocation() + { + var server = CreateServer(options => + { + options.AutomaticAuthentication = true; + + options.Notifications.MessageReceived = notification => + { + notification.Token = "CustomToken"; + return Task.FromResult(null); + }; + + options.Notifications.SecurityTokenReceived = notification => + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob") + }; + + notification.AuthenticationTicket = new AuthenticationTicket( + new ClaimsPrincipal(new ClaimsIdentity(claims, notification.Options.AuthenticationScheme)), + new AuthenticationProperties(), notification.Options.AuthenticationScheme); + + notification.HandleResponse(); + + return Task.FromResult(null); + }; + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer Token"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.ResponseText.ShouldBe("Bob le Magnifique"); + } + + [Fact] + public async Task BearerTurns401To403IfAuthenticated() + { + var server = CreateServer(options => + { + options.Notifications.SecurityTokenReceived = notification => + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob") + }; + + notification.AuthenticationTicket = new AuthenticationTicket( + new ClaimsPrincipal(new ClaimsIdentity(claims, notification.Options.AuthenticationScheme)), + new AuthenticationProperties(), notification.Options.AuthenticationScheme); + + notification.HandleResponse(); + + return Task.FromResult(null); + }; + }); + + var response = await SendAsync(server, "http://example.com/unauthorized", "Bearer Token"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task BearerDoesNothingTo401IfNotAuthenticated() + { + var server = CreateServer(options => + { + options.Notifications.SecurityTokenReceived = notification => + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, "Bob le Magnifique"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob") + }; + + notification.AuthenticationTicket = new AuthenticationTicket( + new ClaimsPrincipal(new ClaimsIdentity(claims, notification.Options.AuthenticationScheme)), + new AuthenticationProperties(), notification.Options.AuthenticationScheme); + + notification.HandleResponse(); + + return Task.FromResult(null); + }; + }); + + var response = await SendAsync(server, "http://example.com/unauthorized"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + } + + class BlobTokenValidator : ISecurityTokenValidator + { + + public BlobTokenValidator(string authenticationScheme) + { + AuthenticationScheme = authenticationScheme; + } + + public string AuthenticationScheme { get; } + + public bool CanValidateToken => true; + + public int MaximumTokenSizeInBytes + { + get + { + throw new NotImplementedException(); + } + + set + { + throw new NotImplementedException(); + } + } + + public bool CanReadToken(string securityToken) => true; + + public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) + { + validatedToken = null; + + var claims = new[] + { + // Make sure to use a different name identifier + // than the one defined by CustomTokenValidated. + new Claim(ClaimTypes.NameIdentifier, "Bob le Tout Puissant"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob"), + }; + + return new ClaimsPrincipal(new ClaimsIdentity(claims, AuthenticationScheme)); + } + } + + private static TestServer CreateServer(Action configureOptions, Func handler = null) + { + return TestServer.Create(app => + { + if (configureOptions != null) + { + app.UseOAuthBearerAuthentication(configureOptions); + } + + app.Use(async (context, next) => + { + if (context.Request.Path == new PathString("/oauth")) + { + if (context.User == null || + context.User.Identity == null || + !context.User.Identity.IsAuthenticated) + { + context.Response.StatusCode = 401; + + return; + } + + var identifier = context.User.FindFirst(ClaimTypes.NameIdentifier); + if (identifier == null) + { + context.Response.StatusCode = 500; + + return; + } + + await context.Response.WriteAsync(identifier.Value); + } + + else if (context.Request.Path == new PathString("/unauthorized")) + { + // Simulate Authorization failure + var result = await context.Authentication.AuthenticateAsync(OAuthBearerAuthenticationDefaults.AuthenticationScheme); + context.Authentication.Challenge(OAuthBearerAuthenticationDefaults.AuthenticationScheme); + } + + else + { + await next(); + } + }); + }, + services => services.AddAuthentication()); + } + + // TODO: see if we can share the TestExtensions SendAsync method (only diff is auth header) + private static async Task SendAsync(TestServer server, string uri, string authorizationHeader = null) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(authorizationHeader)) + { + request.Headers.Add("Authorization", authorizationHeader); + } + + var transaction = new Transaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + + return transaction; + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs new file mode 100644 index 000000000..183ae6018 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectHandlerTests.cs @@ -0,0 +1,739 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +// this controls if the logs are written to the console. +// they can be reviewed for general content. +//#define _Verbose + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.Notifications; +using Microsoft.AspNet.Authentication.OpenIdConnect; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.DataProtection; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Http.Features.Authentication; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.WebEncoders; +using Microsoft.IdentityModel.Protocols; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect +{ + /// + /// These tests are designed to test OpenIdConnectAuthenticationHandler. + /// + public class OpenIdConnectHandlerTests + { + static List CompleteLogEntries; + static Dictionary LogEntries; + + static OpenIdConnectHandlerTests() + { + LogEntries = + new Dictionary() + { + { "OIDCH_0000:", LogLevel.Debug }, + { "OIDCH_0001:", LogLevel.Debug }, + { "OIDCH_0002:", LogLevel.Information }, + { "OIDCH_0003:", LogLevel.Information }, + { "OIDCH_0004:", LogLevel.Error }, + { "OIDCH_0005:", LogLevel.Error }, + { "OIDCH_0006:", LogLevel.Error }, + { "OIDCH_0007:", LogLevel.Error }, + { "OIDCH_0008:", LogLevel.Debug }, + { "OIDCH_0009:", LogLevel.Debug }, + { "OIDCH_0010:", LogLevel.Error }, + { "OIDCH_0011:", LogLevel.Error }, + { "OIDCH_0012:", LogLevel.Debug }, + { "OIDCH_0013:", LogLevel.Debug }, + { "OIDCH_0014:", LogLevel.Debug }, + { "OIDCH_0015:", LogLevel.Debug }, + { "OIDCH_0016:", LogLevel.Debug }, + { "OIDCH_0017:", LogLevel.Error }, + { "OIDCH_0018:", LogLevel.Debug }, + { "OIDCH_0019:", LogLevel.Debug }, + { "OIDCH_0020:", LogLevel.Debug }, + { "OIDCH_0026:", LogLevel.Error }, + }; + + BuildLogEntryList(); + } + + /// + /// Builds the complete list of log entries that are available in the runtime. + /// + private static void BuildLogEntryList() + { + CompleteLogEntries = new List(); + foreach (var entry in LogEntries) + { + CompleteLogEntries.Add(new LogEntry { State = entry.Key, Level = entry.Value }); + } + } + + /// + /// Sanity check that logging is filtering, hi / low water marks are checked + /// + [Fact] + public void LoggingLevel() + { + var logger = new CustomLogger(LogLevel.Debug); + logger.IsEnabled(LogLevel.Critical).ShouldBe(true); + logger.IsEnabled(LogLevel.Debug).ShouldBe(true); + logger.IsEnabled(LogLevel.Error).ShouldBe(true); + logger.IsEnabled(LogLevel.Information).ShouldBe(true); + logger.IsEnabled(LogLevel.Verbose).ShouldBe(true); + logger.IsEnabled(LogLevel.Warning).ShouldBe(true); + + logger = new CustomLogger(LogLevel.Critical); + logger.IsEnabled(LogLevel.Critical).ShouldBe(true); + logger.IsEnabled(LogLevel.Debug).ShouldBe(false); + logger.IsEnabled(LogLevel.Error).ShouldBe(false); + logger.IsEnabled(LogLevel.Information).ShouldBe(false); + logger.IsEnabled(LogLevel.Verbose).ShouldBe(false); + logger.IsEnabled(LogLevel.Warning).ShouldBe(false); + } + + /// + /// Test produces expected logs. + /// Each call to 'RunVariation' is configured with an and . + /// The list of expected log entries is checked and any errors reported. + /// captures the logs so they can be prepared. + /// + /// + [Fact] + public async Task AuthenticateCore() + { + //System.Diagnostics.Debugger.Launch(); + + var propertiesFormatter = new AuthenticationPropertiesFormater(); + var protectedProperties = propertiesFormatter.Protect(new AuthenticationProperties()); + var state = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + UrlEncoder.Default.UrlEncode(protectedProperties); + var code = Guid.NewGuid().ToString(); + var message = + new OpenIdConnectMessage + { + Code = code, + State = state, + }; + + var errors = new Dictionary>>(); + + var logsEntriesExpected = new int[] { 0, 1, 7, 14, 15 }; + await RunVariation(LogLevel.Debug, message, CodeReceivedHandledOptions, errors, logsEntriesExpected); + + logsEntriesExpected = new int[] { 0, 1, 7, 14, 16 }; + await RunVariation(LogLevel.Debug, message, CodeReceivedSkippedOptions, errors, logsEntriesExpected); + + logsEntriesExpected = new int[] { 0, 1, 7, 14 }; + await RunVariation(LogLevel.Debug, message, DefaultOptions, errors, logsEntriesExpected); + + // each message below should return before processing the idtoken + message.IdToken = "invalid_token"; + + logsEntriesExpected = new int[] { 0, 1, 2 }; + await RunVariation(LogLevel.Debug, message, MessageReceivedHandledOptions, errors, logsEntriesExpected); + + logsEntriesExpected = new int[]{ 2 }; + await RunVariation(LogLevel.Information, message, MessageReceivedHandledOptions, errors, logsEntriesExpected); + + logsEntriesExpected = new int[] { 0, 1, 3 }; + await RunVariation(LogLevel.Debug, message, MessageReceivedSkippedOptions, errors, logsEntriesExpected); + + logsEntriesExpected = new int[] { 3 }; + await RunVariation(LogLevel.Information, message, MessageReceivedSkippedOptions, errors, logsEntriesExpected); + + logsEntriesExpected = new int[] {0, 1, 7, 20, 8 }; + await RunVariation(LogLevel.Debug, message, SecurityTokenReceivedHandledOptions, errors, logsEntriesExpected); + + logsEntriesExpected = new int[] {0, 1, 7, 20, 9 }; + await RunVariation(LogLevel.Debug, message, SecurityTokenReceivedSkippedOptions, errors, logsEntriesExpected); + +#if _Verbose + Console.WriteLine("\n ===== \n"); + DisplayErrors(errors); +#endif + errors.Count.ShouldBe(0); + } + + /// + /// Tests that processes a messaage as expected. + /// The test runs two independant paths: Using and + /// + /// for this variation + /// the that has arrived + /// the delegate used for setting the options. + /// container for propogation of errors. + /// the expected log entries + /// a Task + private async Task RunVariation(LogLevel logLevel, OpenIdConnectMessage message, Action action, Dictionary>> errors, int[] logsEntriesExpected) + { + var expectedLogs = PopulateLogEntries(logsEntriesExpected); + string variation = action.Method.ToString().Substring(5, action.Method.ToString().IndexOf('(') - 5); +#if _Verbose + Console.WriteLine(Environment.NewLine + "=====" + Environment.NewLine + "Variation: " + variation + ", LogLevel: " + logLevel.ToString() + Environment.NewLine + Environment.NewLine + "Expected Logs: "); + DisplayLogs(expectedLogs); + Console.WriteLine(Environment.NewLine + "Logs using ConfigureOptions:"); +#endif + var form = new FormUrlEncodedContent(message.Parameters); + var loggerFactory = new CustomLoggerFactory(logLevel); + var server = CreateServer(new CustomConfigureOptions(action), loggerFactory); + await server.CreateClient().PostAsync("http://localhost", form); + CheckLogs(variation + ":ConfigOptions", loggerFactory.Logger.Logs, expectedLogs, errors); + +#if _Verbose + Console.WriteLine(Environment.NewLine + "Logs using IOptions:"); +#endif + form = new FormUrlEncodedContent(message.Parameters); + loggerFactory = new CustomLoggerFactory(logLevel); + server = CreateServer(new Options(action), loggerFactory); + await server.CreateClient().PostAsync("http://localhost", form); + CheckLogs(variation + ":IOptions", loggerFactory.Logger.Logs, expectedLogs, errors); + } + + /// + /// Populates a list of expected log entries for a test variation. + /// + /// the index for the in CompleteLogEntries of interest. + /// a that represents the expected entries for a test variation. + private List PopulateLogEntries(int[] items) + { + var entries = new List(); + foreach(var item in items) + { + entries.Add(CompleteLogEntries[item]); + } + + return entries; + } + + private void DisplayLogs(List logs) + { + foreach (var logentry in logs) + { + Console.WriteLine(logentry.ToString()); + } + } + + private void DisplayErrors(Dictionary>> errors) + { + if (errors.Count > 0) + { + foreach (var error in errors) + { + Console.WriteLine("Error in Variation: " + error.Key); + foreach (var logError in error.Value) + { + Console.WriteLine("*Captured*, *Expected* : *" + (logError.Item1?.ToString() ?? "null") + "*, *" + (logError.Item2?.ToString() ?? "null") + "*"); + } + Console.WriteLine(Environment.NewLine); + } + } + } + + /// + /// Adds to errors if a variation if any are found. + /// + /// if this has been seen before, errors will be appended, test results are easier to understand if this is unique. + /// these are the logs the runtime generated + /// these are the errors that were expected + /// the dictionary to record any errors + private void CheckLogs(string variation, List capturedLogs, List expectedLogs, Dictionary>> errors) + { + var localErrors = new List>(); + + if (capturedLogs.Count >= expectedLogs.Count) + { + for (int i = 0; i < capturedLogs.Count; i++) + { + if (i + 1 > expectedLogs.Count) + { + localErrors.Add(new Tuple(capturedLogs[i], null)); + } + else + { + if (!TestUtilities.AreEqual(capturedLogs[i], expectedLogs[i])) + { + localErrors.Add(new Tuple(capturedLogs[i], expectedLogs[i])); + } + } + } + } + else + { + for (int i = 0; i < expectedLogs.Count; i++) + { + if (i + 1 > capturedLogs.Count) + { + localErrors.Add(new Tuple(null, expectedLogs[i])); + } + else + { + if (!TestUtilities.AreEqual(expectedLogs[i], capturedLogs[i])) + { + localErrors.Add(new Tuple(capturedLogs[i], expectedLogs[i])); + } + } + } + } + + if (localErrors.Count != 0) + { + if (errors.ContainsKey(variation)) + { + foreach (var error in localErrors) + { + errors[variation].Add(error); + } + } + else + { + errors[variation] = localErrors; + } + } + } + + #region Configure Options + + private static void CodeReceivedHandledOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + AuthorizationCodeReceived = (notification) => + { + notification.HandleResponse(); + return Task.FromResult(null); + } + }; + } + + private static void CodeReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + AuthorizationCodeReceived = (notification) => + { + notification.SkipToNextMiddleware(); + return Task.FromResult(null); + } + }; + } + + private static void DefaultOptions(OpenIdConnectAuthenticationOptions options) + { + options.AuthenticationScheme = "OpenIdConnectHandlerTest"; + options.ConfigurationManager = ConfigurationManager.DefaultStaticConfigurationManager; + options.StateDataFormat = new AuthenticationPropertiesFormater(); + } + + private static void MessageReceivedHandledOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + MessageReceived = (notification) => + { + notification.HandleResponse(); + return Task.FromResult(null); + } + }; + } + + private static void MessageReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + MessageReceived = (notification) => + { + notification.SkipToNextMiddleware(); + return Task.FromResult(null); + } + }; + } + + private static void SecurityTokenReceivedHandledOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + SecurityTokenReceived = (notification) => + { + notification.HandleResponse(); + return Task.FromResult(null); + } + }; + } + + private static void SecurityTokenReceivedSkippedOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + SecurityTokenReceived = (notification) => + { + notification.SkipToNextMiddleware(); + return Task.FromResult(null); + } + }; + } + + private static void SecurityTokenValidatedHandledOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + SecurityTokenValidated = (notification) => + { + notification.HandleResponse(); + return Task.FromResult(null); + } + }; + } + + private static void SecurityTokenValidatedSkippedOptions(OpenIdConnectAuthenticationOptions options) + { + DefaultOptions(options); + options.Notifications = + new OpenIdConnectAuthenticationNotifications + { + SecurityTokenValidated = (notification) => + { + notification.SkipToNextMiddleware(); + return Task.FromResult(null); + } + }; + } + + #endregion + + private static TestServer CreateServer(IOptions options, ILoggerFactory loggerFactory) + { + return TestServer.Create( + app => + { + app.UseCustomOpenIdConnectAuthentication(options, loggerFactory); + app.Use(async (context, next) => + { + await next(); + }); + }, + services => + { + services.AddWebEncoders(); + services.AddDataProtection(); + } + ); + } + + private static TestServer CreateServer(CustomConfigureOptions configureOptions, ILoggerFactory loggerFactory) + { + return TestServer.Create( + app => + { + app.UseCustomOpenIdConnectAuthentication(configureOptions, loggerFactory); + app.Use(async (context, next) => + { + await next(); + }); + }, + services => + { + services.AddWebEncoders(); + services.AddDataProtection(); + } + ); + } + } + + /// + /// Extension specifies as the middleware. + /// + public static class OpenIdConnectAuthenticationExtensions + { + /// + /// Adds the into the ASP.NET runtime. + /// + /// The application builder + /// Options which control the processing of the OpenIdConnect protocol and token validation. + /// custom loggerFactory + /// The application builder + public static IApplicationBuilder UseCustomOpenIdConnectAuthentication(this IApplicationBuilder app, CustomConfigureOptions customConfigureOption, ILoggerFactory loggerFactory) + { + return app.UseMiddleware(customConfigureOption, loggerFactory); + } + + /// + /// Adds the into the ASP.NET runtime. + /// + /// The application builder + /// Options which control the processing of the OpenIdConnect protocol and token validation. + /// custom loggerFactory + /// The application builder + public static IApplicationBuilder UseCustomOpenIdConnectAuthentication(this IApplicationBuilder app, IOptions options, ILoggerFactory loggerFactory) + { + return app.UseMiddleware(options, loggerFactory); + } + } + + /// + /// Provides a Facade over IOptions + /// + public class Options : IOptions + { + OpenIdConnectAuthenticationOptions _options; + + public Options(Action action) + { + _options = new OpenIdConnectAuthenticationOptions(); + action(_options); + } + + OpenIdConnectAuthenticationOptions IOptions.Options + { + get + { + return _options; + } + } + + /// + /// For now returns _options + /// + /// configuration to return + /// + public OpenIdConnectAuthenticationOptions GetNamedOptions(string name) + { + return _options; + } + } + + public class CustomConfigureOptions : ConfigureOptions + { + public CustomConfigureOptions(Action action) + : base(action) + { + } + + public override void Configure(OpenIdConnectAuthenticationOptions options, string name = "") + { + base.Configure(options, name); + return; + } + } + + /// + /// Used to control which methods are handled + /// + public class CustomOpenIdConnectAuthenticationHandler : OpenIdConnectAuthenticationHandler + { + public async Task BaseInitializeAsyncPublic(AuthenticationOptions options, HttpContext context, ILogger logger, IUrlEncoder encoder) + { + await base.BaseInitializeAsync(options, context, logger, encoder); + } + + public override bool ShouldHandleScheme(string authenticationScheme) + { + return true; + } + + public override void Challenge(ChallengeContext context) + { + } + + protected override void ApplyResponseChallenge() + { + } + + protected override async Task ApplyResponseChallengeAsync() + { + var redirectToIdentityProviderNotification = new RedirectToIdentityProviderNotification(Context, Options) + { + }; + + await Options.Notifications.RedirectToIdentityProvider(redirectToIdentityProviderNotification); + } + } + + /// + /// Used to set as the AuthenticationHandler + /// which can be configured to handle certain messages. + /// + public class CustomOpenIdConnectAuthenticationMiddleware : OpenIdConnectAuthenticationMiddleware + { + public CustomOpenIdConnectAuthenticationMiddleware( + RequestDelegate next, + IDataProtectionProvider dataProtectionProvider, + ILoggerFactory loggerFactory, + IUrlEncoder encoder, + IOptions externalOptions, + IOptions options, + ConfigureOptions configureOptions = null + ) + : base(next, dataProtectionProvider, loggerFactory, encoder, externalOptions, options, configureOptions) + { + Logger = (loggerFactory as CustomLoggerFactory).Logger; + } + + protected override AuthenticationHandler CreateHandler() + { + return new CustomOpenIdConnectAuthenticationHandler(); + } + } + + public class LogEntry + { + public LogEntry() { } + + public int EventId { get; set; } + + public Exception Exception { get; set; } + + public Func Formatter { get; set; } + + public LogLevel Level { get; set; } + + public object State { get; set; } + + public override string ToString() + { + if (Formatter != null) + { + return Formatter(this.State, this.Exception); + } + else + { + string message = (Formatter != null ? Formatter(State, Exception) : (State?.ToString() ?? "null")); + message += ", LogLevel: " + Level.ToString(); + message += ", EventId: " + EventId.ToString(); + message += ", Exception: " + (Exception == null ? "null" : Exception.Message); + return message; + } + } + } + + public class CustomLogger : ILogger, IDisposable + { + LogLevel _logLevel = 0; + + public CustomLogger(LogLevel logLevel = LogLevel.Debug) + { + _logLevel = logLevel; + } + + List logEntries = new List(); + + public IDisposable BeginScopeImpl(object state) + { + return this; + } + + public void Dispose() + { + } + + public bool IsEnabled(LogLevel logLevel) + { + return (logLevel >= _logLevel); + } + + public void Log(LogLevel logLevel, int eventId, object state, Exception exception, Func formatter) + { + if (IsEnabled(logLevel)) + { + logEntries.Add( + new LogEntry + { + EventId = eventId, + Exception = exception, + Formatter = formatter, + Level = logLevel, + State = state, + }); + +#if _Verbose + Console.WriteLine(state?.ToString() ?? "state null"); +#endif + } + } + + public List Logs { get { return logEntries; } } + } + + public class CustomLoggerFactory : ILoggerFactory + { + CustomLogger _logger; + LogLevel _logLevel = LogLevel.Debug; + + public CustomLoggerFactory(LogLevel logLevel) + { + _logLevel = logLevel; + _logger = new CustomLogger(_logLevel); + } + + public LogLevel MinimumLevel + { + get { return _logLevel; } + set {_logLevel = value; } + } + + public void AddProvider(ILoggerProvider provider) + { + } + + public ILogger CreateLogger(string categoryName) + { + return _logger; + } + + public CustomLogger Logger { get { return _logger; } } + } + + /// + /// Processing a requires 'unprotecting' the state. + /// This class side-steps that process. + /// + public class AuthenticationPropertiesFormater : ISecureDataFormat + { + public string Protect(AuthenticationProperties data) + { + return "protectedData"; + } + + AuthenticationProperties ISecureDataFormat.Unprotect(string protectedText) + { + return new AuthenticationProperties(); + } + } + + /// + /// Used to set up different configurations of metadata for different tests + /// + public class ConfigurationManager + { + /// + /// Simple static empty manager. + /// + static public IConfigurationManager DefaultStaticConfigurationManager + { + get { return new StaticConfigurationManager(new OpenIdConnectConfiguration()); } + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs new file mode 100644 index 000000000..32aa028f0 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs @@ -0,0 +1,382 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Microsoft.AspNet.Authentication.Cookies; +using Microsoft.AspNet.Authentication.DataHandler; +using Microsoft.AspNet.Authentication.OpenIdConnect; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.DataProtection; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.WebEncoders; +using Newtonsoft.Json; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect +{ + public class OpenIdConnectMiddlewareTests + { + static string noncePrefix = "OpenIdConnect." + "Nonce."; + static string nonceDelimiter = "."; + + [Fact] + public async Task ChallengeWillTriggerRedirect() + { + var server = CreateServer(options => + { + options.Authority = "https://login.windows.net/common"; + options.ClientId = "Test Id"; + options.SignInScheme = OpenIdConnectAuthenticationDefaults.AuthenticationScheme; + }); + var transaction = await SendAsync(server, "https://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var location = transaction.Response.Headers.Location.ToString(); + location.ShouldContain("https://login.windows.net/common/oauth2/authorize?"); + location.ShouldContain("client_id="); + location.ShouldContain("&response_type="); + location.ShouldContain("&scope="); + location.ShouldContain("&state="); + location.ShouldContain("&response_mode="); + } + + [Fact] + public async Task ChallengeWillSetNonceCookie() + { + var server = CreateServer(options => + { + options.Authority = "https://login.windows.net/common"; + options.ClientId = "Test Id"; + }); + var transaction = await SendAsync(server, "https://example.com/challenge"); + transaction.SetCookie.Single().ShouldContain("OpenIdConnect.nonce."); + } + + [Fact] + public async Task ChallengeWillSetDefaultScope() + { + var server = CreateServer(options => + { + options.Authority = "https://login.windows.net/common"; + options.ClientId = "Test Id"; + }); + var transaction = await SendAsync(server, "https://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + transaction.Response.Headers.Location.Query.ShouldContain("&scope=" + UrlEncoder.Default.UrlEncode("openid profile")); + } + + [Fact] + public async Task ChallengeWillUseOptionsProperties() + { + var server = CreateServer(options => + { + options.Authority = "https://login.windows.net/common"; + options.ClientId = "Test Id"; + options.SignInScheme = OpenIdConnectAuthenticationDefaults.AuthenticationScheme; + options.Scope = "https://www.googleapis.com/auth/plus.login"; + options.ResponseType = "id_token"; + }); + var transaction = await SendAsync(server, "https://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var query = transaction.Response.Headers.Location.Query; + query.ShouldContain("scope=" + UrlEncoder.Default.UrlEncode("https://www.googleapis.com/auth/plus.login")); + query.ShouldContain("response_type=" + UrlEncoder.Default.UrlEncode("id_token")); + } + + [Fact] + public async Task ChallengeWillUseNotifications() + { + ISecureDataFormat stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); + var server = CreateServer(options => + { + options.Authority = "https://login.windows.net/common"; + options.ClientId = "Test Id"; + options.Notifications = new OpenIdConnectAuthenticationNotifications + { + MessageReceived = notification => + { + notification.ProtocolMessage.Scope = "test openid profile"; + notification.HandleResponse(); + return Task.FromResult(null); + } + }; + }); + + var properties = new AuthenticationProperties(); + var state = stateFormat.Protect(properties); + var transaction = await SendAsync(server,"https://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + } + + + [Fact] + public async Task SignOutWithDefaultRedirectUri() + { + var server = CreateServer(options => + { + options.Authority = "https://login.windows.net/common"; + options.ClientId = "Test Id"; + }); + + var transaction = await SendAsync(server, "https://example.com/signout"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + transaction.Response.Headers.Location.AbsoluteUri.ShouldBe("https://login.windows.net/common/oauth2/logout"); + } + + [Fact] + public async Task SignOutWithCustomRedirectUri() + { + var server = CreateServer(options => + { + options.Authority = "https://login.windows.net/common"; + options.ClientId = "Test Id"; + options.PostLogoutRedirectUri = "https://example.com/logout"; + }); + + var transaction = await SendAsync(server, "https://example.com/signout"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + transaction.Response.Headers.Location.AbsoluteUri.ShouldContain(UrlEncoder.Default.UrlEncode("https://example.com/logout")); + } + + [Fact] + public async Task SignOutWith_Specific_RedirectUri_From_Authentication_Properites() + { + var server = CreateServer(options => + { + options.Authority = "https://login.windows.net/common"; + options.ClientId = "Test Id"; + options.PostLogoutRedirectUri = "https://example.com/logout"; + }); + + var transaction = await SendAsync(server, "https://example.com/signout_with_specific_redirect_uri"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + transaction.Response.Headers.Location.AbsoluteUri.ShouldContain(UrlEncoder.Default.UrlEncode("http://www.example.com/specific_redirect_uri")); + } + + [Fact] + // Test Cases for calculating the expiration time of cookie from cookie name + public void NonceCookieExpirationTime() + { + DateTime utcNow = DateTime.UtcNow; + + GetNonceExpirationTime(noncePrefix + DateTime.MaxValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(DateTime.MaxValue); + + GetNonceExpirationTime(noncePrefix + DateTime.MinValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue + TimeSpan.FromHours(1)); + + GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(utcNow + TimeSpan.FromHours(1)); + + GetNonceExpirationTime(noncePrefix, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); + + GetNonceExpirationTime("", TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); + + GetNonceExpirationTime(noncePrefix + noncePrefix, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); + + GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(utcNow + TimeSpan.FromHours(1)); + + GetNonceExpirationTime(utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); + } + + private static TestServer CreateServer(Action configureOptions, Func handler = null) + { + return TestServer.Create(app => + { + app.UseCookieAuthentication(options => + { + options.AuthenticationScheme = "OpenIdConnect"; + }); + app.UseOpenIdConnectAuthentication(configureOptions); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge")) + { + context.Authentication.Challenge("OpenIdConnect"); + res.StatusCode = 401; + } + else if (req.Path == new PathString("/signin")) + { + // REVIEW: this used to just be res.SignIn() + context.Authentication.SignIn("OpenIdConnect", new ClaimsPrincipal()); + } + else if (req.Path == new PathString("/signout")) + { + context.Authentication.SignOut(OpenIdConnectAuthenticationDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString("/signout_with_specific_redirect_uri")) + { + context.Authentication.SignOut( + OpenIdConnectAuthenticationDefaults.AuthenticationScheme, + new AuthenticationProperties() { RedirectUri = "http://www.example.com/specific_redirect_uri" }); + } + else if (handler != null) + { + await handler(context); + } + else + { + await next(); + } + }); + }, + services => + { + services.AddAuthentication(); + services.Configure(options => + { + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }); + }); + } + + private static async Task SendAsync(TestServer server, string uri, string cookieHeader = null) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(cookieHeader)) + { + request.Headers.Add("Cookie", cookieHeader); + } + var transaction = new Transaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); + } + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + return transaction; + } + + private class Transaction + { + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage Response { get; set; } + + public IList SetCookie { get; set; } + + public string ResponseText { get; set; } + public XElement ResponseElement { get; set; } + + public string AuthenticationCookieValue + { + get + { + if (SetCookie != null && SetCookie.Count > 0) + { + var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNet.Cookie=")); + if (authCookie != null) + { + return authCookie.Substring(0, authCookie.IndexOf(';')); + } + } + + return null; + } + } + + public string FindClaimValue(string claimType) + { + XElement claim = ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + } + private static void Describe(HttpResponse res, ClaimsIdentity identity) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (identity != null) + { + xml.Add(identity.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value)))); + } + using (var memory = new MemoryStream()) + { + using (var writer = new XmlTextWriter(memory, Encoding.UTF8)) + { + xml.WriteTo(writer); + } + res.Body.Write(memory.ToArray(), 0, memory.ToArray().Length); + } + } + + private class TestHttpMessageHandler : HttpMessageHandler + { + public Func Sender { get; set; } + + protected override Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) + { + if (Sender != null) + { + return Task.FromResult(Sender(request)); + } + + return Task.FromResult(null); + } + } + + private static HttpResponseMessage ReturnJsonResponse(object content) + { + var res = new HttpResponseMessage(HttpStatusCode.OK); + var text = JsonConvert.SerializeObject(content); + res.Content = new StringContent(text, Encoding.UTF8, "application/json"); + return res; + } + + private static DateTime GetNonceExpirationTime(string keyname, TimeSpan nonceLifetime) + { + DateTime nonceTime = DateTime.MinValue; + string timestamp = null; + int endOfTimestamp; + if (keyname.StartsWith(noncePrefix, StringComparison.Ordinal)) + { + timestamp = keyname.Substring(noncePrefix.Length); + endOfTimestamp = timestamp.IndexOf('.'); + + if (endOfTimestamp != -1) + { + timestamp = timestamp.Substring(0, endOfTimestamp); + try + { + nonceTime = DateTime.FromBinary(Convert.ToInt64(timestamp, CultureInfo.InvariantCulture)); + if ((nonceTime >= DateTime.UtcNow) && ((DateTime.MaxValue - nonceTime) < nonceLifetime)) + nonceTime = DateTime.MaxValue; + else + nonceTime += nonceLifetime; + } + catch + { + } + } + } + return nonceTime; + } + + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/TestUtilities.cs b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/TestUtilities.cs new file mode 100644 index 000000000..1e6bea793 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/OpenIdConnect/TestUtilities.cs @@ -0,0 +1,108 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect +{ + /// + /// These utilities are designed to test openidconnect related flows + /// + public class TestUtilities + { + public static bool AreEqual(object obj1, object obj2, Func comparer = null) where T : class + { + if (obj1 == null && obj2 == null) + { + return true; + } + + if (obj1 == null || obj2 == null) + { + return false; + } + + if (obj1.GetType() != obj2.GetType()) + { + return false; + } + + if (obj1.GetType() != typeof(T)) + { + return false; + } + + if (comparer != null) + { + return comparer(obj1, obj2); + } + + if (typeof(T) == typeof(LogEntry)) + { + return AreEqual(obj1 as LogEntry, obj2 as LogEntry); + } + else if (typeof(T) == typeof(Exception)) + { + return AreEqual(obj1 as Exception, obj2 as Exception); + } + + throw new ArithmeticException("Unknown type, no comparer. Type: " + typeof(T).ToString()); + + } + + /// + /// Never call this method directly, call AreObjectsEqual, as it deals with nulls and types"/> + /// + /// + /// + /// + private static bool AreEqual(LogEntry logEntry1, LogEntry logEntry2) + { + if (logEntry1.EventId != logEntry2.EventId) + { + return false; + } + + if (!AreEqual(logEntry1.Exception, logEntry2.Exception)) + { + return false; + } + + if (logEntry1.State == null && logEntry2.State == null) + { + return true; + } + + if (logEntry1.State == null) + { + return false; + } + + if (logEntry2.State == null) + { + return false; + } + + string logValue1 = logEntry1.Formatter == null ? logEntry1.State.ToString() : logEntry1.Formatter(logEntry1.State, logEntry1.Exception); + string logValue2 = logEntry2.Formatter == null ? logEntry2.State.ToString() : logEntry2.Formatter(logEntry2.State, logEntry2.Exception); + + return (logValue1.StartsWith(logValue2) || (logValue2.StartsWith(logValue1))); + } + + /// + /// Never call this method directly, call AreObjectsEqual, as it deals with nulls and types"/> + /// + /// + /// + /// + private static bool AreEqual(Exception exception1, Exception exception2) + { + if (!string.Equals(exception1.Message, exception2.Message)) + { + return false; + } + + return AreEqual(exception1.InnerException, exception2.InnerException); + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/SecurityHelperTests.cs b/test/Microsoft.AspNet.Authentication.Test/SecurityHelperTests.cs new file mode 100644 index 000000000..a02283ab7 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/SecurityHelperTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; +using Microsoft.AspNet.Http.Internal; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Authentication +{ + public class SecurityHelperTests + { + [Fact] + public void AddingToAnonymousIdentityDoesNotKeepAnonymousIdentity() + { + var context = new DefaultHttpContext(); + context.User.ShouldNotBe(null); + context.User.Identity.IsAuthenticated.ShouldBe(false); + + SecurityHelper.AddUserPrincipal(context, new GenericPrincipal(new GenericIdentity("Test1", "Alpha"), new string[0])); + + context.User.ShouldNotBe(null); + context.User.Identity.AuthenticationType.ShouldBe("Alpha"); + context.User.Identity.Name.ShouldBe("Test1"); + + context.User.ShouldBeTypeOf(); + context.User.Identity.ShouldBeTypeOf(); + + ((ClaimsPrincipal)context.User).Identities.Count().ShouldBe(1); + } + + [Fact] + public void AddingExistingIdentityChangesDefaultButPreservesPrior() + { + var context = new DefaultHttpContext(); + context.User = new GenericPrincipal(new GenericIdentity("Test1", "Alpha"), null); + + context.User.Identity.AuthenticationType.ShouldBe("Alpha"); + context.User.Identity.Name.ShouldBe("Test1"); + + SecurityHelper.AddUserPrincipal(context, new GenericPrincipal(new GenericIdentity("Test2", "Beta"), new string[0])); + + context.User.Identity.AuthenticationType.ShouldBe("Beta"); + context.User.Identity.Name.ShouldBe("Test2"); + + SecurityHelper.AddUserPrincipal(context, new GenericPrincipal(new GenericIdentity("Test3", "Gamma"), new string[0])); + + context.User.Identity.AuthenticationType.ShouldBe("Gamma"); + context.User.Identity.Name.ShouldBe("Test3"); + + var principal = context.User; + principal.Identities.Count().ShouldBe(3); + principal.Identities.Skip(0).First().Name.ShouldBe("Test3"); + principal.Identities.Skip(1).First().Name.ShouldBe("Test2"); + principal.Identities.Skip(2).First().Name.ShouldBe("Test1"); + } + + [Fact] + public void AddingPreservesNewIdentitiesAndDropsEmpty() + { + var context = new DefaultHttpContext(); + var existingPrincipal = new ClaimsPrincipal(new ClaimsIdentity()); + var identityNoAuthTypeWithClaim = new ClaimsIdentity(); + identityNoAuthTypeWithClaim.AddClaim(new Claim("identityNoAuthTypeWithClaim", "yes")); + existingPrincipal.AddIdentity(identityNoAuthTypeWithClaim); + var identityEmptyWithAuthType = new ClaimsIdentity("empty"); + existingPrincipal.AddIdentity(identityEmptyWithAuthType); + context.User = existingPrincipal; + + context.User.Identity.IsAuthenticated.ShouldBe(false); + + var newPrincipal = new ClaimsPrincipal(); + var newEmptyIdentity = new ClaimsIdentity(); + var identityTwo = new ClaimsIdentity("yep"); + newPrincipal.AddIdentity(newEmptyIdentity); + newPrincipal.AddIdentity(identityTwo); + + SecurityHelper.AddUserPrincipal(context, newPrincipal); + + // Preserve newPrincipal order + context.User.Identity.IsAuthenticated.ShouldBe(false); + context.User.Identity.Name.ShouldBe(null); + + var principal = context.User; + principal.Identities.Count().ShouldBe(4); + principal.Identities.Skip(0).First().ShouldBe(newEmptyIdentity); + principal.Identities.Skip(1).First().ShouldBe(identityTwo); + principal.Identities.Skip(2).First().ShouldBe(identityNoAuthTypeWithClaim); + principal.Identities.Skip(3).First().ShouldBe(identityEmptyWithAuthType); + + // This merge should drop newEmptyIdentity since its empty + SecurityHelper.AddUserPrincipal(context, new GenericPrincipal(new GenericIdentity("Test3", "Gamma"), new string[0])); + + context.User.Identity.AuthenticationType.ShouldBe("Gamma"); + context.User.Identity.Name.ShouldBe("Test3"); + + principal = context.User; + principal.Identities.Count().ShouldBe(4); + principal.Identities.Skip(0).First().Name.ShouldBe("Test3"); + principal.Identities.Skip(1).First().ShouldBe(identityTwo); + principal.Identities.Skip(2).First().ShouldBe(identityNoAuthTypeWithClaim); + principal.Identities.Skip(3).First().ShouldBe(identityEmptyWithAuthType); + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/TestClock.cs b/test/Microsoft.AspNet.Authentication.Test/TestClock.cs new file mode 100644 index 000000000..20495125d --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/TestClock.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Authentication; + +namespace Microsoft.AspNet.Authentication +{ + public class TestClock : ISystemClock + { + public TestClock() + { + UtcNow = new DateTimeOffset(2013, 6, 11, 12, 34, 56, 789, TimeSpan.Zero); + } + + public DateTimeOffset UtcNow { get; set; } + + public void Add(TimeSpan timeSpan) + { + UtcNow = UtcNow + timeSpan; + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/TestExtensions.cs b/test/Microsoft.AspNet.Authentication.Test/TestExtensions.cs new file mode 100644 index 000000000..b406f5813 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/TestExtensions.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.TestHost; + +namespace Microsoft.AspNet.Authentication +{ + public static class TestExtensions + { + public const string CookieAuthenticationScheme = "External"; + + public static async Task SendAsync(this TestServer server, string uri, string cookieHeader = null) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(cookieHeader)) + { + request.Headers.Add("Cookie", cookieHeader); + } + var transaction = new Transaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); + } + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + return transaction; + } + + public static void Describe(this HttpResponse res, ClaimsPrincipal principal) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (principal != null) + { + foreach (var identity in principal.Identities) + { + xml.Add(identity.Claims.Select(claim => + new XElement("claim", new XAttribute("type", claim.Type), + new XAttribute("value", claim.Value), + new XAttribute("issuer", claim.Issuer)))); + } + } + using (var memory = new MemoryStream()) + { + using (var writer = new XmlTextWriter(memory, Encoding.UTF8)) + { + xml.WriteTo(writer); + } + res.Body.Write(memory.ToArray(), 0, memory.ToArray().Length); + } + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/TestHttpMessageHandler.cs b/test/Microsoft.AspNet.Authentication.Test/TestHttpMessageHandler.cs new file mode 100644 index 000000000..1a93b16df --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/TestHttpMessageHandler.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Authentication +{ + public class TestHttpMessageHandler : HttpMessageHandler + { + public Func Sender { get; set; } + + protected override Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) + { + if (Sender != null) + { + return Task.FromResult(Sender(request)); + } + + return Task.FromResult(null); + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/Transaction.cs b/test/Microsoft.AspNet.Authentication.Test/Transaction.cs new file mode 100644 index 000000000..e32c3b926 --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/Transaction.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.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Xml.Linq; + +namespace Microsoft.AspNet.Authentication +{ + public class Transaction + { + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage Response { get; set; } + + public IList SetCookie { get; set; } + + public string ResponseText { get; set; } + public XElement ResponseElement { get; set; } + + public string AuthenticationCookieValue + { + get + { + if (SetCookie != null && SetCookie.Count > 0) + { + var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNet." + TestExtensions.CookieAuthenticationScheme + "=")); + if (authCookie != null) + { + return authCookie.Substring(0, authCookie.IndexOf(';')); + } + } + + return null; + } + } + + public string FindClaimValue(string claimType, string issuer = null) + { + var claim = ResponseElement.Elements("claim") + .SingleOrDefault(elt => elt.Attribute("type").Value == claimType && + (issuer == null || elt.Attribute("issuer").Value == issuer)); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/Twitter/TwitterMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Twitter/TwitterMiddlewareTests.cs new file mode 100644 index 000000000..abbe3d37f --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/Twitter/TwitterMiddlewareTests.cs @@ -0,0 +1,132 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Authentication.Twitter +{ + public class TwitterMiddlewareTests + { + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var server = CreateServer( + app => app.UseTwitterAuthentication(options => + { + options.ConsumerKey = "Test Consumer Key"; + options.ConsumerSecret = "Test Consumer Secret"; + options.Notifications = new TwitterAuthenticationNotifications + { + OnApplyRedirect = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + } + }; + options.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://api.twitter.com/oauth/request_token") + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = + new StringContent("oauth_callback_confirmed=true&oauth_token=test_oauth_token&oauth_token_secret=test_oauth_token_secret", + Encoding.UTF8, + "application/x-www-form-urlencoded") + }; + } + return null; + } + }; + options.BackchannelCertificateValidator = null; + }), + context => + { + context.Authentication.Challenge("Twitter"); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var query = transaction.Response.Headers.Location.Query; + query.ShouldContain("custom=test"); + } + + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer( + app => app.UseTwitterAuthentication(options => + { + options.ConsumerKey = "Test Consumer Key"; + options.ConsumerSecret = "Test Consumer Secret"; + options.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://api.twitter.com/oauth/request_token") + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = + new StringContent("oauth_callback_confirmed=true&oauth_token=test_oauth_token&oauth_token_secret=test_oauth_token_secret", + Encoding.UTF8, + "application/x-www-form-urlencoded") + }; + } + return null; + } + }; + options.BackchannelCertificateValidator = null; + }), + context => + { + context.Authentication.Challenge("Twitter"); + return true; + }); + var transaction = await server.SendAsync("http://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var location = transaction.Response.Headers.Location.AbsoluteUri; + location.ShouldContain("https://twitter.com/oauth/authenticate?oauth_token="); + } + + private static TestServer CreateServer(Action configure, Func handler) + { + return TestServer.Create(app => + { + app.UseCookieAuthentication(options => + { + options.AuthenticationScheme = "External"; + }); + if (configure != null) + { + configure(app); + } + app.Use(async (context, next) => + { + if (handler == null || !handler(context)) + { + await next(); + } + }); + }, + services => + { + services.AddAuthentication(); + services.Configure(options => + { + options.SignInScheme = "External"; + }); + }); + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/katanatest.redmond.corp.microsoft.com.cer b/test/Microsoft.AspNet.Authentication.Test/katanatest.redmond.corp.microsoft.com.cer new file mode 100644 index 000000000..bfd5220e2 Binary files /dev/null and b/test/Microsoft.AspNet.Authentication.Test/katanatest.redmond.corp.microsoft.com.cer differ diff --git a/test/Microsoft.AspNet.Authentication.Test/project.json b/test/Microsoft.AspNet.Authentication.Test/project.json new file mode 100644 index 000000000..c7f594c4f --- /dev/null +++ b/test/Microsoft.AspNet.Authentication.Test/project.json @@ -0,0 +1,28 @@ +{ + "compilationOptions": { + "warningsAsErrors": "true" + }, + "dependencies": { + "Microsoft.AspNet.Authentication.Cookies": "1.0.0-*", + "Microsoft.AspNet.Authentication.Facebook": "1.0.0-*", + "Microsoft.AspNet.Authentication.Google": "1.0.0-*", + "Microsoft.AspNet.Authentication.MicrosoftAccount": "1.0.0-*", + "Microsoft.AspNet.Authentication.OAuthBearer": "1.0.0-*", + "Microsoft.AspNet.Authentication.OpenIdConnect": "1.0.0-*", + "Microsoft.AspNet.Authentication.Twitter": "1.0.0-*", + "Microsoft.AspNet.DataProtection": "1.0.0-*", + "Microsoft.AspNet.TestHost": "1.0.0-*", + "Moq": "4.2.1312.1622", + "xunit.runner.aspnet": "2.0.0-aspnet-*" + }, + "commands": { + "test": "xunit.runner.aspnet" + }, + "frameworks": { + "dnx451": { + "dependencies": { + "Shouldly": "1.1.1.1" + } + } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/selfSigned.cer b/test/Microsoft.AspNet.Authentication.Test/selfSigned.cer new file mode 100644 index 000000000..6acc7af5a Binary files /dev/null and b/test/Microsoft.AspNet.Authentication.Test/selfSigned.cer differ diff --git a/test/Microsoft.AspNet.Authorization.Test/AuthorizationPolicyFacts.cs b/test/Microsoft.AspNet.Authorization.Test/AuthorizationPolicyFacts.cs new file mode 100644 index 000000000..208c53b1c --- /dev/null +++ b/test/Microsoft.AspNet.Authorization.Test/AuthorizationPolicyFacts.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.AspNet.Authorization; +using Xunit; + +namespace Microsoft.AspNet.Authroization.Test +{ + public class AuthorizationPolicyFacts + { + [Fact] + public void RequireRoleThrowsIfEmpty() + { + Assert.Throws(() => new AuthorizationPolicyBuilder().RequireRole()); + } + + [Fact] + public void CanCombineAuthorizeAttributes() + { + // Arrange + var attributes = new AuthorizeAttribute[] { + new AuthorizeAttribute(), + new AuthorizeAttribute("1") { ActiveAuthenticationSchemes = "dupe" }, + new AuthorizeAttribute("2") { ActiveAuthenticationSchemes = "dupe" }, + new AuthorizeAttribute { Roles = "r1,r2", ActiveAuthenticationSchemes = "roles" }, + }; + var options = new AuthorizationOptions(); + options.AddPolicy("1", policy => policy.RequireClaim("1")); + options.AddPolicy("2", policy => policy.RequireClaim("2")); + + // Act + var combined = AuthorizationPolicy.Combine(options, attributes); + + // Assert + Assert.Equal(2, combined.ActiveAuthenticationSchemes.Count()); + Assert.True(combined.ActiveAuthenticationSchemes.Contains("dupe")); + Assert.True(combined.ActiveAuthenticationSchemes.Contains("roles")); + Assert.Equal(4, combined.Requirements.Count()); + Assert.True(combined.Requirements.Any(r => r is DenyAnonymousAuthorizationRequirement)); + Assert.Equal(2, combined.Requirements.OfType().Count()); + Assert.Equal(1, combined.Requirements.OfType().Count()); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Authorization.Test/DefaultAuthorizationServiceTests.cs b/test/Microsoft.AspNet.Authorization.Test/DefaultAuthorizationServiceTests.cs new file mode 100644 index 000000000..d5491164d --- /dev/null +++ b/test/Microsoft.AspNet.Authorization.Test/DefaultAuthorizationServiceTests.cs @@ -0,0 +1,828 @@ +// Copyright (c) .NET Foundation. All 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.Framework.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNet.Authorization.Test +{ + public class DefaultAuthorizationServiceTests + { + private IAuthorizationService BuildAuthorizationService(Action setupServices = null) + { + var services = new ServiceCollection(); + services.AddAuthorization(); + if (setupServices != null) + { + setupServices(services); + } + return services.BuildServiceProvider().GetRequiredService(); + } + + [Fact] + public void AuthorizeCombineThrowsOnUnknownPolicy() + { + Assert.Throws(() => AuthorizationPolicy.Combine(new AuthorizationOptions(), new AuthorizeAttribute[] { + new AuthorizeAttribute { Policy = "Wut" } + })); + } + + [Fact] + public async Task Authorize_ShouldAllowIfClaimIsPresent() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); + }); + }); + var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic")); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task Authorize_ShouldAllowIfClaimIsPresentWithSpecifiedAuthType() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); + }); + }); + var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic")); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task Authorize_ShouldAllowIfClaimIsAmongValues() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewPage"), + new Claim("Permission", "CanViewAnything") + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task Authorize_ShouldFailWhenAllRequirementsNotHandled() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("SomethingElse", "CanViewPage"), + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task Authorize_ShouldNotAllowIfClaimTypeIsNotPresent() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("SomethingElse", "CanViewPage"), + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task Authorize_ShouldNotAllowIfClaimValueIsNotPresent() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewComment"), + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task Authorize_ShouldNotAllowIfNoClaims() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[0], + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task Authorize_ShouldNotAllowIfUserIsNull() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); + }); + }); + + // Act + var allowed = await authorizationService.AuthorizeAsync(null, null, "Basic"); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task Authorize_ShouldNotAllowIfNotCorrectAuthType() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); + }); + }); + var user = new ClaimsPrincipal(new ClaimsIdentity()); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task Authorize_ShouldAllowWithNoAuthType() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireClaim("Permission", "CanViewPage")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewPage"), + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task Authorize_ShouldNotAllowIfUnknownPolicy() + { + // Arrange + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewComment"), + }, + null) + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task Authorize_CustomRolePolicy() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireRole("Administrator") + .RequireClaim(ClaimTypes.Role, "User"); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Role, "User"), + new Claim(ClaimTypes.Role, "Administrator") + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task Authorize_HasAnyClaimOfTypePolicy() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireClaim(ClaimTypes.Role); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Role, ""), + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task Authorize_PolicyCanAuthenticationSchemeWithNameClaim() + { + // Arrange + var policy = new AuthorizationPolicyBuilder("AuthType").RequireClaim(ClaimTypes.Name); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, "Name") }, "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task RolePolicyCanRequireSingleRole() + { + // Arrange + var policy = new AuthorizationPolicyBuilder("AuthType").RequireRole("Admin"); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Role, "Admin") }, "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, policy.Build()); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task RolePolicyCanRequireOneOfManyRoles() + { + // Arrange + var policy = new AuthorizationPolicyBuilder("AuthType").RequireRole("Admin", "Users"); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Role, "Users") }, "AuthType")); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task RolePolicyCanBlockWrongRole() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequireClaim("Permission", "CanViewPage"); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Role, "Nope"), + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, policy.Build()); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task RolePolicyCanBlockNoRole() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequireRole("Admin", "Users")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Basic"); + + // Assert + Assert.False(allowed); + } + + [Fact] + public void PolicyThrowsWithNoRequirements() + { + Assert.Throws(() => BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => { }); + }); + })); + } + + [Fact] + public async Task RequireUserNameFailsForWrongUserName() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Hao", policy => policy.RequireUserName("Hao")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Name, "Tek"), + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Any"); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task CanRequireUserName() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Hao", policy => policy.RequireUserName("Hao")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Name, "Hao"), + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Hao"); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task CanRequireUserNameWithDiffClaimType() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Hao", policy => policy.RequireUserName("Hao")); + }); + }); + var identity = new ClaimsIdentity("AuthType", "Name", "Role"); + identity.AddClaim(new Claim("Name", "Hao")); + var user = new ClaimsPrincipal(identity); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Hao"); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task CanRequireRoleWithDiffClaimType() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Hao", policy => policy.RequireRole("Hao")); + }); + }); + var identity = new ClaimsIdentity("AuthType", "Name", "Role"); + identity.AddClaim(new Claim("Role", "Hao")); + var user = new ClaimsPrincipal(identity); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, "Hao"); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task CanApproveAnyAuthenticatedUser() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Any", policy => policy.RequireAuthenticatedUser()); + }); + }); + var user = new ClaimsPrincipal(new ClaimsIdentity()); + user.AddIdentity(new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Name, "Name"), + }, + "AuthType")); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Any"); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task CanBlockNonAuthenticatedUser() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Any", policy => policy.RequireAuthenticatedUser()); + }); + }); + var user = new ClaimsPrincipal(new ClaimsIdentity()); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Any"); + + // Assert + Assert.False(allowed); + } + + public class CustomRequirement : IAuthorizationRequirement { } + public class CustomHandler : AuthorizationHandler + { + public override void Handle(AuthorizationContext context, CustomRequirement requirement) + { + context.Succeed(requirement); + } + } + + [Fact] + public async Task CustomReqWithNoHandlerFails() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); + }); + }); + var user = new ClaimsPrincipal(); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Custom"); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task CustomReqWithHandlerSucceeds() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddTransient(); + services.ConfigureAuthorization(options => + { + options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); + }); + }); + var user = new ClaimsPrincipal(); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Custom"); + + // Assert + Assert.True(allowed); + } + + public class PassThroughRequirement : AuthorizationHandler, IAuthorizationRequirement + { + public PassThroughRequirement(bool succeed) + { + Succeed = succeed; + } + + public bool Succeed { get; set; } + + public override void Handle(AuthorizationContext context, PassThroughRequirement requirement) + { + if (Succeed) { + context.Succeed(requirement); + } + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task PassThroughRequirementWillSucceedWithoutCustomHandler(bool shouldSucceed) + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Passthrough", policy => policy.Requirements.Add(new PassThroughRequirement(shouldSucceed))); + }); + }); + var user = new ClaimsPrincipal(); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Passthrough"); + + // Assert + Assert.Equal(shouldSucceed, allowed); + } + + public async Task CanCombinePolicies() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + var basePolicy = new AuthorizationPolicyBuilder().RequireClaim("Base", "Value").Build(); + options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequireClaim("Claim", "Exists")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Base", "Value"), + new Claim("Claim", "Exists") + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined"); + + // Assert + Assert.True(allowed); + } + + public async Task CombinePoliciesWillFailIfBasePolicyFails() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + var basePolicy = new AuthorizationPolicyBuilder().RequireClaim("Base", "Value").Build(); + options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequireClaim("Claim", "Exists")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Claim", "Exists") + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined"); + + // Assert + Assert.False(allowed); + } + + public async Task CombinedPoliciesWillFailIfExtraRequirementFails() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + var basePolicy = new AuthorizationPolicyBuilder().RequireClaim("Base", "Value").Build(); + options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequireClaim("Claim", "Exists")); + }); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("Base", "Value"), + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined"); + + // Assert + Assert.False(allowed); + } + + public class ExpenseReport { } + + public static class Operations + { + public static OperationAuthorizationRequirement Edit = new OperationAuthorizationRequirement { Name = "Edit" }; + public static OperationAuthorizationRequirement Create = new OperationAuthorizationRequirement { Name = "Create" }; + public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = "Delete" }; + } + + public class ExpenseReportAuthorizationHandler : AuthorizationHandler + { + public ExpenseReportAuthorizationHandler(IEnumerable authorized) + { + _allowed = authorized; + } + + private IEnumerable _allowed; + + public override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement, ExpenseReport resource) + { + if (_allowed.Contains(requirement)) + { + context.Succeed(requirement); + } + } + } + + public class SuperUserHandler : AuthorizationHandler + { + public override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement) + { + if (context.User.HasClaim("SuperUser", "yes")) + { + context.Succeed(requirement); + } + } + } + + public async Task CanAuthorizeAllSuperuserOperations() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddInstance(new ExpenseReportAuthorizationHandler(new OperationAuthorizationRequirement[] { Operations.Edit })); + services.AddTransient(); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("SuperUser", "yes"), + }, + "AuthType") + ); + + // Act + // Assert + Assert.True(await authorizationService.AuthorizeAsync(user, null, Operations.Edit)); + Assert.True(await authorizationService.AuthorizeAsync(user, null, Operations.Delete)); + Assert.True(await authorizationService.AuthorizeAsync(user, null, Operations.Create)); + } + + public async Task CanAuthorizeOnlyAllowedOperations() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddInstance(new ExpenseReportAuthorizationHandler(new OperationAuthorizationRequirement[] { Operations.Edit })); + services.AddTransient(); + }); + var user = new ClaimsPrincipal(); + + // Act + // Assert + Assert.True(await authorizationService.AuthorizeAsync(user, null, Operations.Edit)); + Assert.False(await authorizationService.AuthorizeAsync(user, null, Operations.Delete)); + Assert.False(await authorizationService.AuthorizeAsync(user, null, Operations.Create)); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Authorization.Test/Microsoft.AspNet.Authorization.Test.xproj b/test/Microsoft.AspNet.Authorization.Test/Microsoft.AspNet.Authorization.Test.xproj new file mode 100644 index 000000000..579dd0f44 --- /dev/null +++ b/test/Microsoft.AspNet.Authorization.Test/Microsoft.AspNet.Authorization.Test.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 7af5ad96-eb6e-4d0e-8abe-c0b543c0f4c2 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Authorization.Test/project.json b/test/Microsoft.AspNet.Authorization.Test/project.json new file mode 100644 index 000000000..065ecb7d3 --- /dev/null +++ b/test/Microsoft.AspNet.Authorization.Test/project.json @@ -0,0 +1,21 @@ +{ + "compilationOptions": { + "warningsAsErrors": "true" + }, + "dependencies": { + "Microsoft.AspNet.Authorization": "1.0.0-*", + "Microsoft.Framework.DependencyInjection": "1.0.0-*", + "Moq": "4.2.1312.1622", + "xunit.runner.aspnet": "2.0.0-aspnet-*" + }, + "commands": { + "test": "xunit.runner.aspnet" + }, + "frameworks": { + "dnx451": { + "dependencies": { + "Shouldly": "1.1.1.1" + } + } + } +} diff --git a/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs b/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs deleted file mode 100644 index 7d439897b..000000000 --- a/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs +++ /dev/null @@ -1,306 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNet.Security; -using Xunit; - -namespace Microsoft.AspNet.Security.Test -{ - public class DefaultAuthorizationServiceTests - { - [Fact] - public void Check_ShouldAllowIfClaimIsPresent() - { - // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - var user = new ClaimsPrincipal( - new ClaimsIdentity( new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic") - ); - - // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); - - // Assert - Assert.True(allowed); - } - - [Fact] - public void Check_ShouldAllowIfClaimIsAmongValues() - { - // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - var user = new ClaimsPrincipal( - new ClaimsIdentity( - new Claim[] { - new Claim("Permission", "CanViewPage"), - new Claim("Permission", "CanViewAnything") - }, - "Basic") - ); - - // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); - - // Assert - Assert.True(allowed); - } - - [Fact] - public void Check_ShouldNotAllowIfClaimTypeIsNotPresent() - { - // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - var user = new ClaimsPrincipal( - new ClaimsIdentity( - new Claim[] { - new Claim("SomethingElse", "CanViewPage"), - }, - "Basic") - ); - - // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); - - // Assert - Assert.False(allowed); - } - - [Fact] - public void Check_ShouldNotAllowIfClaimValueIsNotPresent() - { - // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - var user = new ClaimsPrincipal( - new ClaimsIdentity( - new Claim[] { - new Claim("Permission", "CanViewComment"), - }, - "Basic") - ); - - // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); - - // Assert - Assert.False(allowed); - } - - [Fact] - public void Check_ShouldNotAllowIfNoClaims() - { - // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - var user = new ClaimsPrincipal( - new ClaimsIdentity( - new Claim[0], - "Basic") - ); - - // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); - - // Assert - Assert.False(allowed); - } - - [Fact] - public void Check_ShouldNotAllowIfUserIsNull() - { - // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - ClaimsPrincipal user = null; - - // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); - - // Assert - Assert.False(allowed); - } - - [Fact] - public void Check_ShouldNotAllowIfUserIsNotAuthenticated() - { - // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - var user = new ClaimsPrincipal( - new ClaimsIdentity( - new Claim[] { - new Claim("Permission", "CanViewComment"), - }, - null) - ); - - // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); - - // Assert - Assert.False(allowed); - } - - [Fact] - public void Check_ShouldApplyPoliciesInOrder() - { - // Arrange - string result = ""; - var policies = new IAuthorizationPolicy[] { - new FakePolicy() { - Order = 20, - ApplyingAsyncAction = (context) => { result += "20"; } - }, - new FakePolicy() { - Order = -1, - ApplyingAsyncAction = (context) => { result += "-1"; } - }, - new FakePolicy() { - Order = 30, - ApplyingAsyncAction = (context) => { result += "30"; } - }, - }; - - var authorizationService = new DefaultAuthorizationService(policies); - - // Act - var allowed = authorizationService.Authorize(null, null); - - // Assert - Assert.Equal("-12030", result); - } - - [Fact] - public void Check_ShouldInvokeApplyingApplyAppliedInOrder() - { - // Arrange - string result = ""; - var policies = new IAuthorizationPolicy[] { - new FakePolicy() { - Order = 20, - ApplyingAsyncAction = (context) => { result += "Applying20"; }, - ApplyAsyncAction = (context) => { result += "Apply20"; }, - AppliedAsyncAction = (context) => { result += "Applied20"; } - }, - new FakePolicy() { - Order = -1, - ApplyingAsyncAction = (context) => { result += "Applying-1"; }, - ApplyAsyncAction = (context) => { result += "Apply-1"; }, - AppliedAsyncAction = (context) => { result += "Applied-1"; } - }, - new FakePolicy() { - Order = 30, - ApplyingAsyncAction = (context) => { result += "Applying30"; }, - ApplyAsyncAction = (context) => { result += "Apply30"; }, - AppliedAsyncAction = (context) => { result += "Applied30"; } - }, - }; - - var authorizationService = new DefaultAuthorizationService(policies); - - // Act - var allowed = authorizationService.Authorize(null, null); - - // Assert - Assert.Equal("Applying-1Applying20Applying30Apply-1Apply20Apply30Applied-1Applied20Applied30", result); - } - - [Fact] - public void Check_ShouldConvertNullClaimsToEmptyList() - { - // Arrange - IList claims = null; - var policies = new IAuthorizationPolicy[] { - new FakePolicy() { - Order = 20, - ApplyingAsyncAction = (context) => { claims = context.Claims; } - } - }; - - var authorizationService = new DefaultAuthorizationService(policies); - - // Act - var allowed = authorizationService.Authorize(null, null); - - // Assert - Assert.NotNull(claims); - Assert.Equal(0, claims.Count); - } - - [Fact] - public void Check_ShouldThrowWhenPoliciesDontStop() - { - // Arrange - var policies = new IAuthorizationPolicy[] { - new FakePolicy() { - ApplyAsyncAction = (context) => { context.Retry = true; } - } - }; - - var authorizationService = new DefaultAuthorizationService(policies); - - // Act - // Assert - Exception ex = Assert.Throws(() => authorizationService.Authorize(null, null)); - } - - [Fact] - public void Check_ApplyCanMutateCheckedClaims() - { - - // Arrange - var user = new ClaimsPrincipal( - new ClaimsIdentity( new Claim[] { new Claim("Permission", "CanDeleteComments") }, "Basic") - ); - - var policies = new IAuthorizationPolicy[] { - new FakePolicy() { - ApplyAsyncAction = (context) => { - // for instance, if user owns the comment - if(!context.Claims.Any(claim => claim.Type == "Permission" && claim.Value == "CanDeleteComments")) - { - context.Claims.Add(new Claim("Permission", "CanDeleteComments")); - context.Retry = true; - } - } - } - }; - - var authorizationService = new DefaultAuthorizationService(policies); - - // Act - var allowed = authorizationService.Authorize(Enumerable.Empty(), user); - - // Assert - Assert.True(allowed); - } - - [Fact] - public void Check_PoliciesCanMutateUsersClaims() - { - - // Arrange - var user = new ClaimsPrincipal( - new ClaimsIdentity(new Claim[0], "Basic") - ); - - var policies = new IAuthorizationPolicy[] { - new FakePolicy() { - ApplyAsyncAction = (context) => { - if (!context.Authorized) - { - context.UserClaims.Add(new Claim("Permission", "CanDeleteComments")); - context.Retry = true; - } - } - } - }; - - var authorizationService = new DefaultAuthorizationService(policies); - - // Act - var allowed = authorizationService.Authorize(new Claim("Permission", "CanDeleteComments"), user); - - // Assert - Assert.True(allowed); - } - } -} diff --git a/test/Microsoft.AspNet.Security.Test/FakePolicy.cs b/test/Microsoft.AspNet.Security.Test/FakePolicy.cs deleted file mode 100644 index 3e365cde2..000000000 --- a/test/Microsoft.AspNet.Security.Test/FakePolicy.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNet.Security; - -namespace Microsoft.AspNet.Security.Test -{ - public class FakePolicy : IAuthorizationPolicy - { - - public int Order { get; set; } - - public Task ApplyingAsync(AuthorizationPolicyContext context) - { - if (ApplyingAsyncAction != null) - { - ApplyingAsyncAction(context); - } - - return Task.FromResult(0); - } - - public Task ApplyAsync(AuthorizationPolicyContext context) - { - if (ApplyAsyncAction != null) - { - ApplyAsyncAction(context); - } - - return Task.FromResult(0); - - } - - public Task AppliedAsync(AuthorizationPolicyContext context) - { - if (AppliedAsyncAction != null) - { - AppliedAsyncAction(context); - } - - return Task.FromResult(0); - } - - public Action ApplyingAsyncAction { get; set;} - - public Action ApplyAsyncAction { get; set;} - - public Action AppliedAsyncAction { get; set;} - } -} diff --git a/test/Microsoft.AspNet.Security.Test/Microsoft.AspNet.Security.kproj b/test/Microsoft.AspNet.Security.Test/Microsoft.AspNet.Security.kproj deleted file mode 100644 index 3a317e7f3..000000000 --- a/test/Microsoft.AspNet.Security.Test/Microsoft.AspNet.Security.kproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - 12.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 8da26cd1-1302-4cfd-9270-9fa1b7c6138b - Library - - - - - - - 2.0 - - - - - - - - - - \ No newline at end of file diff --git a/test/Microsoft.AspNet.Security.Test/project.json b/test/Microsoft.AspNet.Security.Test/project.json deleted file mode 100644 index cfdc5ed7f..000000000 --- a/test/Microsoft.AspNet.Security.Test/project.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "version" : "0.1-alpha-*", - "compilationOptions": { - "warningsAsErrors": true - }, - "dependencies": { - "Microsoft.AspNet.Security" : "0.1-alpha-*", - "Moq": "4.2.1312.1622", - "Xunit.KRunner": "0.1-alpha-*", - "xunit.abstractions": "2.0.0-aspnet-*", - "xunit.assert": "2.0.0-aspnet-*", - "xunit.core": "2.0.0-aspnet-*", - "xunit.execution": "2.0.0-aspnet-*" - }, - "commands": { - "test": "Xunit.KRunner" - }, - "configurations": { - "net45": { - "dependencies": { - "System.Runtime": "" - } - } - } -}