diff --git a/NuGet.Config b/NuGet.Config index 53454b2000..ee06e90062 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -2,5 +2,6 @@ + diff --git a/README.md b/README.md index ab7b29e979..06a74de411 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,4 @@ ASP.NET Security ASP.NET Security contains the security and authorization middlewares for ASP.NET vNext. -This project is part of ASP.NET vNext. You can find samples, documentation and getting started instructions for ASP.NET vNext at the [Home](https://github.com/aspnet/home) repo. +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 2cc23038e0..ccddd42c7a 100644 --- a/Security.sln +++ b/Security.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22013.1 +VisualStudioVersion = 14.0.22422.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4D2B6A51-2F9F-44F5-8131-EA5CAC053652}" EndProject @@ -36,6 +36,12 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.O EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CookieSessionSample", "samples\CookieSessionSample\CookieSessionSample.kproj", "{19711880-46DA-4A26-9E0F-9B2E41D27651}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "OpenIdConnectSample", "samples\OpenIdConnectSample\OpenIdConnectSample.kproj", "{BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.OAuthBearer", "src\Microsoft.AspNet.Security.OAuthBearer\Microsoft.AspNet.Security.OAuthBearer.kproj", "{2755BFE5-7421-4A31-A644-F817DF5CAA98}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.OpenIdConnect", "src\Microsoft.AspNet.Security.OpenIdConnect\Microsoft.AspNet.Security.OpenIdConnect.kproj", "{674D128E-83BB-481A-A9D9-6D47872E1FC8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -156,6 +162,42 @@ Global {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 + {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 + {674D128E-83BB-481A-A9D9-6D47872E1FC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {674D128E-83BB-481A-A9D9-6D47872E1FC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {674D128E-83BB-481A-A9D9-6D47872E1FC8}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {674D128E-83BB-481A-A9D9-6D47872E1FC8}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {674D128E-83BB-481A-A9D9-6D47872E1FC8}.Debug|x86.ActiveCfg = Debug|Any CPU + {674D128E-83BB-481A-A9D9-6D47872E1FC8}.Debug|x86.Build.0 = Debug|Any CPU + {674D128E-83BB-481A-A9D9-6D47872E1FC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {674D128E-83BB-481A-A9D9-6D47872E1FC8}.Release|Any CPU.Build.0 = Release|Any CPU + {674D128E-83BB-481A-A9D9-6D47872E1FC8}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {674D128E-83BB-481A-A9D9-6D47872E1FC8}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {674D128E-83BB-481A-A9D9-6D47872E1FC8}.Release|x86.ActiveCfg = Release|Any CPU + {674D128E-83BB-481A-A9D9-6D47872E1FC8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -172,5 +214,8 @@ Global {1FCF26C2-A3C7-4308-B698-4AFC3560BC0C} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {4A636011-68EE-4CE5-836D-EA8E13CF71E4} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {19711880-46DA-4A26-9E0F-9B2E41D27651} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} + {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} + {2755BFE5-7421-4A31-A644-F817DF5CAA98} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {674D128E-83BB-481A-A9D9-6D47872E1FC8} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} EndGlobalSection EndGlobal diff --git a/build.cmd b/build.cmd index 86ca5bbbf1..220a1ff561 100644 --- a/build.cmd +++ b/build.cmd @@ -19,10 +19,10 @@ 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 -IF "%SKIP_KRE_INSTALL%"=="1" goto run -CALL packages\KoreBuild\build\kvm upgrade -runtime CLR -x86 -CALL packages\KoreBuild\build\kvm install default -runtime CoreCLR -x86 +IF "%SKIP_DOTNET_INSTALL%"=="1" goto run +CALL packages\KoreBuild\build\dotnetsdk upgrade -runtime CLR -x86 +CALL packages\KoreBuild\build\dotnetsdk install default -runtime CoreCLR -x86 :run -CALL packages\KoreBuild\build\kvm use default -runtime CLR -x86 +CALL packages\KoreBuild\build\dotnetsdk use default -runtime CLR -x86 packages\Sake\tools\Sake.exe -I packages\KoreBuild\build -f makefile.shade %* diff --git a/build.sh b/build.sh index c7873ef58e..350d7e389a 100644 --- a/build.sh +++ b/build.sh @@ -28,11 +28,11 @@ if test ! -d packages/KoreBuild; then fi if ! type k > /dev/null 2>&1; then - source packages/KoreBuild/build/kvm.sh + source packages/KoreBuild/build/dotnetsdk.sh fi if ! type k > /dev/null 2>&1; then - kvm upgrade + dotnetsdk upgrade fi mono packages/Sake/tools/Sake.exe -I packages/KoreBuild/build -f makefile.shade "$@" diff --git a/samples/OpenIdConnectSample/OpenIdConnectSample.kproj b/samples/OpenIdConnectSample/OpenIdConnectSample.kproj new file mode 100644 index 0000000000..c6ab693ae7 --- /dev/null +++ b/samples/OpenIdConnectSample/OpenIdConnectSample.kproj @@ -0,0 +1,30 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + bef0f5c3-ef4e-4649-9c49-d5e279a3ca2b + OpenIDConnectSample + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + OpenIDConnectSample + + + 2.0 + 42023 + + + + + + + + + + + \ No newline at end of file diff --git a/samples/OpenIdConnectSample/Startup.cs b/samples/OpenIdConnectSample/Startup.cs new file mode 100644 index 0000000000..bd443424ef --- /dev/null +++ b/samples/OpenIdConnectSample/Startup.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Security; +using Microsoft.AspNet.Security.Cookies; +using Microsoft.AspNet.Security.OpenIdConnect; +using Microsoft.Framework.DependencyInjection; + +namespace OpenIdConnectSample +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + app.UseServices(services => + { + services.AddDataProtection(); + services.Configure(options => + { + options.SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType; + }); + + }); + + app.UseCookieAuthentication(options => + { + }); + + 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 (context.User == null || !context.User.Identity.IsAuthenticated) + { + context.Response.Challenge(new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectAuthenticationDefaults.AuthenticationType); + + 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 0000000000..65652110bb --- /dev/null +++ b/samples/OpenIdConnectSample/project.json @@ -0,0 +1,18 @@ +{ + "dependencies": { + "Kestrel": "1.0.0-*", + "Microsoft.AspNet.Security.Cookies": "1.0.0-*", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Security.OpenIdConnect": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*" + }, + "frameworks": { + "aspnet50": { }, + "aspnetcore50": { } + }, + "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/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs index 63c524afa6..e290232575 100644 --- a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs @@ -146,7 +146,7 @@ namespace Microsoft.AspNet.Security.Cookies { Domain = Options.CookieDomain, HttpOnly = Options.CookieHttpOnly, - Path = Options.CookiePath ?? "/", + Path = Options.CookiePath ?? (RequestPathBase.HasValue ? RequestPathBase.ToString() : "/"), }; if (Options.CookieSecure == CookieSecureOption.SameAsRequest) { diff --git a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs index 121ff409b2..ec6e102063 100644 --- a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs @@ -24,7 +24,6 @@ namespace Microsoft.AspNet.Security.Cookies { AuthenticationType = CookieAuthenticationDefaults.AuthenticationType; ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter; - CookiePath = "/"; ExpireTimeSpan = TimeSpan.FromDays(14); SlidingExpiration = true; CookieHttpOnly = true; diff --git a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieValidateIdentityContext.cs b/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieValidateIdentityContext.cs index ea16822042..771d75d40c 100644 --- a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieValidateIdentityContext.cs +++ b/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieValidateIdentityContext.cs @@ -7,7 +7,7 @@ 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.Http.Interfaces.Security; using Microsoft.AspNet.Security.Infrastructure; using Microsoft.AspNet.Security.Notifications; diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationHandler.cs index 6520902559..7eabae1019 100644 --- a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationHandler.cs @@ -9,6 +9,8 @@ using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Core.Collections; +using Microsoft.AspNet.Http.Extensions; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.Security.OAuth; using Microsoft.AspNet.WebUtilities; @@ -39,7 +41,7 @@ namespace Microsoft.AspNet.Security.Facebook tokenResponse.EnsureSuccessStatusCode(); string oauthTokenResponse = await tokenResponse.Content.ReadAsStringAsync(); - IFormCollection form = FormHelpers.ParseForm(oauthTokenResponse); + IFormCollection form = new FormCollection(FormReader.ReadForm(oauthTokenResponse)); var response = new JObject(); foreach (string key in form.Keys) { diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/project.json b/src/Microsoft.AspNet.Security.MicrosoftAccount/project.json index 5d7e261368..530d34c575 100644 --- a/src/Microsoft.AspNet.Security.MicrosoftAccount/project.json +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/project.json @@ -6,6 +6,11 @@ }, "frameworks": { "aspnet50": {}, - "aspnetcore50": {} + "aspnetcore50": { + "dependencies": { + "System.Dynamic.Runtime": "4.0.0-beta-*", + "System.ObjectModel": "4.0.10-beta-*" + } + } } } diff --git a/src/Microsoft.AspNet.Security.OAuth/Notifications/IOAuthBearerAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.OAuth/Notifications/IOAuthBearerAuthenticationNotifications.cs deleted file mode 100644 index 048d8f2927..0000000000 --- a/src/Microsoft.AspNet.Security.OAuth/Notifications/IOAuthBearerAuthenticationNotifications.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Security.OAuth -{ - /// - /// Specifies callback methods which the invokes to enable developer control over the authentication process. /> - /// - public interface IOAuthBearerAuthenticationNotifications - { - /// - /// Invoked before the is created. Gives the application an - /// opportunity to find the identity from a different location, adjust, or reject the token. - /// - /// Contains the token string. - /// A representing the completed operation. - Task RequestToken(OAuthRequestTokenContext context); - - /// - /// 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. - /// - /// Contains information about the login session as well as the user . - /// A representing the completed operation. - Task ValidateIdentity(OAuthValidateIdentityContext context); - - /// - /// Called each time a challenge is being sent to the client. By implementing this method the application - /// may modify the challenge as needed. - /// - /// Contains the default challenge. - /// A representing the completed operation. - Task ApplyChallenge(OAuthChallengeContext context); - } -} diff --git a/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthBearerAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthBearerAuthenticationNotifications.cs deleted file mode 100644 index 2c24f3ff1f..0000000000 --- a/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthBearerAuthenticationNotifications.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Security.OAuth -{ - /// - /// OAuth bearer token middleware provider - /// - public class OAuthBearerAuthenticationNotifications : IOAuthBearerAuthenticationNotifications - { - /// - /// Initializes a new instance of the class - /// - public OAuthBearerAuthenticationNotifications() - { - OnRequestToken = context => Task.FromResult(null); - OnValidateIdentity = context => Task.FromResult(null); - OnApplyChallenge = context => - { - context.HttpContext.Response.Headers.AppendValues("WWW-Authenticate", context.Challenge); - return Task.FromResult(0); - }; - } - - /// - /// Handles processing OAuth bearer token. - /// - public Func OnRequestToken { get; set; } - - /// - /// Handles validating the identity produced from an OAuth bearer token. - /// - public Func OnValidateIdentity { get; set; } - - /// - /// Handles applying the authentication challenge to the response message. - /// - public Func OnApplyChallenge { get; set; } - - /// - /// Handles processing OAuth bearer token. - /// - /// - /// - public virtual Task RequestToken(OAuthRequestTokenContext context) - { - return OnRequestToken(context); - } - - /// - /// Handles validating the identity produced from an OAuth bearer token. - /// - /// - /// - public virtual Task ValidateIdentity(OAuthValidateIdentityContext context) - { - return OnValidateIdentity.Invoke(context); - } - - /// - /// Handles applying the authentication challenge to the response message. - /// - /// - /// - public Task ApplyChallenge(OAuthChallengeContext context) - { - return OnApplyChallenge(context); - } - } -} diff --git a/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthValidateIdentityContext.cs b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthValidateIdentityContext.cs deleted file mode 100644 index 5dc04ffb40..0000000000 --- a/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthValidateIdentityContext.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNet.Http; - -namespace Microsoft.AspNet.Security.OAuth -{ - /// - /// Contains the authentication ticket data from an OAuth bearer token. - /// - public class OAuthValidateIdentityContext : BaseValidatingTicketContext - { - /// - /// Initializes a new instance of the class - /// - /// - /// - /// - public OAuthValidateIdentityContext( - HttpContext context, - OAuthBearerAuthenticationOptions options, - AuthenticationTicket ticket) : base(context, options, ticket) - { - } - } -} diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationHandler.cs index 5a4ffebbd2..b165ca41c0 100644 --- a/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationHandler.cs @@ -8,6 +8,7 @@ using System.Net.Http.Headers; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Extensions; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.Security.Infrastructure; using Microsoft.AspNet.WebUtilities; diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationMiddleware.cs index 2628d38491..18dd9c9a86 100644 --- a/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationMiddleware.cs @@ -1,16 +1,16 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Net.Http; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Security.DataHandler; using Microsoft.AspNet.Security.DataProtection; using Microsoft.AspNet.Security.Infrastructure; using Microsoft.Framework.Logging; using Microsoft.Framework.OptionsModel; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Net.Http; namespace Microsoft.AspNet.Security.OAuth { @@ -45,18 +45,22 @@ namespace Microsoft.AspNet.Security.OAuth { throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "AuthenticationType")); } + if (string.IsNullOrWhiteSpace(Options.ClientId)) { throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "ClientId")); } + if (string.IsNullOrWhiteSpace(Options.ClientSecret)) { throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "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")); diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationHandler.cs deleted file mode 100644 index 406085b670..0000000000 --- a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationHandler.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Threading.Tasks; -using Microsoft.AspNet.Security.Infrastructure; -using Microsoft.Framework.Logging; - -namespace Microsoft.AspNet.Security.OAuth -{ - internal class OAuthBearerAuthenticationHandler : AuthenticationHandler - { - private readonly ILogger _logger; - private readonly string _challenge; - - public OAuthBearerAuthenticationHandler(ILogger logger, string challenge) - { - _logger = logger; - _challenge = challenge; - } - - protected override AuthenticationTicket AuthenticateCore() - { - return AuthenticateCoreAsync().GetAwaiter().GetResult(); - } - - protected override async Task AuthenticateCoreAsync() - { - try - { - // Find token in default location - string requestToken = null; - string authorization = Request.Headers.Get("Authorization"); - if (!string.IsNullOrEmpty(authorization)) - { - if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) - { - requestToken = authorization.Substring("Bearer ".Length).Trim(); - } - } - - // Give application opportunity to find from a different location, adjust, or reject token - var requestTokenContext = new OAuthRequestTokenContext(Context, requestToken); - await Options.Notifications.RequestToken(requestTokenContext); - - // If no token found, no further work possible - if (string.IsNullOrEmpty(requestTokenContext.Token)) - { - return null; - } - - // Call provider to process the token into data - var tokenReceiveContext = new AuthenticationTokenReceiveContext( - Context, - Options.AccessTokenFormat, - requestTokenContext.Token); - - await Options.AccessTokenProvider.ReceiveAsync(tokenReceiveContext); - if (tokenReceiveContext.Ticket == null) - { - tokenReceiveContext.DeserializeTicket(tokenReceiveContext.Token); - } - - AuthenticationTicket ticket = tokenReceiveContext.Ticket; - if (ticket == null) - { - _logger.WriteWarning("invalid bearer token received"); - return null; - } - - // Validate expiration time if present - DateTimeOffset currentUtc = Options.SystemClock.UtcNow; - - if (ticket.Properties.ExpiresUtc.HasValue && - ticket.Properties.ExpiresUtc.Value < currentUtc) - { - _logger.WriteWarning("expired bearer token received"); - return null; - } - - // Give application final opportunity to override results - var context = new OAuthValidateIdentityContext(Context, Options, ticket); - if (ticket != null && - ticket.Identity != null && - ticket.Identity.IsAuthenticated) - { - // bearer token with identity starts validated - context.Validated(); - } - - await Options.Notifications.ValidateIdentity(context); - if (!context.IsValidated) - { - return null; - } - - // resulting identity values go back to caller - return context.Ticket; - } - catch (Exception ex) - { - _logger.WriteError("Authentication failed", ex); - return null; - } - } - - protected override void ApplyResponseChallenge() - { - if (Response.StatusCode != 401) - { - return; - } - - if (ChallengeContext != null) - { - OAuthChallengeContext challengeContext = new OAuthChallengeContext(Context, _challenge); - Options.Notifications.ApplyChallenge(challengeContext); - } - } - - protected override void ApplyResponseGrant() - { - // N/A - } - } -} diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationMiddleware.cs deleted file mode 100644 index 0661c4fede..0000000000 --- a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationMiddleware.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNet.Builder; -using Microsoft.AspNet.Security.DataHandler; -using Microsoft.AspNet.Security.DataProtection; -using Microsoft.AspNet.Security.Infrastructure; -using Microsoft.Framework.Logging; -using Microsoft.Framework.OptionsModel; -using System; - -namespace Microsoft.AspNet.Security.OAuth -{ - /// - /// 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 - { - private readonly ILogger _logger; - - private readonly string _challenge; - - /// - /// 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( - RequestDelegate next, - IServiceProvider services, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - IOptions options, - ConfigureOptions configureOptions) - : base(next, services, options, configureOptions) - { - _logger = loggerFactory.Create(); - - if (!string.IsNullOrWhiteSpace(Options.Challenge)) - { - _challenge = Options.Challenge; - } - else if (string.IsNullOrWhiteSpace(Options.Realm)) - { - _challenge = "Bearer"; - } - else - { - _challenge = "Bearer realm=\"" + Options.Realm + "\""; - } - - if (Options.Notifications == null) - { - Options.Notifications = new OAuthBearerAuthenticationNotifications(); - } - - if (Options.AccessTokenFormat == null) - { - IDataProtector dataProtector = dataProtectionProvider.CreateDataProtector( - this.GetType().FullName, Options.AuthenticationType, "v1"); - Options.AccessTokenFormat = new TicketDataFormat(dataProtector); - } - - if (Options.AccessTokenProvider == null) - { - Options.AccessTokenProvider = new AuthenticationTokenProvider(); - } - } - - /// - /// 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(_logger, _challenge); - } - } -} diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationOptions.cs deleted file mode 100644 index d2f9b9190b..0000000000 --- a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationOptions.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNet.Security.Infrastructure; - -namespace Microsoft.AspNet.Security.OAuth -{ - /// - /// Options class provides information needed to control Bearer Authentication middleware behavior - /// - public class OAuthBearerAuthenticationOptions : AuthenticationOptions - { - /// - /// Creates an instance of bearer authentication options with default values. - /// - public OAuthBearerAuthenticationOptions() : base() - { - SystemClock = new SystemClock(); - AuthenticationType = OAuthBearerAuthenticationDefaults.AuthenticationType; - } - - /// - /// Determines what realm value is included when the bearer middleware adds a response header to an unauthorized request. - /// If not assigned, the response header does not have a realm. - /// - public string Realm { get; set; } - - /// - /// Specifies the full challenge to send to the client, and should start with "Bearer". If a challenge is provided then the - /// Realm property is ignored. If no challenge is specified then one is created using "Bearer" and the value of the Realm - /// property. - /// - 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 IOAuthBearerAuthenticationNotifications Notifications { get; set; } - - /// - /// The data format used to un-protect the information contained in the access token. - /// If not provided by the application the default data protection provider depends on the host server. - /// The SystemWeb host on IIS will use ASP.NET machine key data protection, and HttpListener and other self-hosted - /// servers will use DPAPI data protection. If a different access token - /// provider or format is assigned, a compatible instance must be assigned to the OAuthAuthorizationServerOptions.AccessTokenProvider - /// and OAuthAuthorizationServerOptions.AccessTokenFormat of the authorization server. - /// - public ISecureDataFormat AccessTokenFormat { get; set; } - - /// - /// Receives the bearer token the client application will be providing to web application. If not provided the token - /// produced on the server's default data protection by using the AccessTokenFormat. If a different access token - /// provider or format is assigned, a compatible instance must be assigned to the OAuthAuthorizationServerOptions.AccessTokenProvider - /// and OAuthAuthorizationServerOptions.AccessTokenFormat of the authorization server. - /// - public IAuthenticationTokenProvider AccessTokenProvider { 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; } - } -} diff --git a/src/Microsoft.AspNet.Security.OAuth/project.json b/src/Microsoft.AspNet.Security.OAuth/project.json index 0c44ae0cff..39ba020fc7 100644 --- a/src/Microsoft.AspNet.Security.OAuth/project.json +++ b/src/Microsoft.AspNet.Security.OAuth/project.json @@ -2,12 +2,8 @@ "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.Http.Extensions": "1.0.0-*", "Microsoft.AspNet.Security": "1.0.0-*", "Microsoft.AspNet.Security.DataProtection": "1.0.0-*", - "Microsoft.AspNet.WebUtilities": "1.0.0-*", - "Microsoft.Framework.Logging": "1.0.0-*", - "Newtonsoft.Json": "6.0.6" }, "frameworks": { "aspnet50": { @@ -18,28 +14,7 @@ }, "aspnetcore50": { "dependencies": { - "System.Collections": "4.0.10-beta-*", - "System.ComponentModel": "4.0.0-beta-*", - "System.Console": "4.0.0-beta-*", - "System.Diagnostics.Debug": "4.0.10-beta-*", - "System.Diagnostics.Tools": "4.0.0-beta-*", - "System.Dynamic.Runtime": "4.0.0-beta-*", - "System.Globalization": "4.0.10-beta-*", - "System.IO": "4.0.10-beta-*", - "System.Linq": "4.0.0-beta-*", - "System.Net.Http.WinHttpHandler": "4.0.0-beta-*", - "System.ObjectModel": "4.0.10-beta-*", - "System.Reflection": "4.0.10-beta-*", - "System.Resources.ResourceManager": "4.0.0-beta-*", - "System.Runtime": "4.0.20-beta-*", - "System.Runtime.Extensions": "4.0.10-beta-*", - "System.Runtime.InteropServices": "4.0.20-beta-*", - "System.Security.Claims": "4.0.0-beta-*", - "System.Security.Cryptography.Hashing.Algorithms": "4.0.0-beta-*", - "System.Security.Principal": "4.0.0-beta-*", - "System.Threading": "4.0.0-beta-*", - "System.Threading.Tasks": "4.0.10-beta-*", - "System.Net.Http": "4.0.0-beta-*" + "System.Net.Http.WinHttpHandler": "4.0.0-beta-*" } } } diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/Microsoft.AspNet.Security.OAuthBearer.kproj b/src/Microsoft.AspNet.Security.OAuthBearer/Microsoft.AspNet.Security.OAuthBearer.kproj new file mode 100644 index 0000000000..55d65389e4 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuthBearer/Microsoft.AspNet.Security.OAuthBearer.kproj @@ -0,0 +1,22 @@ + + + + 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.Security.OAuthBearer/NotNullAttribute.cs b/src/Microsoft.AspNet.Security.OAuthBearer/NotNullAttribute.cs new file mode 100644 index 0000000000..29f5827495 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuthBearer/NotNullAttribute.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Security.OAuthBearer +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/AuthenticationChallengeNotification.cs b/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/AuthenticationChallengeNotification.cs new file mode 100644 index 0000000000..f2685af0c5 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/AuthenticationChallengeNotification.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.Notifications; + +namespace Microsoft.AspNet.Security.OAuthBearer +{ + public class AuthenticationChallengeNotification : BaseNotification + { + public AuthenticationChallengeNotification(HttpContext context, TOptions options) : base(context, options) + { + } + } +} diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs new file mode 100644 index 0000000000..808615e1d4 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.Notifications; + +/// +/// Specifies events which the invokes to enable developer control over the authentication process. /> +/// +namespace Microsoft.AspNet.Security.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.Security.OAuth/OAuthBearerAuthenticationDefaults.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationDefaults.cs similarity index 92% rename from src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationDefaults.cs rename to src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationDefaults.cs index 70f1f1e616..5b62827cc7 100644 --- a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationDefaults.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationDefaults.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace Microsoft.AspNet.Security.OAuth +namespace Microsoft.AspNet.Security.OAuthBearer { /// /// Default values used by authorization server and bearer authentication. diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationExtensions.cs similarity index 97% rename from src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationExtensions.cs rename to src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationExtensions.cs index b5512d01cd..0f31dfaf8a 100644 --- a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationExtensions.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationExtensions.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNet.Security.OAuth; +using Microsoft.AspNet.Security.OAuthBearer; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.OptionsModel; diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs new file mode 100644 index 0000000000..a4555790dc --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Security.Infrastructure; +using Microsoft.AspNet.Security.Notifications; +using Microsoft.Framework.Logging; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.AspNet.Security.OAuthBearer +{ + public class OAuthBearerAuthenticationHandler : AuthenticationHandler + { + private readonly ILogger _logger; + private OpenIdConnectConfiguration _configuration; + + public OAuthBearerAuthenticationHandler(ILogger logger) + { + _logger = logger; + } + + 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)) + { + string 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 + { + IEnumerable 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)) + { + ClaimsPrincipal principal = validator.ValidateToken(token, validationParameters, out validatedToken); + AuthenticationTicket ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), Options.AuthenticationType); + var securityTokenValidatedNotification = new SecurityTokenValidatedNotification(Context, Options) + { + ProtocolMessage = Context, + AuthenticationTicket = ticket + }; + + if (securityTokenReceivedNotification.HandledResponse) + { + return securityTokenValidatedNotification.AuthenticationTicket; + } + + if (securityTokenReceivedNotification.Skipped) + { + return null; + } + + return ticket; + } + } + + throw new InvalidOperationException("No SecurityTokenValidator available for token: " + token ?? "null"); + } + catch (Exception ex) + { + _logger.WriteError("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 ((Response.StatusCode != 401) || (ChallengeContext == null)) + { + return; + } + + await Options.Notifications.ApplyChallenge(new AuthenticationChallengeNotification(Context, Options)); + } + + protected override void ApplyResponseGrant() + { + // N/A + } + } +} diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs new file mode 100644 index 0000000000..0403896ae2 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IdentityModel.Tokens; +using System.Net.Http; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Security.DataProtection; +using Microsoft.AspNet.Security.Infrastructure; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.AspNet.Security.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 + { + private readonly ILogger _logger; + + /// + /// 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( + RequestDelegate next, + IServiceProvider services, + ILoggerFactory loggerFactory, + IOptions options, + ConfigureOptions configureOptions) + : base(next, services, options, configureOptions) + { + _logger = loggerFactory.Create(); + 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"; + } + + HttpClient 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(_logger); + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] + private static HttpMessageHandler ResolveHttpMessageHandler(OAuthBearerAuthenticationOptions options) + { + HttpMessageHandler handler = options.BackchannelHttpHandler ?? +#if ASPNET50 + 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.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs new file mode 100644 index 0000000000..08acf5bf45 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens; +using System.Net.Http; +using Microsoft.AspNet.Security.Infrastructure; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.AspNet.Security.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() + { + AuthenticationType = OAuthBearerAuthenticationDefaults.AuthenticationType; + BackchannelTimeout = TimeSpan.FromMinutes(1); + Challenge = OAuthBearerAuthenticationDefaults.AuthenticationType; + 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 ASPNET50 + /// + /// 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; + } + + 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. + /// 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.Security.OAuthBearer/Resources.Designer.cs b/src/Microsoft.AspNet.Security.OAuthBearer/Resources.Designer.cs new file mode 100644 index 0000000000..37abb160e5 --- /dev/null +++ b/src/Microsoft.AspNet.Security.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.Security.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.Security.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.Security.OAuthBearer/Resources.resx b/src/Microsoft.AspNet.Security.OAuthBearer/Resources.resx new file mode 100644 index 0000000000..2a19bea96a --- /dev/null +++ b/src/Microsoft.AspNet.Security.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.Security.OAuthBearer/project.json b/src/Microsoft.AspNet.Security.OAuthBearer/project.json new file mode 100644 index 0000000000..affb7e5b11 --- /dev/null +++ b/src/Microsoft.AspNet.Security.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.Security": "1.0.0-*", + "Microsoft.IdentityModel.Protocol.Extensions": "2.0.0-beta1-*", + "System.IdentityModel.Tokens": "5.0.0-beta1-*" + }, + "frameworks": { + "aspnet50": { + "frameworkAssemblies": { + "System.Net.Http.WebRequest": "", + "System.Net.Http": "" + } + }, + "aspnetcore50": { + "dependencies": { + "System.Net.Http.WinHttpHandler": "4.0.0-beta-*" + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.OpenIdConnect/INonceCache.cs b/src/Microsoft.AspNet.Security.OpenIdConnect/INonceCache.cs new file mode 100644 index 0000000000..3f5255f56d --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIdConnect/INonceCache.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Security.OpenIdConnect +{ + public interface INonceCache + { + string AddNonce(string nonce); + bool TryRemoveNonce(string nonce); + bool HasNonce(string nonce); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.OpenIdConnect/Microsoft.AspNet.Security.OpenIdConnect.kproj b/src/Microsoft.AspNet.Security.OpenIdConnect/Microsoft.AspNet.Security.OpenIdConnect.kproj new file mode 100644 index 0000000000..ed1a12239f --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIdConnect/Microsoft.AspNet.Security.OpenIdConnect.kproj @@ -0,0 +1,29 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 674d128e-83bb-481a-a9d9-6d47872e1fc8 + Microsoft.AspNet.Security.OpenIdConnect + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + True + + + True + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs b/src/Microsoft.AspNet.Security.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs new file mode 100644 index 0000000000..9c7694e560 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIdConnect/Notifications/AuthorizationCodeReceivedNotification.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.OpenIdConnect; +using Microsoft.IdentityModel.Protocols; +using System.Diagnostics.CodeAnalysis; +using System.IdentityModel.Tokens; + +namespace Microsoft.AspNet.Security.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.Security.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs b/src/Microsoft.AspNet.Security.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs new file mode 100644 index 0000000000..5942efb4dc --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIdConnect/OpenIdConnectAuthenticationDefaults.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Security.OpenIdConnect +{ + /// + /// Default values related to OpenIdConnect authentication middleware + /// + public static class OpenIdConnectAuthenticationDefaults + { + /// + /// The default value used for OpenIdConnectAuthenticationOptions.AuthenticationType + /// + public const string AuthenticationType = "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 + /// + internal 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 + /// + internal const string AuthenticationPropertiesKey = "OpenIdConnect.AuthenticationProperties"; + } +} diff --git a/src/Microsoft.AspNet.Security.OpenIdConnect/OpenIdConnectAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.OpenIdConnect/OpenIdConnectAuthenticationExtensions.cs new file mode 100644 index 0000000000..5d4a72b217 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIdConnect/OpenIdConnectAuthenticationExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Security.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 + }); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs new file mode 100644 index 0000000000..bbe59f50d2 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.IdentityModel.Tokens; +using System.Net.Http; +using System.Text; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.DataHandler; +using Microsoft.AspNet.Security.DataHandler.Encoder; +using Microsoft.AspNet.Security.DataHandler.Serializer; +using Microsoft.AspNet.Security.DataProtection; +using Microsoft.AspNet.Security.Infrastructure; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.AspNet.Security.OpenIdConnect +{ + /// + /// ASP.NET middleware for obtaining identities using OpenIdConnect protocol. + /// + public class OpenIdConnectAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly ILogger _logger; + + /// + /// Initializes a + /// + /// The next middleware in the ASP.NET pipeline to invoke + /// The ASP.NET application + /// Configuration options for the middleware + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] + public OpenIdConnectAuthenticationMiddleware( + RequestDelegate next, + IServiceProvider services, + IDataProtectionProvider dataProtectionProvider, + ILoggerFactory loggerFactory, + IOptions externalOptions, + IOptions options, + ConfigureOptions configureOptions) + : base(next, services, options, configureOptions) + { + _logger = loggerFactory.Create(); + + if (string.IsNullOrWhiteSpace(Options.TokenValidationParameters.AuthenticationType)) + { + Options.TokenValidationParameters.AuthenticationType = externalOptions.Options.SignInAsAuthenticationType; + } + + if (Options.StateDataFormat == null) + { + var dataProtector = dataProtectionProvider.CreateDataProtector( + typeof(OpenIdConnectAuthenticationMiddleware).FullName, + typeof(string).FullName, + Options.AuthenticationType, + "v1"); + + Options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + if (Options.StringDataFormat == null) + { + var dataProtector = dataProtectionProvider.CreateDataProtector( + typeof(OpenIdConnectAuthenticationMiddleware).FullName, + typeof(string).FullName, + Options.AuthenticationType, + "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"; + } + + HttpClient 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(_logger); + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] + private static HttpMessageHandler ResolveHttpMessageHandler(OpenIdConnectAuthenticationOptions options) + { + HttpMessageHandler handler = options.BackchannelHttpHandler ?? +#if ASPNET50 + 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; + } + + 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.Security.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs new file mode 100644 index 0000000000..327bbfa599 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Security.Notifications; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.AspNet.Security.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.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs new file mode 100644 index 0000000000..a5f1bcd303 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IdentityModel.Tokens; +using System.Net.Http; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.AspNet.Security.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.AuthenticationType) + { + } + + /// + /// Initializes a new + /// + /// + /// Defaults: + /// AddNonceToRequest: true. + /// AuthenticationMode: . + /// BackchannelTimeout: 1 minute. + /// Caption: . + /// ProtocolValidator: new . + /// RefreshOnIssuerKeyNotFound: true + /// ResponseType: + /// Scope: . + /// TokenValidationParameters: new with AuthenticationType = authenticationType. + /// UseTokenLifetime: true. + /// + /// will be used to when creating the for the AuthenticationType 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 authenticationType) + { + AuthenticationMode = AuthenticationMode.Active; + AuthenticationType = authenticationType; + BackchannelTimeout = TimeSpan.FromMinutes(1); + Caption = OpenIdConnectAuthenticationDefaults.Caption; + ProtocolValidator = new OpenIdConnectProtocolValidator(); + RefreshOnIssuerKeyNotFound = true; + ResponseType = OpenIdConnectResponseTypes.CodeIdToken; + Scope = OpenIdConnectScopes.OpenIdProfile; + TokenValidationParameters = new TokenValidationParameters(); + UseTokenLifetime = true; + } + + /// + /// Gets or sets the Authority to use when making OpenIdConnect calls. + /// + public string Authority { get; set; } + + /// + /// 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; } + +#if ASPNET50 + /// + /// 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("BackchannelTimeout", value, Resources.ArgsException_BackchallelLessThanZero); + } + + _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; } + } + + /// + /// 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; } + + /// + /// 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 discovery endpoint for obtaining metadata + /// + public string MetadataAddress { get; set; } + + /// + /// Gets or sets the expected audience for any received JWT token. + /// + /// + /// The expected audience for any received JWT token. + /// + public string Audience { 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; } + + /// + /// 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; + } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + _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 the 'resource'. + /// + public string Resource { get; 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 AuthenticationType used when creating the . + /// + public string SignInAsAuthenticationType + { + get { return TokenValidationParameters.AuthenticationType; } + set { TokenValidationParameters.AuthenticationType = value; } + } + + /// + /// 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.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs new file mode 100644 index 0000000000..96156f6534 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs @@ -0,0 +1,567 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.IdentityModel.Tokens; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Security.Infrastructure; +using Microsoft.AspNet.Security.Notifications; +using Microsoft.Framework.Logging; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.AspNet.Security.OpenIdConnect +{ + /// + /// A per-request authentication handler for the OpenIdConnectAuthenticationMiddleware. + /// + public class OpenIdConnectAuthenticationHandler : AuthenticationHandler + { + private const string NonceProperty = "N"; + private const string UriSchemeDelimiter = "://"; + private readonly ILogger _logger; + private OpenIdConnectConfiguration _configuration; + + /// + /// Creates a new OpenIdConnectAuthenticationHandler + /// + /// + public OpenIdConnectAuthenticationHandler(ILogger logger) + { + _logger = logger; + } + + 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); + } + + OpenIdConnectMessage 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.Wreply + AuthenticationProperties properties = new AuthenticationProperties(); + if (properties != null && !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) + { + string redirectUri = notification.ProtocolMessage.CreateLogoutRequestUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + _logger.WriteWarning("The logout redirect URI is malformed: " + 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. + /// + /// + protected override async Task ApplyResponseChallengeAsync() + { + if ((Response.StatusCode != 401) || (ChallengeContext == null)) + { + return; + } + + // order for redirect_uri + // 1. challenge.Properties.RedirectUri + // 2. CurrentUri + AuthenticationProperties properties = new AuthenticationProperties(ChallengeContext.Properties); + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = CurrentUri; + } + + // this value will be passed to the AuthorizationCodeReceivedNotification + if (!string.IsNullOrWhiteSpace(Options.RedirectUri)) + { + properties.Dictionary.Add(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey, Options.RedirectUri); + } + + if (_configuration == null && Options.ConfigurationManager != null) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + OpenIdConnectMessage openIdConnectMessage = new OpenIdConnectMessage + { + ClientId = Options.ClientId, + IssuerAddress = _configuration == null ? string.Empty : (_configuration.AuthorizationEndpoint ?? string.Empty), + RedirectUri = Options.RedirectUri, + RequestType = OpenIdConnectRequestType.AuthenticationRequest, + Resource = Options.Resource, + ResponseMode = OpenIdConnectResponseModes.FormPost, + ResponseType = Options.ResponseType, + Scope = Options.Scope, + State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + Uri.EscapeDataString(Options.StateDataFormat.Protect(properties)) + }; + + // TODO - brentschmaltz, if INonceCache is set should we even consider if ProtocolValidator is set? + if (Options.ProtocolValidator.RequireNonce) + { + openIdConnectMessage.Nonce = Options.ProtocolValidator.GenerateNonce(); + if (Options.NonceCache != null) + { + Options.NonceCache.AddNonce(openIdConnectMessage.Nonce); + } + else + { + RememberNonce(openIdConnectMessage.Nonce); + } + } + + var notification = new RedirectToIdentityProviderNotification(Context, Options) + { + ProtocolMessage = openIdConnectMessage + }; + + await Options.Notifications.RedirectToIdentityProvider(notification); + if (!notification.HandledResponse) + { + string redirectUri = notification.ProtocolMessage.CreateAuthenticationRequestUrl(); + if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + _logger.WriteWarning("The authenticate redirect URI is malformed: " + redirectUri); + } + + Response.Redirect(redirectUri); + } + } + + protected override AuthenticationTicket AuthenticateCore() + { + return AuthenticateCoreAsync().GetAwaiter().GetResult(); + } + + /// + /// Invoked to process incoming OpenIdConnect messages. + /// + /// An if successful. + protected override async Task AuthenticateCoreAsync() + { + // 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 openIdConnectMessage = 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) + { + IFormCollection form = await Request.ReadFormAsync(); + Request.Body.Seek(0, SeekOrigin.Begin); + + openIdConnectMessage = new OpenIdConnectMessage(form); + } + + if (openIdConnectMessage == null) + { + return null; + } + + try + { + var messageReceivedNotification = new MessageReceivedNotification(Context, Options) + { + ProtocolMessage = openIdConnectMessage + }; + + await Options.Notifications.MessageReceived(messageReceivedNotification); + if (messageReceivedNotification.HandledResponse) + { + return messageReceivedNotification.AuthenticationTicket; + } + + if (messageReceivedNotification.Skipped) + { + 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. + AuthenticationProperties properties = GetPropertiesFromState(openIdConnectMessage.State); + if (properties == null) + { + _logger.WriteWarning("The state field is missing or invalid."); + return null; + } + + // devs will need to hook AuthenticationFailedNotification to avoid having 'raw' runtime errors displayed to users. + if (!string.IsNullOrWhiteSpace(openIdConnectMessage.Error)) + { + throw new OpenIdConnectProtocolException( + string.Format(CultureInfo.InvariantCulture, + openIdConnectMessage.Error, + Resources.Exception_OpenIdConnectMessageError, openIdConnectMessage.ErrorDescription ?? string.Empty, openIdConnectMessage.ErrorUri ?? string.Empty)); + } + + // code is only accepted with id_token, in this version, hence check for code is inside this if + // OpenIdConnect protocol allows a Code to be received without the id_token + if (string.IsNullOrWhiteSpace(openIdConnectMessage.IdToken)) + { + _logger.WriteWarning("The id_token is missing."); + return null; + } + + var securityTokenReceivedNotification = new SecurityTokenReceivedNotification(Context, Options) + { + ProtocolMessage = openIdConnectMessage + }; + + 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); + } + + // Copy and augment to avoid cross request race conditions for updated configurations. + TokenValidationParameters 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 == null ? new[] { _configuration.Issuer } : validationParameters.ValidIssuers.Concat(new[] { _configuration.Issuer })); + } + + validationParameters.IssuerSigningKeys = (validationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : validationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys)); + } + + AuthenticationTicket ticket; + SecurityToken validatedToken = null; + ClaimsPrincipal principal = null; + JwtSecurityToken jwt = null; + + foreach (var validator in Options.SecurityTokenValidators) + { + if (validator.CanReadToken(openIdConnectMessage.IdToken)) + { + principal = validator.ValidateToken(openIdConnectMessage.IdToken, validationParameters, out validatedToken); + jwt = validatedToken as JwtSecurityToken; + if (jwt == null) + { + throw new InvalidOperationException("Validated Security Token must be a JwtSecurityToken was: " + (validatedToken == null ? "null" : validatedToken.GetType().ToString())); + } + } + } + + if (validatedToken == null) + { + throw new InvalidOperationException("No SecurityTokenValidator found for token: " + openIdConnectMessage.IdToken); + } + + ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationType); + if (!string.IsNullOrWhiteSpace(openIdConnectMessage.SessionState)) + { + ticket.Properties.Dictionary[OpenIdConnectSessionProperties.SessionState] = openIdConnectMessage.SessionState; + } + + if (_configuration != null && !string.IsNullOrWhiteSpace(_configuration.CheckSessionIframe)) + { + ticket.Properties.Dictionary[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe; + } + + if (Options.UseTokenLifetime) + { + // Override any session persistence to match the token lifetime. + DateTime issued = validatedToken.ValidFrom; + if (issued != DateTime.MinValue) + { + ticket.Properties.IssuedUtc = issued; + } + + DateTime expires = validatedToken.ValidTo; + if (expires != DateTime.MinValue) + { + ticket.Properties.ExpiresUtc = expires; + } + + ticket.Properties.AllowRefresh = false; + } + + var securityTokenValidatedNotification = new SecurityTokenValidatedNotification(Context, Options) + { + AuthenticationTicket = ticket, + ProtocolMessage = openIdConnectMessage + }; + + await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification); + if (securityTokenValidatedNotification.HandledResponse) + { + return securityTokenValidatedNotification.AuthenticationTicket; + } + + if (securityTokenValidatedNotification.Skipped) + { + return null; + } + + var protocolValidationContext = new OpenIdConnectProtocolValidationContext + { + AuthorizationCode = openIdConnectMessage.Code, + Nonce = RetrieveNonce(jwt.Payload.Nonce), + }; + + Options.ProtocolValidator.Validate(jwt, protocolValidationContext); + if (openIdConnectMessage.Code != null) + { + var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options) + { + AuthenticationTicket = ticket, + Code = openIdConnectMessage.Code, + JwtSecurityToken = jwt, + ProtocolMessage = openIdConnectMessage, + RedirectUri = ticket.Properties.Dictionary.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey) ? + ticket.Properties.Dictionary[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey] : string.Empty, + }; + + await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification); + if (authorizationCodeReceivedNotification.HandledResponse) + { + return authorizationCodeReceivedNotification.AuthenticationTicket; + } + + if (authorizationCodeReceivedNotification.Skipped) + { + return null; + } + } + + return ticket; + } + catch (Exception exception) + { + _logger.WriteError("Exception occurred while processing message", 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 = openIdConnectMessage, + Exception = exception + }; + + await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); + if (authenticationFailedNotification.HandledResponse) + { + return authenticationFailedNotification.AuthenticationTicket; + } + + if (authenticationFailedNotification.Skipped) + { + 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 RememberNonce(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.IsSecure + }); + } + + /// + /// Searches for a matching nonce. + /// + /// the nonce that was found in the jwt token. + /// 'nonceExpectedValue' 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 RetrieveNonce(string nonceExpectedValue) + { + if (nonceExpectedValue == null) + { + return null; + } + + foreach (var nonceKey in Request.Cookies.Keys) + { + if (nonceKey.StartsWith(OpenIdConnectAuthenticationDefaults.CookieNoncePrefix)) + { + try + { + string nonceDecodedValue = Options.StringDataFormat.Unprotect(nonceKey.Substring(OpenIdConnectAuthenticationDefaults.CookieNoncePrefix.Length, nonceKey.Length - OpenIdConnectAuthenticationDefaults.CookieNoncePrefix.Length)); + if (nonceDecodedValue == nonceExpectedValue) + { + var cookieOptions = new CookieOptions + { + HttpOnly = true, + Secure = Request.IsSecure + }; + + Response.Cookies.Delete(nonceKey, cookieOptions); + return nonceExpectedValue; + } + } + catch (Exception ex) + { + _logger.WriteWarning("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> + int startIndex = 0; + if (string.IsNullOrWhiteSpace(state) || (startIndex = state.IndexOf(OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey, StringComparison.Ordinal)) == -1) + { + return null; + } + + int authenticationIndex = startIndex + OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey.Length; + if (authenticationIndex == -1 || authenticationIndex == state.Length || state[authenticationIndex] != '=') + { + return null; + } + + // scan rest of string looking for '&' + authenticationIndex++; + int 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() + { + AuthenticationTicket ticket = await AuthenticateAsync(); + + if (ticket != null) + { + if (ticket.Principal != null) + { + Request.HttpContext.Response.SignIn(ticket.Properties, ticket.Principal.Identities); + } + + // 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.Security.OpenIdConnect/Resources.Designer.cs b/src/Microsoft.AspNet.Security.OpenIdConnect/Resources.Designer.cs new file mode 100644 index 0000000000..f5bfc6044a --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIdConnect/Resources.Designer.cs @@ -0,0 +1,101 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34014 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Security.OpenIdConnect { + using System; + using System.Reflection; + + + + /// + /// 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.Owin.Security.OpenIdConnect.Resources", 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 BackchannelTimeout cannot be less or equal to TimeSpan.Zero.. + /// + internal static string ArgsException_BackchallelLessThanZero { + get { + return ResourceManager.GetString("ArgsException_BackchallelLessThanZero", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "OpenIdConnectMessage.Error was not null, indicating an error. Error: '{0}'. Error_Description (may be empty): '{1}'. Error_Uri (may be empty): '{2}'.". + /// + internal static string Exception_OpenIdConnectMessageError { + get { + return ResourceManager.GetString("Exception_OpenIdConnectMessageError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OIDC_20001: The query string for Logout is not a well formed URI. The runtime cannot redirect. Redirect uri: '{0}'.. + /// + internal static string Exception_RedirectUri_LogoutQueryString_IsNotWellFormed { + get { + return ResourceManager.GetString("Exception_RedirectUri_LogoutQueryString_IsNotWellFormed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a 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.Security.OpenIdConnect/project.json b/src/Microsoft.AspNet.Security.OpenIdConnect/project.json new file mode 100644 index 0000000000..728e82b194 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIdConnect/project.json @@ -0,0 +1,19 @@ +{ + "version": "1.0.0-*", + "dependencies": { + "Microsoft.AspNet.Security": "1.0.0-*", + "Microsoft.IdentityModel.Protocol.Extensions": "2.0.0.0-beta1-*" + }, + "frameworks": { + "aspnet50": { + "frameworkAssemblies": { + "System.Net.Http.WebRequest": "" + } + }, + "aspnetcore50": { + "dependencies": { + "System.Net.Http.WinHttpHandler": "4.0.0-beta-*" + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationHandler.cs index 7717642a1d..46dcd43f4c 100644 --- a/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationHandler.cs @@ -10,6 +10,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Core.Collections; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.Security.Infrastructure; using Microsoft.AspNet.Security.Twitter.Messages; @@ -275,7 +276,7 @@ namespace Microsoft.AspNet.Security.Twitter response.EnsureSuccessStatusCode(); string responseText = await response.Content.ReadAsStringAsync(); - IFormCollection responseParameters = FormHelpers.ParseForm(responseText); + IFormCollection 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 }; @@ -351,7 +352,7 @@ namespace Microsoft.AspNet.Security.Twitter string responseText = await response.Content.ReadAsStringAsync(); - IFormCollection responseParameters = FormHelpers.ParseForm(responseText); + IFormCollection responseParameters = new FormCollection(FormReader.ReadForm(responseText)); return new AccessToken { diff --git a/src/Microsoft.AspNet.Security/AuthenticationOptions.cs b/src/Microsoft.AspNet.Security/AuthenticationOptions.cs index e25383cfe3..411e134819 100644 --- a/src/Microsoft.AspNet.Security/AuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security/AuthenticationOptions.cs @@ -1,12 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; using Microsoft.AspNet.Http.Security; -using Microsoft.AspNet.HttpFeature.Security; -using Microsoft.AspNet.PipelineCore.Security; namespace Microsoft.AspNet.Security { diff --git a/src/Microsoft.AspNet.Security/AuthenticationTicket.cs b/src/Microsoft.AspNet.Security/AuthenticationTicket.cs index b9457aa91f..82128f10cd 100644 --- a/src/Microsoft.AspNet.Security/AuthenticationTicket.cs +++ b/src/Microsoft.AspNet.Security/AuthenticationTicket.cs @@ -1,14 +1,8 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.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 { @@ -28,11 +22,34 @@ namespace Microsoft.AspNet.Security Properties = properties ?? new AuthenticationProperties(); } + /// + /// 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 authenticationType) + { + AuthenticationType = authenticationType; + Principal = principal; + Properties = properties ?? new AuthenticationProperties(); + } + + /// + /// Gets the authentication type. + /// + public string AuthenticationType { get; private set; } + /// /// Gets the authenticated user identity. /// public ClaimsIdentity Identity { get; private set; } + /// + /// Gets the claims-principal with authenticated user identities. + /// + public ClaimsPrincipal Principal{ get; private set; } + /// /// Additional state values for the authentication session. /// diff --git a/src/Microsoft.AspNet.Security/AuthorizationContext.cs b/src/Microsoft.AspNet.Security/AuthorizationContext.cs new file mode 100644 index 0000000000..0c6dc06197 --- /dev/null +++ b/src/Microsoft.AspNet.Security/AuthorizationContext.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Security +{ + /// + /// Contains authorization information used by . + /// + public class AuthorizationContext + { + private HashSet _pendingRequirements = new HashSet(); + private bool _failCalled; + private bool _succeedCalled; + + public AuthorizationContext( + [NotNull] AuthorizationPolicy policy, + HttpContext context, + object resource) + { + Policy = policy; + Context = context; + Resource = resource; + foreach (var req in Policy.Requirements) + { + _pendingRequirements.Add(req); + } + } + + public AuthorizationPolicy Policy { get; private set; } + public ClaimsPrincipal User { get { return Context.User; } } + public HttpContext Context { 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.Security/AuthorizationHandler.cs b/src/Microsoft.AspNet.Security/AuthorizationHandler.cs new file mode 100644 index 0000000000..d913318700 --- /dev/null +++ b/src/Microsoft.AspNet.Security/AuthorizationHandler.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Security +{ + // Music store use case + + // await AuthorizeAsync(user, "Edit", albumInstance); + + // No policy name needed because this is auto based on resource (operation is the policy name) + //RegisterOperation which auto generates the policy for Authorize + //bool AuthorizeAsync(ClaimsPrincipal, string operation, TResource instance) + //bool AuthorizeAsync(IAuthorization, ClaimsPrincipal, string operation, TResource instance) + public abstract class AuthorizationHandler : IAuthorizationHandler + where TRequirement : IAuthorizationRequirement + { + public async Task HandleAsync(AuthorizationContext context) + { + foreach (var req in context.Policy.Requirements.OfType()) + { + if (await CheckAsync(context, req)) + { + context.Succeed(req); + } + else + { + context.Fail(); + } + } + } + + public abstract Task CheckAsync(AuthorizationContext context, TRequirement requirement); + } + + // TODO: + //public abstract class AuthorizationHandler : AuthorizationHandler + // where TResource : class + // where TRequirement : IAuthorizationRequirement + //{ + // public override Task HandleAsync(AuthorizationContext context) + // { + // var resource = context.Resource as TResource; + // if (resource != null) + // { + // return HandleAsync(context, resource); + // } + + // return Task.FromResult(0); + + // } + + // public abstract Task HandleAsync(AuthorizationContext context, TResource resource); + //} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/AuthorizationOptions.cs b/src/Microsoft.AspNet.Security/AuthorizationOptions.cs new file mode 100644 index 0000000000..8a53e574d2 --- /dev/null +++ b/src/Microsoft.AspNet.Security/AuthorizationOptions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Security +{ + 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.Security/AuthorizationPolicy.cs b/src/Microsoft.AspNet.Security/AuthorizationPolicy.cs index a58ae858ae..d142eb1b60 100644 --- a/src/Microsoft.AspNet.Security/AuthorizationPolicy.cs +++ b/src/Microsoft.AspNet.Security/AuthorizationPolicy.cs @@ -2,31 +2,18 @@ // 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; namespace Microsoft.AspNet.Security { - /// - /// This class provides a base implementation for - /// - public abstract class AuthorizationPolicy : IAuthorizationPolicy + public class AuthorizationPolicy { - public int Order { get; set; } - - public virtual Task ApplyingAsync(AuthorizationPolicyContext context) + public AuthorizationPolicy(IEnumerable requirements, IEnumerable activeAuthenticationTypes) { - return Task.FromResult(0); + Requirements = requirements; + ActiveAuthenticationTypes = activeAuthenticationTypes; } - public virtual Task ApplyAsync(AuthorizationPolicyContext context) - { - return Task.FromResult(0); - } - - public virtual Task AppliedAsync(AuthorizationPolicyContext context) - { - return Task.FromResult(0); - } + public IEnumerable Requirements { get; private set; } + public IEnumerable ActiveAuthenticationTypes { get; private set; } } } diff --git a/src/Microsoft.AspNet.Security/AuthorizationPolicyBuilder.cs b/src/Microsoft.AspNet.Security/AuthorizationPolicyBuilder.cs new file mode 100644 index 0000000000..7b8617f543 --- /dev/null +++ b/src/Microsoft.AspNet.Security/AuthorizationPolicyBuilder.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; + +namespace Microsoft.AspNet.Security +{ + public class AuthorizationPolicyBuilder + { + public AuthorizationPolicyBuilder(params string[] activeAuthenticationTypes) + { + AddAuthenticationTypes(activeAuthenticationTypes); + } + + public AuthorizationPolicyBuilder(AuthorizationPolicy policy) + { + Combine(policy); + } + + public IList Requirements { get; set; } = new List(); + public IList ActiveAuthenticationTypes { get; set; } = new List(); + + public AuthorizationPolicyBuilder AddAuthenticationTypes(params string[] activeAuthTypes) + { + foreach (var authType in activeAuthTypes) + { + ActiveAuthenticationTypes.Add(authType); + } + return this; + } + + public AuthorizationPolicyBuilder AddRequirements(params IAuthorizationRequirement[] requirements) + { + foreach (var req in requirements) + { + Requirements.Add(req); + } + return this; + } + + public AuthorizationPolicyBuilder Combine(AuthorizationPolicy policy) + { + AddAuthenticationTypes(policy.ActiveAuthenticationTypes.ToArray()); + AddRequirements(policy.Requirements.ToArray()); + return this; + } + + public AuthorizationPolicyBuilder RequiresClaim([NotNull] string claimType, params string[] requiredValues) + { + Requirements.Add(new ClaimsAuthorizationRequirement + { + ClaimType = claimType, + AllowedValues = requiredValues + }); + return this; + } + + public AuthorizationPolicyBuilder RequiresClaim([NotNull] string claimType) + { + Requirements.Add(new ClaimsAuthorizationRequirement + { + ClaimType = claimType, + AllowedValues = null + }); + return this; + } + + public AuthorizationPolicyBuilder RequiresRole([NotNull] params string[] roles) + { + RequiresClaim(ClaimTypes.Role, roles); + return this; + } + + public AuthorizationPolicyBuilder RequireAuthenticatedUser() + { + Requirements.Add(new DenyAnonymousAuthorizationRequirement()); + return this; + } + + public AuthorizationPolicy Build() + { + return new AuthorizationPolicy(Requirements, ActiveAuthenticationTypes); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/AuthorizationPolicyContext.cs b/src/Microsoft.AspNet.Security/AuthorizationPolicyContext.cs deleted file mode 100644 index b394bfcb68..0000000000 --- a/src/Microsoft.AspNet.Security/AuthorizationPolicyContext.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.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 17ec58196d..0000000000 --- a/src/Microsoft.AspNet.Security/AuthorizationServiceExtensions.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.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([NotNull] 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([NotNull] 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([NotNull] 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([NotNull] this IAuthorizationService service, Claim claim, ClaimsPrincipal user, object resource) - { - return service.Authorize(new Claim[] { claim }, user, resource); - } - - /// - /// 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. - public static Task AuthorizeAsync([NotNull] this IAuthorizationService service, IEnumerable claims, ClaimsPrincipal user) - { - return service.AuthorizeAsync(claims, user, null); - } - - /// - /// 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. - public static bool Authorize([NotNull] this IAuthorizationService service, IEnumerable claims, ClaimsPrincipal user) - { - return service.Authorize(claims, user, null); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/ClaimsAuthorizationHandler.cs b/src/Microsoft.AspNet.Security/ClaimsAuthorizationHandler.cs new file mode 100644 index 0000000000..9f3f58c3ac --- /dev/null +++ b/src/Microsoft.AspNet.Security/ClaimsAuthorizationHandler.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Security +{ + public class ClaimsAuthorizationHandler : AuthorizationHandler + { + public override Task CheckAsync(AuthorizationContext context, ClaimsAuthorizationRequirement requirement) + { + if (context.Context.User == null) + { + return Task.FromResult(false); + } + + bool found = false; + if (requirement.AllowedValues == null || !requirement.AllowedValues.Any()) + { + found = context.Context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase)); + } + else + { + found = context.Context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase) + && requirement.AllowedValues.Contains(c.Value, StringComparer.Ordinal)); + } + return Task.FromResult(found); + } + } +} diff --git a/src/Microsoft.AspNet.Security/ClaimsAuthorizationRequirement.cs b/src/Microsoft.AspNet.Security/ClaimsAuthorizationRequirement.cs new file mode 100644 index 0000000000..8ec5e7c7e1 --- /dev/null +++ b/src/Microsoft.AspNet.Security/ClaimsAuthorizationRequirement.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Security +{ + // 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 : IAuthorizationRequirement + { + public string ClaimType { get; set; } + public IEnumerable AllowedValues { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security/DefaultAuthorizationService.cs b/src/Microsoft.AspNet.Security/DefaultAuthorizationService.cs index 0b05a6d3dd..d20cf65445 100644 --- a/src/Microsoft.AspNet.Security/DefaultAuthorizationService.cs +++ b/src/Microsoft.AspNet.Security/DefaultAuthorizationService.cs @@ -1,101 +1,67 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.Framework.OptionsModel; namespace Microsoft.AspNet.Security { public class DefaultAuthorizationService : IAuthorizationService { - private readonly IList _policies; - public int MaxRetries = 99; + private readonly IList _handlers; + private readonly AuthorizationOptions _options; - public DefaultAuthorizationService(IEnumerable policies) + public DefaultAuthorizationService(IOptions options, IEnumerable handlers) { - if (policies == null) - { - _policies = Enumerable.Empty().ToArray(); - } - else - { - _policies = policies.OrderBy(x => x.Order).ToArray(); - } + _handlers = handlers.ToArray(); + _options = options.Options; } - public async Task AuthorizeAsync(IEnumerable claims, ClaimsPrincipal user, object resource) + public Task AuthorizeAsync([NotNull] string policyName, HttpContext context, object resource = null) { - var context = new AuthorizationPolicyContext(claims, user, resource); - - foreach (var policy in _policies) + var policy = _options.GetPolicy(policyName); + if (policy == null) { - await policy.ApplyingAsync(context); + return Task.FromResult(false); } + return AuthorizeAsync(policy, context, resource); + } - // we only apply the policies for a limited number of times to prevent - // infinite loops - - int retries; - for (retries = 0; retries < MaxRetries; retries++) + public async Task AuthorizeAsync([NotNull] AuthorizationPolicy policy, [NotNull] HttpContext context, object resource = null) + { + var user = context.User; + try { - // we don't need to check for owned claims if the permission is already granted - if (!context.Authorized) + // Generate the user identities if policy specified the AuthTypes + if (policy.ActiveAuthenticationTypes != null && policy.ActiveAuthenticationTypes.Any() ) { - if (context.User != null) + var principal = new ClaimsPrincipal(); + + var results = await context.AuthenticateAsync(policy.ActiveAuthenticationTypes); + // REVIEW: re requesting the identities fails for MVC currently, so we only request if not found + foreach (var result in results) { - if (ClaimsMatch(context.Claims, context.UserClaims)) - { - context.Authorized = true; - } + principal.AddIdentity(result.Identity); } + context.User = principal; } - // reset the retry flag - context.Retry = false; + var authContext = new AuthorizationContext(policy, context, resource); - // give a chance for policies to change claims or the grant - foreach (var policy in _policies) + foreach (var handler in _handlers) { - await policy.ApplyAsync(context); - } - - // if no policies have changed the context, stop checking - if (!context.Retry) - { - break; + await handler.HandleAsync(authContext); } + return authContext.HasSucceeded; } - - if (retries == MaxRetries) + finally { - throw new InvalidOperationException("Too many authorization retries."); + context.User = user; } - - 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).GetAwaiter().GetResult(); - } - - 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/DenyAnonymousAuthorizationHandler.cs b/src/Microsoft.AspNet.Security/DenyAnonymousAuthorizationHandler.cs new file mode 100644 index 0000000000..878a32ec92 --- /dev/null +++ b/src/Microsoft.AspNet.Security/DenyAnonymousAuthorizationHandler.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Security +{ + public class DenyAnonymousAuthorizationHandler : AuthorizationHandler + { + public override Task CheckAsync(AuthorizationContext context, DenyAnonymousAuthorizationRequirement requirement) + { + var user = context.User; + var userIsAnonymous = + user == null || + user.Identity == null || + !user.Identity.IsAuthenticated; + return Task.FromResult(!userIsAnonymous); + } + } +} diff --git a/src/Microsoft.AspNet.Security/DenyAnonymousAuthorizationRequirement.cs b/src/Microsoft.AspNet.Security/DenyAnonymousAuthorizationRequirement.cs new file mode 100644 index 0000000000..286d5fd69a --- /dev/null +++ b/src/Microsoft.AspNet.Security/DenyAnonymousAuthorizationRequirement.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Security; + +namespace Microsoft.AspNet.Security +{ + public class DenyAnonymousAuthorizationRequirement : IAuthorizationRequirement { } +} diff --git a/src/Microsoft.AspNet.Security/IAuthorizationPolicy.cs b/src/Microsoft.AspNet.Security/IAuthorizationHandler.cs similarity index 50% rename from src/Microsoft.AspNet.Security/IAuthorizationPolicy.cs rename to src/Microsoft.AspNet.Security/IAuthorizationHandler.cs index 0f121d558a..975a305b86 100644 --- a/src/Microsoft.AspNet.Security/IAuthorizationPolicy.cs +++ b/src/Microsoft.AspNet.Security/IAuthorizationHandler.cs @@ -5,11 +5,9 @@ using System.Threading.Tasks; namespace Microsoft.AspNet.Security { - public interface IAuthorizationPolicy + public interface IAuthorizationHandler { - int Order { get; set; } - Task ApplyingAsync(AuthorizationPolicyContext context); - Task ApplyAsync(AuthorizationPolicyContext context); - Task AppliedAsync(AuthorizationPolicyContext context); + Task HandleAsync(AuthorizationContext context); + //void Handle(AuthorizationContext context); } } diff --git a/src/Microsoft.AspNet.Security/IAuthorizationRequirement.cs b/src/Microsoft.AspNet.Security/IAuthorizationRequirement.cs new file mode 100644 index 0000000000..bd25247df2 --- /dev/null +++ b/src/Microsoft.AspNet.Security/IAuthorizationRequirement.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Security +{ + public interface IAuthorizationRequirement + { + } +} diff --git a/src/Microsoft.AspNet.Security/IAuthorizationService.cs b/src/Microsoft.AspNet.Security/IAuthorizationService.cs index 9fcef75a2f..8bcafda3d7 100644 --- a/src/Microsoft.AspNet.Security/IAuthorizationService.cs +++ b/src/Microsoft.AspNet.Security/IAuthorizationService.cs @@ -1,34 +1,32 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Collections.Generic; -using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Security { /// - /// Checks claims based permissions for a user. + /// Checks policy based permissions for a user /// public interface IAuthorizationService { /// - /// Checks if a user has specific claims for a specific context obj. + /// Checks if a user meets a specific authorization policy /// - /// 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); + /// The policy to check against a specific context. + /// The HttpContext to check the policy against. + /// The resource the policy should be checked with. + /// true when the user fulfills the policy, false otherwise. + Task AuthorizeAsync(string policyName, HttpContext context, object resource = null); /// - /// Checks if a user has specific claims for a specific context obj. + /// Checks if a user meets a specific authorization policy /// - /// 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); - + /// The policy to check against a specific context. + /// The HttpContext to check the policy against. + /// The resource the policy should be checked with. + /// true when the user fulfills the policy, false otherwise. + Task AuthorizeAsync(AuthorizationPolicy policy, HttpContext context, object resource = null); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler.cs b/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler.cs index a5fd9876cb..d7a0db8fcd 100644 --- a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler.cs @@ -12,7 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Security; -using Microsoft.AspNet.HttpFeature.Security; +using Microsoft.AspNet.Http.Interfaces.Security; using Microsoft.AspNet.Security.DataHandler.Encoder; using Microsoft.Framework.Logging; @@ -60,6 +60,8 @@ namespace Microsoft.AspNet.Security.Infrastructure public IAuthenticationHandler PriorHandler { get; set; } + public bool Faulted { get; set; } + protected async Task BaseInitializeAsync(AuthenticationOptions options, HttpContext context) { _baseOptions = options; @@ -94,12 +96,29 @@ namespace Microsoft.AspNet.Security.Infrastructure } /// - /// 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(); } @@ -217,15 +236,29 @@ namespace Microsoft.AspNet.Security.Infrastructure 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); - }).GetAwaiter().GetResult(); // 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() @@ -240,13 +273,27 @@ namespace Microsoft.AspNet.Security.Infrastructure /// 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; + } } /// diff --git a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationMiddleware.cs index b7fb77fe81..a703619de6 100644 --- a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationMiddleware.cs @@ -41,9 +41,25 @@ namespace Microsoft.AspNet.Security.Infrastructure { AuthenticationHandler handler = CreateHandler(); await handler.Initialize(Options, context); - if (!await handler.InvokeAsync()) + try { - await _next(context); + 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(); } diff --git a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenReceiveContext.cs b/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenReceiveContext.cs index af077b551d..2e68dc3080 100644 --- a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenReceiveContext.cs +++ b/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenReceiveContext.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; using Microsoft.AspNet.Http; using Microsoft.AspNet.Security.Notifications; @@ -10,15 +8,11 @@ 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; } @@ -26,11 +20,6 @@ namespace Microsoft.AspNet.Security.Infrastructure 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/HttpContextExtensions.cs b/src/Microsoft.AspNet.Security/Infrastructure/HttpContextExtensions.cs index 59c29fe82b..5ba7a9ce4f 100644 --- a/src/Microsoft.AspNet.Security/Infrastructure/HttpContextExtensions.cs +++ b/src/Microsoft.AspNet.Security/Infrastructure/HttpContextExtensions.cs @@ -2,8 +2,8 @@ // 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.HttpFeature.Security; -using Microsoft.AspNet.PipelineCore.Security; +using Microsoft.AspNet.Http.Core.Security; +using Microsoft.AspNet.Http.Interfaces.Security; namespace Microsoft.AspNet.Security.Infrastructure { diff --git a/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs b/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs index 9e50ecf4f7..5d232426f6 100644 --- a/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs +++ b/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs @@ -1,19 +1,19 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - using System; +using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Security.Notifications { - public class AuthenticationFailedNotification + public class AuthenticationFailedNotification : BaseNotification { - public AuthenticationFailedNotification() + public AuthenticationFailedNotification(HttpContext context, TOptions options) : base(context, options) { } - 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/BaseNotification.cs b/src/Microsoft.AspNet.Security/Notifications/BaseNotification.cs new file mode 100644 index 0000000000..d1ea6fef75 --- /dev/null +++ b/src/Microsoft.AspNet.Security/Notifications/BaseNotification.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Security.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/MessageReceivedNotification.cs b/src/Microsoft.AspNet.Security/Notifications/MessageReceivedNotification.cs index e270289179..f583746c3c 100644 --- a/src/Microsoft.AspNet.Security/Notifications/MessageReceivedNotification.cs +++ b/src/Microsoft.AspNet.Security/Notifications/MessageReceivedNotification.cs @@ -1,16 +1,21 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Security.Notifications { - public class MessageReceivedNotification + public class MessageReceivedNotification : BaseNotification { - public MessageReceivedNotification() + public MessageReceivedNotification(HttpContext context, TOptions options) : base(context, options) { } - public bool Cancel { get; set; } 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.Security/Notifications/NotificationResultState.cs b/src/Microsoft.AspNet.Security/Notifications/NotificationResultState.cs new file mode 100644 index 0000000000..78d6c85caa --- /dev/null +++ b/src/Microsoft.AspNet.Security/Notifications/NotificationResultState.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +using System; + +namespace Microsoft.AspNet.Security.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.Security/Notifications/RedirectFromIdentityProviderNotification.cs b/src/Microsoft.AspNet.Security/Notifications/RedirectFromIdentityProviderNotification.cs index d0bd97a76b..768b384a9d 100644 --- a/src/Microsoft.AspNet.Security/Notifications/RedirectFromIdentityProviderNotification.cs +++ b/src/Microsoft.AspNet.Security/Notifications/RedirectFromIdentityProviderNotification.cs @@ -1,17 +1,21 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Security.Notifications { - public class RedirectFromIdentityProviderNotification + public class RedirectFromIdentityProviderNotification : BaseNotification { - public AuthenticationTicket AuthenticationTicket { get; set; } + public RedirectFromIdentityProviderNotification(HttpContext context, TOptions options) + : base(context, options) + { + } public string SignInAsAuthenticationType { get; set; } - public bool Cancel { get; set; } - public bool IsRequestCompleted { get; set; } + + public TMessage ProtocolMessage { get; set; } } } diff --git a/src/Microsoft.AspNet.Security/Notifications/RedirectToIdentityProviderNotification.cs b/src/Microsoft.AspNet.Security/Notifications/RedirectToIdentityProviderNotification.cs index 467731fa0c..524664d7ed 100644 --- a/src/Microsoft.AspNet.Security/Notifications/RedirectToIdentityProviderNotification.cs +++ b/src/Microsoft.AspNet.Security/Notifications/RedirectToIdentityProviderNotification.cs @@ -1,16 +1,16 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Security.Notifications { - public class RedirectToIdentityProviderNotification + public class RedirectToIdentityProviderNotification : BaseNotification { - public RedirectToIdentityProviderNotification() + public RedirectToIdentityProviderNotification(HttpContext context, TOptions options) : base(context, options) { } - 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 index f8aa2adef0..7db29788a2 100644 --- a/src/Microsoft.AspNet.Security/Notifications/SecurityTokenReceivedNotification.cs +++ b/src/Microsoft.AspNet.Security/Notifications/SecurityTokenReceivedNotification.cs @@ -1,16 +1,18 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Security.Notifications { - public class SecurityTokenReceivedNotification + public class SecurityTokenReceivedNotification : BaseNotification { - public SecurityTokenReceivedNotification() + public SecurityTokenReceivedNotification(HttpContext context, TOptions options) : base(context, options) { } - public bool Cancel { get; set; } public string SecurityToken { get; set; } + + public TMessage ProtocolMessage { 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 index 400b8b5814..bdef232a71 100644 --- a/src/Microsoft.AspNet.Security/Notifications/SecurityTokenValidatedNotification.cs +++ b/src/Microsoft.AspNet.Security/Notifications/SecurityTokenValidatedNotification.cs @@ -1,16 +1,16 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Security.Notifications { - public class SecurityTokenValidatedNotification + public class SecurityTokenValidatedNotification : BaseNotification { - public SecurityTokenValidatedNotification() + public SecurityTokenValidatedNotification(HttpContext context, TOptions options) : base(context, options) { } - public AuthenticationTicket AuthenticationTicket { get; set; } - public bool Cancel { get; set; } + public TMessage ProtocolMessage { get; set; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Security/PassThroughAuthorizationHandler.cs b/src/Microsoft.AspNet.Security/PassThroughAuthorizationHandler.cs new file mode 100644 index 0000000000..837b2f2676 --- /dev/null +++ b/src/Microsoft.AspNet.Security/PassThroughAuthorizationHandler.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Security +{ + public class PassThroughAuthorizationHandler : IAuthorizationHandler + { + public async Task HandleAsync(AuthorizationContext context) + { + foreach (var handler in context.Policy.Requirements.OfType()) + { + await handler.HandleAsync(context); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security/ServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Security/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..41b0978cdf --- /dev/null +++ b/src/Microsoft.AspNet.Security/ServiceCollectionExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Security; +using Microsoft.Framework.ConfigurationModel; + +namespace Microsoft.Framework.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection ConfigureAuthorization([NotNull] this IServiceCollection services, [NotNull] Action configure) + { + return services.Configure(configure); + } + + // Review: Need UseDefaultSubkey parameter? + public static IServiceCollection AddAuthorization([NotNull] this IServiceCollection services, IConfiguration config = null, Action configureOptions = null) + { + var describe = new ServiceDescriber(config); + services.AddOptions(config); + services.TryAdd(describe.Transient()); + services.Add(describe.Transient()); + services.Add(describe.Transient()); + services.Add(describe.Transient()); + if (configureOptions != null) + { + services.Configure(configureOptions); + } + return services; + } + } +} diff --git a/src/Microsoft.AspNet.Security/project.json b/src/Microsoft.AspNet.Security/project.json index cac89fbad7..b147d3031c 100644 --- a/src/Microsoft.AspNet.Security/project.json +++ b/src/Microsoft.AspNet.Security/project.json @@ -3,8 +3,8 @@ "description": "ASP.NET 5 common types used by the various authentication middleware.", "dependencies": { "Microsoft.AspNet.RequestContainer": "1.0.0-*", - "Microsoft.AspNet.HttpFeature": { "version": "1.0.0-*", "type": "build" }, - "Microsoft.AspNet.PipelineCore": "1.0.0-*", + "Microsoft.AspNet.Http.Interfaces": { "version": "1.0.0-*", "type": "build" }, + "Microsoft.AspNet.Http.Core": "1.0.0-*", "Microsoft.AspNet.Security.DataProtection": "1.0.0-*", "Microsoft.Framework.Logging": "1.0.0-*" }, diff --git a/test/Microsoft.AspNet.Security.Test/Cookies/CookieMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/Cookies/CookieMiddlewareTests.cs index b103a7c757..c66d571b28 100644 --- a/test/Microsoft.AspNet.Security.Test/Cookies/CookieMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Security.Test/Cookies/CookieMiddlewareTests.cs @@ -137,21 +137,11 @@ namespace Microsoft.AspNet.Security.Cookies options.CookieDomain = "another.com"; options.CookieSecure = CookieSecureOption.Always; options.CookieHttpOnly = true; - }, SignInAsAlice); + }, SignInAsAlice, new Uri("http://example.com/base")); - Transaction transaction1 = await SendAsync(server1, "http://example.com/testpath"); - - TestServer server2 = CreateServer(options => - { - options.CookieName = "SecondCookie"; - options.CookieSecure = CookieSecureOption.Never; - options.CookieHttpOnly = false; - }, SignInAsAlice); - - Transaction transaction2 = await SendAsync(server2, "http://example.com/testpath"); + Transaction transaction1 = await SendAsync(server1, "http://example.com/base/testpath"); string setCookie1 = transaction1.SetCookie; - string setCookie2 = transaction2.SetCookie; setCookie1.ShouldContain("TestCookie="); setCookie1.ShouldContain(" path=/foo"); @@ -159,7 +149,19 @@ namespace Microsoft.AspNet.Security.Cookies setCookie1.ShouldContain(" secure"); setCookie1.ShouldContain(" HttpOnly"); + TestServer server2 = CreateServer(options => + { + options.CookieName = "SecondCookie"; + options.CookieSecure = CookieSecureOption.Never; + options.CookieHttpOnly = false; + }, SignInAsAlice, new Uri("http://example.com/base")); + + Transaction transaction2 = await SendAsync(server2, "http://example.com/base/testpath"); + + string setCookie2 = transaction2.SetCookie; + setCookie2.ShouldContain("SecondCookie="); + setCookie2.ShouldContain(" path=/base"); setCookie2.ShouldNotContain(" domain="); setCookie2.ShouldNotContain(" secure"); setCookie2.ShouldNotContain(" HttpOnly"); @@ -343,6 +345,25 @@ namespace Microsoft.AspNet.Security.Cookies responded.Single().ShouldContain("\"location\""); } + [Fact] + public async Task CookieUsesPathBaseByDefault() + { + var clock = new TestClock(); + TestServer server = CreateServer(options => + { + }, + context => + { + Assert.Equal(new PathString("/base"), context.Request.PathBase); + context.Response.SignIn(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))); + return Task.FromResult(null); + }, + new Uri("http://example.com/base")); + + Transaction transaction1 = await SendAsync(server, "http://example.com/base/testpath"); + Assert.True(transaction1.SetCookie.Contains("path=/base")); + } + private static string FindClaimValue(Transaction transaction, string claimType) { XElement claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType); @@ -364,9 +385,9 @@ namespace Microsoft.AspNet.Security.Cookies return me; } - private static TestServer CreateServer(Action configureOptions, Func testpath = null) + private static TestServer CreateServer(Action configureOptions, Func testpath = null, Uri baseAddress = null) { - return TestServer.Create(app => + var server = TestServer.Create(app => { app.UseServices(services => services.AddDataProtection()); app.UseCookieAuthentication(configureOptions); @@ -406,6 +427,8 @@ namespace Microsoft.AspNet.Security.Cookies } }); }); + server.BaseAddress = baseAddress; + return server; } private static void Describe(HttpResponse res, AuthenticationResult result) diff --git a/test/Microsoft.AspNet.Security.Test/Cookies/Infrastructure/CookieChunkingTests.cs b/test/Microsoft.AspNet.Security.Test/Cookies/Infrastructure/CookieChunkingTests.cs index 4df8763bd9..04bbadf68a 100644 --- a/test/Microsoft.AspNet.Security.Test/Cookies/Infrastructure/CookieChunkingTests.cs +++ b/test/Microsoft.AspNet.Security.Test/Cookies/Infrastructure/CookieChunkingTests.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNet.Http; -using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Http.Core; using Xunit; namespace Microsoft.AspNet.Security.Cookies.Infrastructure diff --git a/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs b/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs index a5a003b801..1ba7054390 100644 --- a/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs +++ b/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs @@ -3,307 +3,681 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using Microsoft.AspNet.Security; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; +using Moq; using Xunit; namespace Microsoft.AspNet.Security.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(); + } + + private Mock SetupContext(params ClaimsIdentity[] ids) + { + var context = new Mock(); + context.SetupProperty(c => c.User); + var user = new ClaimsPrincipal(); + user.AddIdentities(ids); + context.Object.User = user; + if (ids != null) + { + var results = new List(); + foreach (var id in ids) + { + results.Add(new AuthenticationResult(id, new AuthenticationProperties(), new AuthenticationDescription())); + } + context.Setup(c => c.AuthenticateAsync(It.IsAny>())).ReturnsAsync(results).Verifiable(); + } + return context; + } + [Fact] - public void Check_ShouldAllowIfClaimIsPresent() + public async Task Authorize_ShouldAllowIfClaimIsPresent() { // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - var user = new ClaimsPrincipal( - new ClaimsIdentity( new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic") - ); + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); + }); + }); + var context = SetupContext(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic")); // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); // Assert Assert.True(allowed); } [Fact] - public void Check_ShouldAllowIfClaimIsAmongValues() + public async Task Authorize_ShouldAllowIfClaimIsPresentWithSpecifiedAuthType() { // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - var user = new ClaimsPrincipal( + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); + }); + }); + var context = SetupContext(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic")); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task Authorize_ShouldAllowIfClaimIsAmongValues() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage", "CanViewAnything")); + }); + }); + var context = SetupContext( new ClaimsIdentity( - new Claim[] { - new Claim("Permission", "CanViewPage"), + new Claim[] { + new Claim("Permission", "CanViewPage"), new Claim("Permission", "CanViewAnything") - }, + }, "Basic") ); // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); // Assert Assert.True(allowed); } [Fact] - public void Check_ShouldNotAllowIfClaimTypeIsNotPresent() + public async Task Authorize_ShouldFailWhenAllRequirementsNotHandled() { // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - var user = new ClaimsPrincipal( + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage", "CanViewAnything")); + }); + }); + var context = SetupContext( new ClaimsIdentity( - new Claim[] { - new Claim("SomethingElse", "CanViewPage"), + new Claim[] { + new Claim("SomethingElse", "CanViewPage"), }, "Basic") ); // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); // Assert Assert.False(allowed); } [Fact] - public void Check_ShouldNotAllowIfClaimValueIsNotPresent() + public async Task Authorize_ShouldNotAllowIfClaimTypeIsNotPresent() { // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - var user = new ClaimsPrincipal( + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage", "CanViewAnything")); + }); + }); + var context = SetupContext( new ClaimsIdentity( - new Claim[] { - new Claim("Permission", "CanViewComment"), + new Claim[] { + new Claim("SomethingElse", "CanViewPage"), }, "Basic") ); // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); // Assert Assert.False(allowed); } [Fact] - public void Check_ShouldNotAllowIfNoClaims() + public async Task Authorize_ShouldNotAllowIfClaimValueIsNotPresent() { // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - var user = new ClaimsPrincipal( + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); + }); + }); + var context = SetupContext( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewComment"), + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task Authorize_ShouldNotAllowIfNoClaims() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); + }); + }); + var context = SetupContext( new ClaimsIdentity( new Claim[0], "Basic") ); // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); // Assert Assert.False(allowed); } [Fact] - public void Check_ShouldNotAllowIfUserIsNull() + public async Task Authorize_ShouldNotAllowIfUserIsNull() { // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - ClaimsPrincipal user = null; + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); + }); + }); + var context = SetupContext(); + context.Object.User = null; // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); // Assert Assert.False(allowed); } [Fact] - public void Check_ShouldNotAllowIfUserIsNotAuthenticated() + public async Task Authorize_ShouldNotAllowIfNotCorrectAuthType() { // Arrange - var authorizationService = new DefaultAuthorizationService(Enumerable.Empty()); - var user = new ClaimsPrincipal( + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); + }); + }); + var context = SetupContext(new ClaimsIdentity()); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task Authorize_ShouldAllowWithNoAuthType() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); + }); + }); + var context = SetupContext( new ClaimsIdentity( - new Claim[] { - new Claim("Permission", "CanViewComment"), + new Claim[] { + new Claim("Permission", "CanViewPage"), + }, + "Basic") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task Authorize_ShouldNotAllowIfUnknownPolicy() + { + // Arrange + var authorizationService = BuildAuthorizationService(); + var context = SetupContext( + new ClaimsIdentity( + new Claim[] { + new Claim("Permission", "CanViewComment"), }, null) ); // Act - var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user); + var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); // Assert Assert.False(allowed); } [Fact] - public void Check_ShouldApplyPoliciesInOrder() + public async Task Authorize_CustomRolePolicy() { // 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(Enumerable.Empty(), 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(Enumerable.Empty(), 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(Enumerable.Empty(), 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(Enumerable.Empty(), null)); - } - - [Fact] - public void Check_ApplyCanMutateCheckedClaims() - { - - // Arrange - var user = new ClaimsPrincipal( - new ClaimsIdentity( new Claim[] { new Claim("Permission", "CanDeleteComments") }, "Basic") + var policy = new AuthorizationPolicyBuilder().RequiresRole("Administrator") + .RequiresClaim(ClaimTypes.Role, "User"); + var authorizationService = BuildAuthorizationService(); + var context = SetupContext( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Role, "User"), + new Claim(ClaimTypes.Role, "Administrator") + }, + "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); + var allowed = await authorizationService.AuthorizeAsync(policy.Build(), context.Object); // Assert Assert.True(allowed); } [Fact] - public void Check_PoliciesCanMutateUsersClaims() + public async Task Authorize_HasAnyClaimOfTypePolicy() { - // Arrange - var user = new ClaimsPrincipal( - new ClaimsIdentity(new Claim[0], "Basic") + var policy = new AuthorizationPolicyBuilder().RequiresClaim(ClaimTypes.Role); + var authorizationService = BuildAuthorizationService(); + var context = SetupContext( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Role, ""), + }, + "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); + var allowed = await authorizationService.AuthorizeAsync(policy.Build(), context.Object); // Assert Assert.True(allowed); } + + [Fact] + public async Task Authorize_PolicyCanAuthenticationTypeWithNameClaim() + { + // Arrange + var policy = new AuthorizationPolicyBuilder("AuthType").RequiresClaim(ClaimTypes.Name); + var authorizationService = BuildAuthorizationService(); + var context = SetupContext( + new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, "Name") }, "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(policy.Build(), context.Object); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task RolePolicyCanRequireSingleRole() + { + // Arrange + var policy = new AuthorizationPolicyBuilder("AuthType").RequiresRole("Admin"); + var authorizationService = BuildAuthorizationService(); + var context = SetupContext( + new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Role, "Admin") }, "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(policy.Build(), context.Object); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task RolePolicyCanRequireOneOfManyRoles() + { + // Arrange + var policy = new AuthorizationPolicyBuilder("AuthType").RequiresRole("Admin", "Users"); + var authorizationService = BuildAuthorizationService(); + var context = SetupContext( + new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Role, "Users") }, "AuthType")); + + // Act + var allowed = await authorizationService.AuthorizeAsync(policy.Build(), context.Object); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task RolePolicyCanBlockWrongRole() + { + // Arrange + var policy = new AuthorizationPolicyBuilder().RequiresClaim("Permission", "CanViewPage"); + var authorizationService = BuildAuthorizationService(); + var context = SetupContext( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Role, "Nope"), + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(policy.Build(), context.Object); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task RolePolicyCanBlockNoRole() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => policy.RequiresRole("Admin", "Users")); + }); + }); + var context = SetupContext( + new ClaimsIdentity( + new Claim[] { + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task PolicyFailsWithNoRequirements() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Basic", policy => { }); + }); + }); + var context = SetupContext( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Name, "Name"), + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task CanApproveAnyAuthenticatedUser() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Any", policy => policy.RequireAuthenticatedUser()); + }); + }); + var context = SetupContext( + new ClaimsIdentity( + new Claim[] { + new Claim(ClaimTypes.Name, "Name"), + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Any", context.Object); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task CanBlockNonAuthenticatedUser() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Any", policy => policy.RequireAuthenticatedUser()); + }); + }); + var context = SetupContext(new ClaimsIdentity()); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Any", context.Object); + + // Assert + Assert.False(allowed); + } + + public class CustomRequirement : IAuthorizationRequirement { } + public class CustomHandler : AuthorizationHandler + { + public override Task CheckAsync(AuthorizationContext context, CustomRequirement requirement) + { + return Task.FromResult(true); + } + } + + [Fact] + public async Task CustomReqWithNoHandlerFails() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); + }); + }); + var context = SetupContext(); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Custom", context.Object); + + // 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 context = SetupContext(); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Custom", context.Object); + + // Assert + Assert.True(allowed); + } + + public class PassThroughRequirement : AuthorizationHandler, IAuthorizationRequirement + { + public PassThroughRequirement(bool succeed) + { + Succeed = succeed; + } + + public bool Succeed { get; set; } + + public override Task CheckAsync(AuthorizationContext context, PassThroughRequirement requirement) + { + return Task.FromResult(Succeed); + } + } + + [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 context = SetupContext(); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Passthrough", context.Object); + + // Assert + Assert.Equal(shouldSucceed, allowed); + } + + public async Task CanCombinePolicies() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + var basePolicy = new AuthorizationPolicyBuilder().RequiresClaim("Base", "Value").Build(); + options.AddPolicy("Combineed", policy => policy.Combine(basePolicy).RequiresClaim("Claim", "Exists")); + }); + }); + var context = SetupContext( + new ClaimsIdentity( + new Claim[] { + new Claim("Base", "Value"), + new Claim("Claim", "Exists") + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Combined", context.Object); + + // Assert + Assert.True(allowed); + } + + public async Task CombinePoliciesWillFailIfBasePolicyFails() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + var basePolicy = new AuthorizationPolicyBuilder().RequiresClaim("Base", "Value").Build(); + options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequiresClaim("Claim", "Exists")); + }); + }); + var context = SetupContext( + new ClaimsIdentity( + new Claim[] { + new Claim("Claim", "Exists") + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Combined", context.Object); + + // Assert + Assert.False(allowed); + } + + public async Task CombinedPoliciesWillFailIfExtraRequirementFails() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.ConfigureAuthorization(options => + { + var basePolicy = new AuthorizationPolicyBuilder().RequiresClaim("Base", "Value").Build(); + options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequiresClaim("Claim", "Exists")); + }); + }); + var context = SetupContext( + new ClaimsIdentity( + new Claim[] { + new Claim("Base", "Value"), + }, + "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync("Combined", context.Object); + + // Assert + Assert.False(allowed); + } } -} +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Security.Test/FakePolicy.cs b/test/Microsoft.AspNet.Security.Test/FakePolicy.cs deleted file mode 100644 index be9139b89d..0000000000 --- a/test/Microsoft.AspNet.Security.Test/FakePolicy.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.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/OAuthBearer/OAuthBearerMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs new file mode 100644 index 0000000000..26f202e08e --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs @@ -0,0 +1,271 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens; +using System.Linq; +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.Security.Notifications; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Security.OAuthBearer +{ + public class OAuthBearerMiddlewareTests + { + [Fact] + public async Task BearerTokenValidation() + { + var server = CreateServer(options => + { + options.Authority = "https://login.windows.net/tushartest.onmicrosoft.com"; + options.Audience = "https://TusharTest.onmicrosoft.com/TodoListService-ManualJwt"; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateLifetime = false + }; + }); + string 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.Notifications.MessageReceived = HeaderReceived; + }); + + var response = await SendAsync(server, "http://example.com/oauth", "someHeader someblob"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + private static Task HeaderReceived(MessageReceivedNotification notification) + { + List claims = + new List + { + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob"), + }; + + notification.AuthenticationTicket = new AuthenticationTicket(new ClaimsIdentity(claims, notification.Options.AuthenticationType), new Http.Security.AuthenticationProperties()); + notification.HandleResponse(); + + return Task.FromResult(null); + } + + [Fact] + public async Task NoHeaderReceived() + { + var server = CreateServer(options => { }); + var response = await SendAsync(server, "http://example.com/oauth"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task HeaderWithoutBearerReceived() + { + var server = CreateServer(options => { }); + var response = await SendAsync(server, "http://example.com/oauth","Token"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task CustomTokenReceived() + { + var server = CreateServer(options => + { + options.Notifications.SecurityTokenReceived = SecurityTokenReceived; + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + private static Task SecurityTokenReceived(SecurityTokenReceivedNotification notification) + { + List claims = + new List + { + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob"), + }; + + notification.AuthenticationTicket = new AuthenticationTicket(new ClaimsIdentity(claims, notification.Options.AuthenticationType), new Http.Security.AuthenticationProperties()); + notification.HandleResponse(); + + return Task.FromResult(null); + } + + [Fact] + public async Task CustomTokenValidated() + { + var server = CreateServer(options => + { + options.Notifications.SecurityTokenValidated = SecurityTokenValidated; + options.SecurityTokenValidators = new List{new BlobTokenValidator(options.AuthenticationType)}; + }); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + private static Task SecurityTokenValidated(SecurityTokenValidatedNotification notification) + { + List claims = + new List + { + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob"), + }; + + notification.AuthenticationTicket = new AuthenticationTicket(new ClaimsIdentity(claims, notification.Options.AuthenticationType), new Http.Security.AuthenticationProperties()); + notification.HandleResponse(); + + return Task.FromResult(null); + } + + [Fact] + public async Task RetrievingTokenFromAlternateLocation() + { + var server = CreateServer(options => { + options.Notifications.MessageReceived = MessageReceived; + options.Notifications.SecurityTokenReceived = SecurityTokenReceived; + }); + var response = await SendAsync(server, "http://example.com/oauth", "Bearer Token"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + private static Task MessageReceived(MessageReceivedNotification notification) + { + notification.Token = "CustomToken"; + return Task.FromResult(null); + } + + class BlobTokenValidator : ISecurityTokenValidator + { + + public BlobTokenValidator(string authenticationType) + { + AuthenticationType = authenticationType; + } + + public string AuthenticationType { get; set; } + + public bool CanValidateToken + { + get + { + return true; + } + } + + public int MaximumTokenSizeInBytes + { + get + { + return 2*2*1024; + } + + set + { + throw new NotImplementedException(); + } + } + + public bool CanReadToken(string securityToken) + { + return true; + } + + public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) + { + validatedToken = null; + List claims = + new List + { + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob"), + }; + + return new ClaimsPrincipal(new ClaimsIdentity(claims, AuthenticationType)); + } + } + + private static TestServer CreateServer(Action configureOptions, Func handler = null) + { + return TestServer.Create(app => + { + app.UseServices(services => + { + services.AddDataProtection(); + }); + + if (configureOptions != null) + { + app.UseOAuthBearerAuthentication(configureOptions); + } + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/oauth")) + { + } + else + { + await next(); + } + + }); + }); + } + + 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; + } + + 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; } + } + } +} diff --git a/test/Microsoft.AspNet.Security.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs new file mode 100644 index 0000000000..e3a5df4718 --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs @@ -0,0 +1,362 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.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.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Security.Cookies; +using Microsoft.AspNet.Security.DataHandler; +using Microsoft.AspNet.Security.DataProtection; +using Microsoft.AspNet.Security.OpenIdConnect; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Newtonsoft.Json; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Security.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.SignInAsAuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType; + }); + 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=" + Uri.EscapeDataString("openid profile")); + } + + [Fact] + public async Task ChallengeWillUseOptionsProperties() + { + var server = CreateServer(options => + { + options.Authority = "https://login.windows.net/common"; + options.ClientId = "Test Id"; + options.SignInAsAuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType; + 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=" + Uri.EscapeDataString("https://www.googleapis.com/auth/plus.login")); + query.ShouldContain("response_type=" + Uri.EscapeDataString("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() + { + ISecureDataFormat stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); + 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() + { + ISecureDataFormat stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest")); + 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(Uri.EscapeDataString("https://example.com/logout")); + } + + [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.UseServices(services => + { + services.AddDataProtection(); + services.Configure(options => + { + options.SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType; + }); + }); + + app.UseCookieAuthentication(options => + { + options.AuthenticationType = "OpenIdConnect"; + }); + app.UseOpenIdConnectAuthentication(configureOptions); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge")) + { + res.Challenge("OpenIdConnect"); + res.StatusCode = 401; + } + else if (req.Path == new PathString("/signin")) + { + res.SignIn(); + } + else if (req.Path == new PathString("/signout")) + { + res.SignOut(OpenIdConnectAuthenticationDefaults.AuthenticationType); + } + else if (handler != null) + { + await handler(context); + } + else + { + await next(); + } + }); + }); + } + + 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.Security.Test/SecurityHelperTests.cs b/test/Microsoft.AspNet.Security.Test/SecurityHelperTests.cs index 76f8f6e0a7..dae0aed318 100644 --- a/test/Microsoft.AspNet.Security.Test/SecurityHelperTests.cs +++ b/test/Microsoft.AspNet.Security.Test/SecurityHelperTests.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Security.Claims; using System.Security.Principal; using Microsoft.AspNet.Http; -using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Security.Infrastructure; using Shouldly; using Xunit; diff --git a/test/Microsoft.AspNet.Security.Test/TestApplicationEnvironment.cs b/test/Microsoft.AspNet.Security.Test/TestApplicationEnvironment.cs deleted file mode 100644 index 01ef364c1d..0000000000 --- a/test/Microsoft.AspNet.Security.Test/TestApplicationEnvironment.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Runtime.Versioning; -using Microsoft.Framework.Runtime; - -namespace Microsoft.AspNet.Security -{ - public class TestApplicationEnvironment : IApplicationEnvironment - { - public string ApplicationBasePath - { - get { return Environment.CurrentDirectory; } - } - - public string ApplicationName - { - get { return "Test App environment"; } - } - - public string Configuration - { - get { return "Test"; } - } - - public FrameworkName RuntimeFramework - { - get { return new FrameworkName(".NETFramework", new Version(4, 5)); } - } - - public string Version - { - get { return "1.0.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 index affdcafebd..1ab85d5840 100644 --- a/test/Microsoft.AspNet.Security.Test/project.json +++ b/test/Microsoft.AspNet.Security.Test/project.json @@ -7,13 +7,15 @@ "Microsoft.AspNet.Security.Facebook": "1.0.0-*", "Microsoft.AspNet.Security.Google": "1.0.0-*", "Microsoft.AspNet.Security.MicrosoftAccount": "1.0.0-*", + "Microsoft.AspNet.Security.OAuthBearer": "1.0.0-*", + "Microsoft.AspNet.Security.OpenIdConnect": "1.0.0-*", "Microsoft.AspNet.Security.Twitter": "1.0.0-*", "Microsoft.AspNet.TestHost": "1.0.0-*", "Moq": "4.2.1312.1622", - "Xunit.KRunner": "1.0.0-*" + "xunit.runner.kre": "1.0.0-*" }, "commands": { - "test": "Xunit.KRunner" + "test": "xunit.runner.kre" }, "frameworks": { "aspnet50": {