diff --git a/NuGet.Config b/NuGet.Config index f41e9c631..4ee105534 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -3,5 +3,6 @@ + diff --git a/Security.sln b/Security.sln index 2cc23038e..ccddd42c7 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/samples/OpenIDConnectSample/OpenIdConnectSample.kproj b/samples/OpenIDConnectSample/OpenIdConnectSample.kproj new file mode 100644 index 000000000..c6ab693ae --- /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 000000000..17850d03b --- /dev/null +++ b/samples/OpenIDConnectSample/Startup.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.Framework.DependencyInjection; +using Microsoft.AspNet.Security.OpenIdConnect; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Security; + +namespace OpenIdConnectSample +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + app.UseServices(services => + { + services.AddDataProtection(); + services.Configure(options => + { + options.SignInAsAuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType; + }); + + }); + + app.UseCookieAuthentication(options => + { + options.AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType; + }); + + app.UseOpenIdConnectAuthentication(options => + { + options.ClientId = "fe78e0b4-6fe7-47e6-812c-fb75cee266a4"; + options.Authority = "https://login.windows.net/cyrano.onmicrosoft.com"; + options.RedirectUri = "http://localhost:42023"; + options.SignInAsAuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType; + options.AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType; + }); + + 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 000000000..65652110b --- /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.OAuth/Notifications/IOAuthBearerAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.OAuth/Notifications/IOAuthBearerAuthenticationNotifications.cs deleted file mode 100644 index 048d8f292..000000000 --- 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 2c24f3ff1..000000000 --- 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 5dc04ffb4..000000000 --- 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/OAuthAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationMiddleware.cs index 2628d3849..18dd9c9a8 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 @@ public OAuthAuthenticationMiddleware( { 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 406085b67..000000000 --- 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 0661c4fed..000000000 --- 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 d2f9b9190..000000000 --- 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 6a2eed249..39ba020fc 100644 --- a/src/Microsoft.AspNet.Security.OAuth/project.json +++ b/src/Microsoft.AspNet.Security.OAuth/project.json @@ -3,7 +3,7 @@ "description": "ASP.NET 5 middleware that enables an application to support any standard OAuth 2.0 authentication workflow.", "dependencies": { "Microsoft.AspNet.Security": "1.0.0-*", - "Microsoft.AspNet.Security.DataProtection": "1.0.0-*" + "Microsoft.AspNet.Security.DataProtection": "1.0.0-*", }, "frameworks": { "aspnet50": { 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 000000000..55d65389e --- /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 000000000..29f582749 --- /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 000000000..f2685af0c --- /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 000000000..808615e1d --- /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 70f1f1e61..5b62827cc 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 b5512d01c..0f31dfaf8 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 000000000..1c2eb287a --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs @@ -0,0 +1,200 @@ +// 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.Runtime.ExceptionServices; +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() + { + ExceptionDispatchInfo authFailedEx = null; + 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; + } + + string authorization = Request.Headers.Get("Authorization"); + 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) + { + // We can't await inside a catch block, capture and handle outside. + authFailedEx = ExceptionDispatchInfo.Capture(ex); + } + + if (authFailedEx != null) + { + _logger.WriteError("Exception occurred while processing message", authFailedEx.SourceException); + + // 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 && authFailedEx.SourceException.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException))) + { + Options.ConfigurationManager.RequestRefresh(); + } + + var authenticationFailedNotification = + new AuthenticationFailedNotification(Context, Options) + { + ProtocolMessage = Context, + Exception = authFailedEx.SourceException + }; + + await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); + if (authenticationFailedNotification.HandledResponse) + { + return authenticationFailedNotification.AuthenticationTicket; + } + + if (authenticationFailedNotification.Skipped) + { + return null; + } + + authFailedEx.Throw(); + } + + return null; + } + + protected override void ApplyResponseChallenge() + { + ApplyResponseChallengeAsync().GetAwaiter().GetResult(); + } + + protected override async Task ApplyResponseChallengeAsync() + { + 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 000000000..0403896ae --- /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 000000000..08acf5bf4 --- /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 000000000..37abb160e --- /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 000000000..2a19bea96 --- /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 000000000..affb7e5b1 --- /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/Microsoft.AspNet.Security.OpenIdConnect.kproj b/src/Microsoft.AspNet.Security.OpenIDConnect/Microsoft.AspNet.Security.OpenIdConnect.kproj new file mode 100644 index 000000000..ed1a12239 --- /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/NonceCache.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/NonceCache.cs new file mode 100644 index 000000000..11c09b345 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/NonceCache.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 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/Notifications/AuthorizationCodeReceivedNotification.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/Notifications/AuthorizationCodeReceivedNotification.cs new file mode 100644 index 000000000..cbcdc0618 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/Notifications/AuthorizationCodeReceivedNotification.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.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 000000000..d6271488a --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationDefaults.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 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 000000000..81a74b65c --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 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 000000000..bfe82b51f --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationMiddleware.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 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 000000000..36ee2fd25 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationNotifications.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.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 000000000..5022cbf3c --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationOptions.cs @@ -0,0 +1,337 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.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 NoneCache { 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 ensure 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 000000000..530503910 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs @@ -0,0 +1,577 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 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.Runtime.ExceptionServices; +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(); // TODO signout.Properties; + 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.NoneCache != null) + { + Options.NoneCache.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); + + // TODO: a delegate on OpenIdConnectAuthenticationOptions would allow for users to hook their own custom message. + openIdConnectMessage = new OpenIdConnectMessage(form); + } + + if (openIdConnectMessage == null) + { + return null; + } + + ExceptionDispatchInfo authFailedEx = 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) + { + // We can't await inside a catch block, capture and handle outside. + authFailedEx = ExceptionDispatchInfo.Capture(exception); + } + + if (authFailedEx != null) + { + _logger.WriteError("Exception occurred while processing message", authFailedEx.SourceException); + + // 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 && authFailedEx.SourceException.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException))) + { + Options.ConfigurationManager.RequestRefresh(); + } + + var authenticationFailedNotification = new AuthenticationFailedNotification(Context, Options) + { + ProtocolMessage = openIdConnectMessage, + Exception = authFailedEx.SourceException + }; + + await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); + if (authenticationFailedNotification.HandledResponse) + { + return authenticationFailedNotification.AuthenticationTicket; + } + + if (authenticationFailedNotification.Skipped) + { + return null; + } + + authFailedEx.Throw(); + } + + return null; + } + + /// + /// 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. + /// Examins 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/Project.json b/src/Microsoft.AspNet.Security.OpenIDConnect/Project.json new file mode 100644 index 000000000..7a6d95a05 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/Project.json @@ -0,0 +1,48 @@ +{ + "version": "1.0.0-*", + "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.4", + "Microsoft.IdentityModel.Protocol.Extensions": "2.0.0.0-beta1-*", + "System.IdentityModel.Tokens": "5.0.0.0-beta1-*" + }, + "frameworks": { + "aspnet50": { + "frameworkAssemblies": { + "System.Net.Http": "", + "System.Net.Http.WebRequest": "" + } + }, + "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.IO.Compression": "4.0.0-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-*" + } + } + } +} \ No newline at end of file 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 000000000..f5bfc6044 --- /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/AuthenticationTicket.cs b/src/Microsoft.AspNet.Security/AuthenticationTicket.cs index b9457aa91..7b7e50998 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 @@ public AuthenticationTicket(ClaimsIdentity identity, AuthenticationProperties pr 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 runtims. + /// 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 authenticated user identity. + /// + public string AuthenticationType { get; private set; } + /// /// Gets the authenticated user identity. /// public ClaimsIdentity Identity { get; private set; } + /// + /// Gets the authenticated user identity. + /// + public ClaimsPrincipal Principal{ get; private set; } + /// /// Additional state values for the authentication session. /// diff --git a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenReceiveContext.cs b/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationTokenReceiveContext.cs index af077b551..2e68dc308 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 @@ public AuthenticationTokenReceiveContext( 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/Notifications/AuthenticationFailedNotification.cs b/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs index 9e50ecf4f..5d232426f 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 000000000..1ef13e2dc --- /dev/null +++ b/src/Microsoft.AspNet.Security/Notifications/BaseNotification.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 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 e27028917..1b1f59988 100644 --- a/src/Microsoft.AspNet.Security/Notifications/MessageReceivedNotification.cs +++ b/src/Microsoft.AspNet.Security/Notifications/MessageReceivedNotification.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 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; } } } \ 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 000000000..1f48e25b3 --- /dev/null +++ b/src/Microsoft.AspNet.Security/Notifications/NotificationResultState.cs @@ -0,0 +1,22 @@ +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 d0bd97a76..768b384a9 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 467731fa0..524664d7e 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 f8aa2adef..7db29788a 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 400b8b581..bdef232a7 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/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs new file mode 100644 index 000000000..6fa8b7afd --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.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 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); + } + + 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; } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Security.Test/OpenIdConnectMiddlewareTests/OpenIdConnectMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/OpenIdConnectMiddlewareTests/OpenIdConnectMiddlewareTests.cs new file mode 100644 index 000000000..652c295ec --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/OpenIdConnectMiddlewareTests/OpenIdConnectMiddlewareTests.cs @@ -0,0 +1,361 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.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/project.json b/test/Microsoft.AspNet.Security.Test/project.json index 305fe5528..504d228b6 100644 --- a/test/Microsoft.AspNet.Security.Test/project.json +++ b/test/Microsoft.AspNet.Security.Test/project.json @@ -7,6 +7,8 @@ "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", @@ -18,7 +20,8 @@ "frameworks": { "aspnet50": { "dependencies": { - "Shouldly": "1.1.1.1" + "Shouldly": "1.1.1.1", + "System.Security.Claims": "1.0.0-*" } } }