From 997f768bf6a92ec69827e5c9780c68d67b05d408 Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Mon, 12 Jan 2015 08:59:24 -0800 Subject: [PATCH 01/11] Additions for OpenIdConnectMiddleware and OAuthBearer Beta1. --- Security.sln | 32 +- .../OpenIDConnectSample.kproj | 30 + samples/OpenIDConnectSample/Startup.cs | 57 ++ samples/OpenIDConnectSample/project.json | 18 + .../OAuthBearerAuthenticationNotifications.cs | 45 +- .../OAuthAuthenticationMiddleware.cs | 12 +- .../OAuthBearerAuthenticationHandler.cs | 176 ++++-- .../OAuthBearerAuthenticationMiddleware.cs | 77 ++- .../OAuthBearerAuthenticationOptions.cs | 126 +++- .../project.json | 4 +- ...rosoft.AspNet.Security.OpenIDConnect.kproj | 29 + .../NonceCache.cs | 11 + .../AuthorizationCodeReceivedNotification.cs | 45 ++ .../OpenIdConnectAuthenticationDefaults.cs | 41 ++ .../OpenIdConnectAuthenticationExtensions.cs | 29 + .../OpenIdConnectAuthenticationMiddleware.cs | 173 ++++++ ...penIdConnectAuthenticationNotifications.cs | 59 ++ .../OpenIdConnectAuthenticationOptions.cs | 337 ++++++++++ .../OpenidConnectAuthenticationHandler.cs | 581 ++++++++++++++++++ .../Project.json | 48 ++ .../Resources.Designer.cs | 101 +++ .../AuthenticationTicket.cs | 30 +- .../AuthenticationTokenReceiveContext.cs | 11 - .../AuthenticationFailedNotification.cs | 7 +- .../Notifications/BaseNotification.cs | 49 ++ .../MessageReceivedNotification.cs | 6 +- .../Notifications/NotificationResultState.cs | 22 + ...edirectFromIdentityProviderNotification.cs | 12 +- .../RedirectToIdentityProviderNotification.cs | 8 +- .../SecurityTokenReceivedNotification.cs | 10 +- .../SecurityTokenValidatedNotification.cs | 10 +- 31 files changed, 2055 insertions(+), 141 deletions(-) create mode 100644 samples/OpenIDConnectSample/OpenIDConnectSample.kproj create mode 100644 samples/OpenIDConnectSample/Startup.cs create mode 100644 samples/OpenIDConnectSample/project.json create mode 100644 src/Microsoft.AspNet.Security.OpenIDConnect/Microsoft.AspNet.Security.OpenIDConnect.kproj create mode 100644 src/Microsoft.AspNet.Security.OpenIDConnect/NonceCache.cs create mode 100644 src/Microsoft.AspNet.Security.OpenIDConnect/Notifications/AuthorizationCodeReceivedNotification.cs create mode 100644 src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationDefaults.cs create mode 100644 src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationExtensions.cs create mode 100644 src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationMiddleware.cs create mode 100644 src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationNotifications.cs create mode 100644 src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationOptions.cs create mode 100644 src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs create mode 100644 src/Microsoft.AspNet.Security.OpenIDConnect/Project.json create mode 100644 src/Microsoft.AspNet.Security.OpenIDConnect/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.Security/Notifications/BaseNotification.cs create mode 100644 src/Microsoft.AspNet.Security/Notifications/NotificationResultState.cs diff --git a/Security.sln b/Security.sln index 2cc23038e..574438f91 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,10 @@ 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.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 +160,30 @@ 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 + {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 +200,7 @@ 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} + {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..501030ad2 --- /dev/null +++ b/samples/OpenIDConnectSample/Startup.cs @@ -0,0 +1,57 @@ +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; +using Microsoft.AspNet.Security.Cookies; + +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..4fd1622b8 --- /dev/null +++ b/samples/OpenIDConnectSample/project.json @@ -0,0 +1,18 @@ +{ + "dependencies": { + "Microsoft.AspNet.Security.Cookies": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Kestrel": "1.0.0-*", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Security.OpenIDConnect": "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": "d://wwwroot" +} diff --git a/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthBearerAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthBearerAuthenticationNotifications.cs index 2c24f3ff1..58fea7a6a 100644 --- a/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthBearerAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthBearerAuthenticationNotifications.cs @@ -1,64 +1,61 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNet.Security.Notifications; using System; using System.Threading.Tasks; +/// +/// Specifies events which the invokes to enable developer control over the authentication process. /> +/// namespace Microsoft.AspNet.Security.OAuth { /// /// OAuth bearer token middleware provider /// - public class OAuthBearerAuthenticationNotifications : IOAuthBearerAuthenticationNotifications + public class OAuthBearerAuthenticationNotifications { /// /// Initializes a new instance of the class /// public OAuthBearerAuthenticationNotifications() { - OnRequestToken = context => Task.FromResult(null); - OnValidateIdentity = context => Task.FromResult(null); + AuthenticationFailed = notification => Task.FromResult(0); + MessageReceived = notification => Task.FromResult(0); + SecurityTokenReceived = notification => Task.FromResult(0); + SecurityTokenValidated = notification => Task.FromResult(0); + OnApplyChallenge = context => { context.HttpContext.Response.Headers.AppendValues("WWW-Authenticate", context.Challenge); return Task.FromResult(0); }; - } + } /// - /// Handles processing OAuth bearer token. + /// Handles applying the authentication challenge to the response message. /// - public Func OnRequestToken { get; set; } + public Func OnApplyChallenge { get; set; } /// - /// Handles validating the identity produced from an OAuth bearer token. + /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. /// - public Func OnValidateIdentity { get; set; } + public Func, Task> AuthenticationFailed { get; set; } /// - /// Handles applying the authentication challenge to the response message. + /// Invoked when a protocol message is first received. /// - public Func OnApplyChallenge { get; set; } + public Func, Task> MessageReceived { get; set; } /// - /// Handles processing OAuth bearer token. + /// Invoked with the security token that has been extracted from the protocol message. /// - /// - /// - public virtual Task RequestToken(OAuthRequestTokenContext context) - { - return OnRequestToken(context); - } + public Func, Task> SecurityTokenReceived { get; set; } /// - /// Handles validating the identity produced from an OAuth bearer token. + /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated. /// - /// - /// - public virtual Task ValidateIdentity(OAuthValidateIdentityContext context) - { - return OnValidateIdentity.Invoke(context); - } + public Func, Task> SecurityTokenValidated { get; set; } /// /// Handles applying the authentication challenge to the response message. 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 index 406085b67..522100a02 100644 --- a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationHandler.cs @@ -1,17 +1,28 @@ // 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.Security; using Microsoft.AspNet.Security.Infrastructure; +using Microsoft.AspNet.Security.Notifications; using Microsoft.Framework.Logging; +using Microsoft.IdentityModel.Protocols; +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; namespace Microsoft.AspNet.Security.OAuth { internal class OAuthBearerAuthenticationHandler : AuthenticationHandler { + private const string HandledResponse = "HandledResponse"; + private readonly ILogger _logger; private readonly string _challenge; + private OpenIdConnectConfiguration _configuration; public OAuthBearerAuthenticationHandler(ILogger logger, string challenge) { @@ -24,87 +35,164 @@ 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; + OAuthRequestTokenContext requestTokenContext = null; try { // Find token in default location - string requestToken = null; - string authorization = Request.Headers.Get("Authorization"); - if (!string.IsNullOrEmpty(authorization)) + requestTokenContext = new OAuthRequestTokenContext(Context, null); + + // Give application opportunity to find from a different location, adjust, or reject token + var messageReceivedNotification = + new MessageReceivedNotification(Context, Options) + { + ProtocolMessage = requestTokenContext, + }; + + // notification can set the token + await Options.Notifications.MessageReceived(messageReceivedNotification); + if (messageReceivedNotification.HandledResponse) + { + return messageReceivedNotification.AuthenticationTicket; + } + + if (messageReceivedNotification.Skipped) { + return null; + } + + if (string.IsNullOrEmpty(requestTokenContext.Token)) + { + string authorization = Request.Headers.Get("Authorization"); if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { - requestToken = authorization.Substring("Bearer ".Length).Trim(); + requestTokenContext.Token = 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); + // notify user token was received + var securityTokenReceivedNotification = + new SecurityTokenReceivedNotification(Context, Options) + { + ProtocolMessage = requestTokenContext, + }; - await Options.AccessTokenProvider.ReceiveAsync(tokenReceiveContext); - if (tokenReceiveContext.Ticket == null) + await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); + if (securityTokenReceivedNotification.HandledResponse) { - tokenReceiveContext.DeserializeTicket(tokenReceiveContext.Token); + return securityTokenReceivedNotification.AuthenticationTicket; } - AuthenticationTicket ticket = tokenReceiveContext.Ticket; - if (ticket == null) + if (securityTokenReceivedNotification.Skipped) { - _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) + if (_configuration == null) { - _logger.WriteWarning("expired bearer token received"); - return null; + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } - // Give application final opportunity to override results - var context = new OAuthValidateIdentityContext(Context, Options, ticket); - if (ticket != null && - ticket.Identity != null && - ticket.Identity.IsAuthenticated) + var validationParameters = Options.TokenValidationParameters.Clone(); + if (validationParameters.ValidIssuer == null && !string.IsNullOrWhiteSpace(_configuration.Issuer)) + { + validationParameters.ValidIssuer = _configuration.Issuer; + } + else { - // bearer token with identity starts validated - context.Validated(); + IEnumerable issuers = new[] { _configuration.Issuer }; + validationParameters.ValidIssuers = (validationParameters.ValidIssuers == null ? issuers : validationParameters.ValidIssuers.Concat(issuers)); } - await Options.Notifications.ValidateIdentity(context); - if (!context.IsValidated) + validationParameters.IssuerSigningKeys = (validationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : validationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys)); + SecurityToken validatedToken; + foreach (var validator in Options.SecurityTokenValidators) { - return null; + if (validator.CanReadToken(requestTokenContext.Token)) + { + ClaimsPrincipal principal = Options.SecurityTokenValidators.First().ValidateToken(requestTokenContext.Token, validationParameters, out validatedToken); + ClaimsIdentity claimsIdentity = principal.Identity as ClaimsIdentity; + AuthenticationTicket ticket = new AuthenticationTicket(claimsIdentity, new AuthenticationProperties()); + var securityTokenValidatedNotification = new SecurityTokenValidatedNotification(Context, Options) + { + ProtocolMessage = requestTokenContext, + AuthenticationTicket = ticket + }; + + if (securityTokenReceivedNotification.HandledResponse) + { + return securityTokenValidatedNotification.AuthenticationTicket; + } + + if (securityTokenReceivedNotification.Skipped) + { + return null; + } + + return ticket; + } } - // resulting identity values go back to caller - return context.Ticket; + throw new InvalidOperationException("No SecurityTokenValidator available for token: " + requestTokenContext.Token); } catch (Exception ex) { - _logger.WriteError("Authentication failed", ex); - return null; + // 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.ToString()); + + // 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 = requestTokenContext, + 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() { if (Response.StatusCode != 401) { @@ -114,8 +202,10 @@ protected override void ApplyResponseChallenge() if (ChallengeContext != null) { OAuthChallengeContext challengeContext = new OAuthChallengeContext(Context, _challenge); - Options.Notifications.ApplyChallenge(challengeContext); + await Options.Notifications.ApplyChallenge(challengeContext); } + + return; } protected override void ApplyResponseGrant() diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationMiddleware.cs index 0661c4fed..694fbda61 100644 --- a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationMiddleware.cs @@ -2,21 +2,25 @@ // 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 Microsoft.IdentityModel.Protocols; using System; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.IdentityModel.Tokens; +using System.Net.Http; 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 + /// + /// 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; @@ -56,16 +60,40 @@ public OAuthBearerAuthenticationMiddleware( Options.Notifications = new OAuthBearerAuthenticationNotifications(); } - if (Options.AccessTokenFormat == null) + if (Options.SecurityTokenValidators == null) { - IDataProtector dataProtector = dataProtectionProvider.CreateDataProtector( - this.GetType().FullName, Options.AuthenticationType, "v1"); - Options.AccessTokenFormat = new TicketDataFormat(dataProtector); + Options.SecurityTokenValidators = new Collection { new JwtSecurityTokenHandler() }; } - if (Options.AccessTokenProvider == null) + if (string.IsNullOrWhiteSpace(Options.TokenValidationParameters.ValidAudience) && !string.IsNullOrWhiteSpace(Options.Audience)) { - Options.AccessTokenProvider = new AuthenticationTokenProvider(); + 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)) + { + 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); + } } } @@ -77,5 +105,28 @@ protected override AuthenticationHandler Creat { return new OAuthBearerAuthenticationHandler(_logger, _challenge); } + + [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.OAuth/OAuthBearerAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationOptions.cs index d2f9b9190..b87a35574 100644 --- a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationOptions.cs @@ -2,6 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNet.Security.Infrastructure; +using Microsoft.IdentityModel.Protocols; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens; +using System.Net.Http; namespace Microsoft.AspNet.Security.OAuth { @@ -10,13 +15,19 @@ namespace Microsoft.AspNet.Security.OAuth /// public class OAuthBearerAuthenticationOptions : AuthenticationOptions { + private ICollection _securityTokenValidators; + private TokenValidationParameters _tokenValidationParameters; + /// /// Creates an instance of bearer authentication options with default values. /// public OAuthBearerAuthenticationOptions() : base() { - SystemClock = new SystemClock(); AuthenticationType = OAuthBearerAuthenticationDefaults.AuthenticationType; + BackchannelTimeout = TimeSpan.FromMinutes(1); + RefreshOnIssuerKeyNotFound = true; + SystemClock = new SystemClock(); + TokenValidationParameters = new TokenValidationParameters(); } /// @@ -32,35 +43,122 @@ public OAuthBearerAuthenticationOptions() : base() /// public string Challenge { get; set; } + /// + /// 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; } + /// /// 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; } + public OAuthBearerAuthenticationNotifications 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. + /// The HttpMessageHandler used to retrieve metadata. + /// This cannot be set at the same time as BackchannelCertificateValidator unless the value + /// is a WebRequestHandler. /// - public ISecureDataFormat AccessTokenFormat { get; set; } + public HttpMessageHandler BackchannelHttpHandler { 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. + /// Gets or sets the timeout when using the backchannel to make an http call. /// - public IAuthenticationTokenProvider AccessTokenProvider { get; set; } + 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 of s used to read and validate s. + /// + /// 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.OAuth/project.json b/src/Microsoft.AspNet.Security.OAuth/project.json index 6a2eed249..0bae926b9 100644 --- a/src/Microsoft.AspNet.Security.OAuth/project.json +++ b/src/Microsoft.AspNet.Security.OAuth/project.json @@ -3,7 +3,9 @@ "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-*", + "Microsoft.IdentityModel.Protocol.Extensions": "2.0.0-beta1-*", + "System.IdentityModel.Tokens": "5.0.0-beta1-*" }, "frameworks": { "aspnet50": { 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..5f8eb40e8 --- /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..6ba5c1f88 --- /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 OWIN runtime. + /// + /// The application builder + /// Options which control the processing of the bearer header. + /// 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..9d7d9af29 --- /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 Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.DataHandler; +using Microsoft.AspNet.Security.DataProtection; +using Microsoft.AspNet.Security.Infrastructure; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Microsoft.IdentityModel.Protocols; +using Microsoft.AspNet.Security.DataHandler.Serializer; +using Microsoft.AspNet.Security.DataHandler.Encoder; +using System.Text; + +namespace Microsoft.AspNet.Security.OpenIdConnect +{ + /// + /// OWIN middleware for obtaining identities using OpenIdConnect protocol. + /// + public class OpenIdConnectAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly ILogger _logger; + + /// + /// Initializes a + /// + /// The next middleware in the OWIN pipeline to invoke + /// The OWIN 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)) + { + 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..e4dc545fc --- /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.IdentityModel.Protocols; +using Microsoft.AspNet.Security.Notifications; + +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..b6a595629 --- /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 Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.IdentityModel.Protocols; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IdentityModel.Tokens; +using System.Net.Http; + +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; + BackchannelTimeout = TimeSpan.FromMinutes(1); + Caption = OpenIdConnectAuthenticationDefaults.Caption; + ProtocolValidator = new OpenIdConnectProtocolValidator(); + RefreshOnIssuerKeyNotFound = true; + ResponseType = OpenIdConnectResponseTypes.CodeIdToken; + Scope = OpenIdConnectScopes.OpenIdProfile; + TokenValidationParameters = new TokenValidationParameters(); + UseTokenLifetime = true; + AuthenticationType = authenticationType; + } + + /// + /// 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 to mitigate against replay attacks when obtaining 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 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; + } + + /// + /// 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; + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs new file mode 100644 index 000000000..570ebce20 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs @@ -0,0 +1,581 @@ +// 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.Http.Security; +using Microsoft.AspNet.Security.Infrastructure; +using Microsoft.AspNet.Security.Notifications; +using Microsoft.Framework.Logging; +using Microsoft.IdentityModel.Protocols; +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; + +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) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + OpenIdConnectMessage openIdConnectMessage = new OpenIdConnectMessage() + { + IssuerAddress = _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) + { + _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + } + + OpenIdConnectMessage openIdConnectMessage = new OpenIdConnectMessage + { + ClientId = Options.ClientId, + IssuerAddress = _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) + { + if (!Request.Body.CanSeek) + { + _logger.WriteVerbose("Buffering request body"); + // Buffer in case this body was not meant for us. + MemoryStream memoryStream = new MemoryStream(); + await Request.Body.CopyToAsync(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + Request.Body = memoryStream; + } + + IFormCollection form = (IFormCollection) 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) + { + _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 (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)); + ISecurityTokenValidator validator = null; + foreach(var v in Options.SecurityTokenValidators) + { + if (v.CanReadToken(openIdConnectMessage.IdToken)) + validator = v; + } + + if (validator == null) + { + throw new InvalidOperationException("No SecurityTokenValidator found for token: " + openIdConnectMessage.IdToken); + } + + SecurityToken validatedToken; + ClaimsPrincipal principal = validator.ValidateToken(openIdConnectMessage.IdToken, validationParameters, out validatedToken); + JwtSecurityToken jwt = validatedToken as JwtSecurityToken; + if (jwt == null) + { + throw new InvalidOperationException("Validated Security Token must be a JwtSecurityToken was: " + validatedToken.GetType()); + } + + AuthenticationTicket ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationType); + if (!string.IsNullOrWhiteSpace(openIdConnectMessage.SessionState)) + { + ticket.Properties.Dictionary[OpenIdConnectSessionProperties.SessionState] = openIdConnectMessage.SessionState; + } + + if (!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.ToString()); + + // 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". + public virtual 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. + protected virtual 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..461599159 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; +using System.Security.Claims; namespace Microsoft.AspNet.Security { @@ -28,11 +22,33 @@ public AuthenticationTicket(ClaimsIdentity identity, AuthenticationProperties pr Properties = properties ?? new AuthenticationProperties(); } + /// + /// Initializes a new instance of the class + /// + /// + /// + 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..7f5ae0697 100644 --- a/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs +++ b/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs @@ -2,18 +2,19 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNet.Http; using System; 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 +} From 722987c8ed027b52e5ecc02324f08c77c46fe6e5 Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Mon, 12 Jan 2015 15:48:30 -0800 Subject: [PATCH 02/11] Split off OAuthBearer. Added tests for OAuthBearer, OpenIdConnect --- Security.sln | 15 + ...IOAuthBearerAuthenticationNotifications.cs | 37 -- .../OAuthValidateIdentityContext.cs | 26 -- ...icrosoft.AspNet.Security.OAuthBearer.kproj | 22 ++ .../NotNullAttribute.cs | 12 + .../OAuthBearerAuthenticationNotifications.cs | 29 +- .../OAuthBearerAuthenticationDefaults.cs | 0 .../OAuthBearerAuthenticationExtensions.cs | 0 .../OAuthBearerAuthenticationHandler.cs | 29 +- .../OAuthBearerAuthenticationMiddleware.cs | 0 .../OAuthBearerAuthenticationOptions.cs | 1 + .../OAuthTokenContext.cs | 32 ++ .../Resources.Designer.cs | 83 ++++ .../Resources.resx | 126 ++++++ .../project.json | 22 ++ .../OpenIdConnectAuthenticationOptions.cs | 2 +- .../AuthenticationTicket.cs | 5 +- .../OAuthBearer/OAuthBearerMiddlewareTests.cs | 138 +++++++ .../OpenIdConnectMiddlewareTests.cs | 359 ++++++++++++++++++ .../project.json | 5 +- 20 files changed, 829 insertions(+), 114 deletions(-) delete mode 100644 src/Microsoft.AspNet.Security.OAuth/Notifications/IOAuthBearerAuthenticationNotifications.cs delete mode 100644 src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthValidateIdentityContext.cs create mode 100644 src/Microsoft.AspNet.Security.OAuthBearer/Microsoft.AspNet.Security.OAuthBearer.kproj create mode 100644 src/Microsoft.AspNet.Security.OAuthBearer/NotNullAttribute.cs rename src/{Microsoft.AspNet.Security.OAuth => Microsoft.AspNet.Security.OAuthBearer}/Notifications/OAuthBearerAuthenticationNotifications.cs (55%) rename src/{Microsoft.AspNet.Security.OAuth => Microsoft.AspNet.Security.OAuthBearer}/OAuthBearerAuthenticationDefaults.cs (100%) rename src/{Microsoft.AspNet.Security.OAuth => Microsoft.AspNet.Security.OAuthBearer}/OAuthBearerAuthenticationExtensions.cs (100%) rename src/{Microsoft.AspNet.Security.OAuth => Microsoft.AspNet.Security.OAuthBearer}/OAuthBearerAuthenticationHandler.cs (87%) rename src/{Microsoft.AspNet.Security.OAuth => Microsoft.AspNet.Security.OAuthBearer}/OAuthBearerAuthenticationMiddleware.cs (100%) rename src/{Microsoft.AspNet.Security.OAuth => Microsoft.AspNet.Security.OAuthBearer}/OAuthBearerAuthenticationOptions.cs (98%) create mode 100644 src/Microsoft.AspNet.Security.OAuthBearer/OAuthTokenContext.cs create mode 100644 src/Microsoft.AspNet.Security.OAuthBearer/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.Security.OAuthBearer/Resources.resx create mode 100644 src/Microsoft.AspNet.Security.OAuthBearer/project.json create mode 100644 test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs create mode 100644 test/Microsoft.AspNet.Security.Test/OpenIdConnectMiddlewareTests/OpenIdConnectMiddlewareTests.cs diff --git a/Security.sln b/Security.sln index 574438f91..73c662584 100644 --- a/Security.sln +++ b/Security.sln @@ -40,6 +40,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "OpenIDConnectSample", "samp 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 +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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -184,6 +186,18 @@ Global {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 + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -202,5 +216,6 @@ Global {19711880-46DA-4A26-9E0F-9B2E41D27651} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} {BEF0F5C3-EF4E-4649-9C49-D5E279A3CA2B} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} {674D128E-83BB-481A-A9D9-6D47872E1FC8} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {2755BFE5-7421-4A31-A644-F817DF5CAA98} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} EndGlobalSection EndGlobal 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/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.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..3f56c4151 --- /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.OAuth +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthBearerAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs similarity index 55% rename from src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthBearerAuthenticationNotifications.cs rename to src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs index 58fea7a6a..f5d3a5826 100644 --- a/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthBearerAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs @@ -24,47 +24,26 @@ public OAuthBearerAuthenticationNotifications() MessageReceived = notification => Task.FromResult(0); SecurityTokenReceived = notification => Task.FromResult(0); SecurityTokenValidated = notification => Task.FromResult(0); - - OnApplyChallenge = context => - { - context.HttpContext.Response.Headers.AppendValues("WWW-Authenticate", context.Challenge); - return Task.FromResult(0); - }; } - /// - /// Handles applying the authentication challenge to the response message. - /// - public Func OnApplyChallenge { get; set; } - /// /// 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; } + public Func, Task> AuthenticationFailed { get; set; } /// /// Invoked when a protocol message is first received. /// - public Func, Task> MessageReceived { get; set; } + 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; } + 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; } - - /// - /// Handles applying the authentication challenge to the response message. - /// - /// - /// - public Task ApplyChallenge(OAuthChallengeContext context) - { - return OnApplyChallenge(context); - } + public Func, Task> SecurityTokenValidated { get; set; } } } diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationDefaults.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationDefaults.cs similarity index 100% rename from src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationDefaults.cs rename to src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationDefaults.cs diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationExtensions.cs similarity index 100% rename from src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationExtensions.cs rename to src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationExtensions.cs diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs similarity index 87% rename from src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationHandler.cs rename to src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs index 522100a02..73c292a80 100644 --- a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs @@ -42,15 +42,15 @@ protected override AuthenticationTicket AuthenticateCore() protected override async Task AuthenticateCoreAsync() { ExceptionDispatchInfo authFailedEx = null; - OAuthRequestTokenContext requestTokenContext = null; + OAuthBearerTokenContext requestTokenContext = null; try { // Find token in default location - requestTokenContext = new OAuthRequestTokenContext(Context, null); + requestTokenContext = new OAuthBearerTokenContext(Context, null); // Give application opportunity to find from a different location, adjust, or reject token var messageReceivedNotification = - new MessageReceivedNotification(Context, Options) + new MessageReceivedNotification(Context, Options) { ProtocolMessage = requestTokenContext, }; @@ -84,9 +84,10 @@ protected override async Task AuthenticateCoreAsync() // notify user token was received var securityTokenReceivedNotification = - new SecurityTokenReceivedNotification(Context, Options) + new SecurityTokenReceivedNotification(Context, Options) { ProtocolMessage = requestTokenContext, + SecurityToken = requestTokenContext.Token, }; await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); @@ -125,7 +126,7 @@ protected override async Task AuthenticateCoreAsync() ClaimsPrincipal principal = Options.SecurityTokenValidators.First().ValidateToken(requestTokenContext.Token, validationParameters, out validatedToken); ClaimsIdentity claimsIdentity = principal.Identity as ClaimsIdentity; AuthenticationTicket ticket = new AuthenticationTicket(claimsIdentity, new AuthenticationProperties()); - var securityTokenValidatedNotification = new SecurityTokenValidatedNotification(Context, Options) + var securityTokenValidatedNotification = new SecurityTokenValidatedNotification(Context, Options) { ProtocolMessage = requestTokenContext, AuthenticationTicket = ticket @@ -164,7 +165,7 @@ protected override async Task AuthenticateCoreAsync() } var authenticationFailedNotification = - new AuthenticationFailedNotification(Context, Options) + new AuthenticationFailedNotification(Context, Options) { ProtocolMessage = requestTokenContext, Exception = authFailedEx.SourceException @@ -192,22 +193,6 @@ protected override void ApplyResponseChallenge() ApplyResponseChallengeAsync().GetAwaiter().GetResult(); } - protected override async Task ApplyResponseChallengeAsync() - { - if (Response.StatusCode != 401) - { - return; - } - - if (ChallengeContext != null) - { - OAuthChallengeContext challengeContext = new OAuthChallengeContext(Context, _challenge); - await Options.Notifications.ApplyChallenge(challengeContext); - } - - return; - } - protected override void ApplyResponseGrant() { // N/A diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs similarity index 100% rename from src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationMiddleware.cs rename to src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs similarity index 98% rename from src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationOptions.cs rename to src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs index b87a35574..e61116e58 100644 --- a/src/Microsoft.AspNet.Security.OAuth/OAuthBearerAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs @@ -25,6 +25,7 @@ public OAuthBearerAuthenticationOptions() : base() { AuthenticationType = OAuthBearerAuthenticationDefaults.AuthenticationType; BackchannelTimeout = TimeSpan.FromMinutes(1); + Notifications = new OAuthBearerAuthenticationNotifications(); RefreshOnIssuerKeyNotFound = true; SystemClock = new SystemClock(); TokenValidationParameters = new TokenValidationParameters(); diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthTokenContext.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthTokenContext.cs new file mode 100644 index 000000000..229ab8b65 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthTokenContext.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.Notifications; + +namespace Microsoft.AspNet.Security.OAuth +{ + /// + /// Contains the context for 'OAuth bearer' authentication. + /// + public class OAuthBearerTokenContext : BaseContext + { + /// + /// Initializes a new + /// + /// HTTP environment + /// The authorization header value. + public OAuthBearerTokenContext( + HttpContext context, + string token) + : base(context) + { + Token = token; + } + + /// + /// The authorization header value + /// + public string Token { get; set; } + } +} 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..892b42b5f --- /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.OAuth { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.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/OpenIdConnectAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationOptions.cs index b6a595629..76c31dd0f 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationOptions.cs @@ -144,7 +144,7 @@ public string Caption /// /// The OpenIdConnect protocol http://openid.net/specs/openid-connect-core-1_0.html - /// recommends adding a nonce to a request to mitigate against replay attacks when obtaining id_tokens. + /// 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; } diff --git a/src/Microsoft.AspNet.Security/AuthenticationTicket.cs b/src/Microsoft.AspNet.Security/AuthenticationTicket.cs index 461599159..1aeab9094 100644 --- a/src/Microsoft.AspNet.Security/AuthenticationTicket.cs +++ b/src/Microsoft.AspNet.Security/AuthenticationTicket.cs @@ -25,8 +25,9 @@ public AuthenticationTicket(ClaimsIdentity identity, AuthenticationProperties pr /// /// 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; 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..7556e7eb6 --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs @@ -0,0 +1,138 @@ +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit; + +namespace Microsoft.AspNet.Security.OAuth +{ + 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 System.IdentityModel.Tokens.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 CustomTokenValidation() + { + var server = CreateServer(options => + { + options.Authority = "https://login.windows.net/tushartest.onmicrosoft.com"; + options.Audience = "https://TusharTest.onmicrosoft.com/TodoListService-ManualJwt"; + options.TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters + { + ValidateLifetime = false + }; + options.Notifications.SecurityTokenReceived = (notification) => + { + List claims = + new List + { + new Claim(ClaimTypes.Email, "bob@contoso.com") + }; + + notification.AuthenticationTicket = new AuthenticationTicket(new ClaimsIdentity(claims, options.AuthenticationType), new Http.Security.AuthenticationProperties()); + notification.HandleResponse(); + return null; + }; + }); + 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); + } + + private static TestServer CreateServer(Action configureOptions, Func handler = null) + { + return TestServer.Create(app => + { + app.UseServices(services => + { + services.AddDataProtection(); + }); + app.UseCookieAuthentication(options => + { + options.AuthenticationType = "Bearer"; + }); + 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), + }; + + 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; } + } + } +} \ 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..a5f07ba36 --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/OpenIdConnectMiddlewareTests/OpenIdConnectMiddlewareTests.cs @@ -0,0 +1,359 @@ +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 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 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..e7b5f1487 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-*" } } } From bba74e10cceceae930164a2fb162111dc9219b14 Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Tue, 13 Jan 2015 13:42:46 -0800 Subject: [PATCH 03/11] OAuthBearer 1. Change ns to OAuthBearer 2. Tests for notifications 3. Remove 'Challenge' 4. Notification M...> changed to HttpContext ConfigurationManager can be null --- .../NotNullAttribute.cs | 2 +- .../OAuthBearerAuthenticationNotifications.cs | 11 +- .../OAuthBearerAuthenticationDefaults.cs | 2 +- .../OAuthBearerAuthenticationExtensions.cs | 4 +- .../OAuthBearerAuthenticationHandler.cs | 76 +++++----- .../OAuthBearerAuthenticationMiddleware.cs | 26 +--- .../OAuthBearerAuthenticationOptions.cs | 17 +-- .../OAuthTokenContext.cs | 32 ---- .../Resources.Designer.cs | 2 +- .../OpenIdConnectAuthenticationMiddleware.cs | 2 +- .../OpenIdConnectAuthenticationOptions.cs | 46 +++--- .../OpenidConnectAuthenticationHandler.cs | 62 ++++---- .../OAuthBearer/OAuthBearerMiddlewareTests.cs | 143 +++++++++++++++--- 13 files changed, 236 insertions(+), 189 deletions(-) delete mode 100644 src/Microsoft.AspNet.Security.OAuthBearer/OAuthTokenContext.cs diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/NotNullAttribute.cs b/src/Microsoft.AspNet.Security.OAuthBearer/NotNullAttribute.cs index 3f56c4151..29f582749 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/NotNullAttribute.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/NotNullAttribute.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.AspNet.Security.OAuth +namespace Microsoft.AspNet.Security.OAuthBearer { [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] internal sealed class NotNullAttribute : Attribute diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs index f5d3a5826..db4d028b9 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNet.Http; using Microsoft.AspNet.Security.Notifications; using System; using System.Threading.Tasks; @@ -8,7 +9,7 @@ /// /// Specifies events which the invokes to enable developer control over the authentication process. /> /// -namespace Microsoft.AspNet.Security.OAuth +namespace Microsoft.AspNet.Security.OAuthBearer { /// /// OAuth bearer token middleware provider @@ -29,21 +30,21 @@ public OAuthBearerAuthenticationNotifications() /// /// 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; } + public Func, Task> AuthenticationFailed { get; set; } /// /// Invoked when a protocol message is first received. /// - public Func, Task> MessageReceived { get; set; } + 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; } + 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; } + public Func, Task> SecurityTokenValidated { get; set; } } } diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationDefaults.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationDefaults.cs index 70f1f1e61..5b62827cc 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/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.OAuthBearer/OAuthBearerAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationExtensions.cs index b5512d01c..dca51c93d 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationExtensions.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationExtensions.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using Microsoft.AspNet.Security.OAuth; +using Microsoft.AspNet.Security.OAuthBearer; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.OptionsModel; +using System; namespace Microsoft.AspNet.Builder { diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs index 73c292a80..d7644b2d0 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.Security.Infrastructure; using Microsoft.AspNet.Security.Notifications; @@ -14,20 +15,16 @@ using System.Security.Claims; using System.Threading.Tasks; -namespace Microsoft.AspNet.Security.OAuth +namespace Microsoft.AspNet.Security.OAuthBearer { internal class OAuthBearerAuthenticationHandler : AuthenticationHandler { - private const string HandledResponse = "HandledResponse"; - private readonly ILogger _logger; - private readonly string _challenge; private OpenIdConnectConfiguration _configuration; - public OAuthBearerAuthenticationHandler(ILogger logger, string challenge) + public OAuthBearerAuthenticationHandler(ILogger logger) { _logger = logger; - _challenge = challenge; } protected override AuthenticationTicket AuthenticateCore() @@ -42,17 +39,14 @@ protected override AuthenticationTicket AuthenticateCore() protected override async Task AuthenticateCoreAsync() { ExceptionDispatchInfo authFailedEx = null; - OAuthBearerTokenContext requestTokenContext = null; + string token = null; try { - // Find token in default location - requestTokenContext = new OAuthBearerTokenContext(Context, null); - // Give application opportunity to find from a different location, adjust, or reject token var messageReceivedNotification = - new MessageReceivedNotification(Context, Options) + new MessageReceivedNotification(Context, Options) { - ProtocolMessage = requestTokenContext, + ProtocolMessage = Context, }; // notification can set the token @@ -67,27 +61,24 @@ protected override async Task AuthenticateCoreAsync() return null; } - if (string.IsNullOrEmpty(requestTokenContext.Token)) + string authorization = Request.Headers.Get("Authorization"); + if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { - string authorization = Request.Headers.Get("Authorization"); - if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) - { - requestTokenContext.Token = authorization.Substring("Bearer ".Length).Trim(); - } + token = authorization.Substring("Bearer ".Length).Trim(); } // If no token found, no further work possible - if (string.IsNullOrEmpty(requestTokenContext.Token)) + if (string.IsNullOrEmpty(token)) { return null; } // notify user token was received var securityTokenReceivedNotification = - new SecurityTokenReceivedNotification(Context, Options) + new SecurityTokenReceivedNotification(Context, Options) { - ProtocolMessage = requestTokenContext, - SecurityToken = requestTokenContext.Token, + ProtocolMessage = Context, + SecurityToken = token, }; await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); @@ -101,34 +92,37 @@ protected override async Task AuthenticateCoreAsync() return null; } - if (_configuration == null) + if (_configuration == null && Options.ConfigurationManager != null) { _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } var validationParameters = Options.TokenValidationParameters.Clone(); - if (validationParameters.ValidIssuer == null && !string.IsNullOrWhiteSpace(_configuration.Issuer)) - { - validationParameters.ValidIssuer = _configuration.Issuer; - } - else + if (_configuration != null) { - IEnumerable issuers = new[] { _configuration.Issuer }; - validationParameters.ValidIssuers = (validationParameters.ValidIssuers == null ? issuers : validationParameters.ValidIssuers.Concat(issuers)); + 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)); } - validationParameters.IssuerSigningKeys = (validationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : validationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys)); SecurityToken validatedToken; foreach (var validator in Options.SecurityTokenValidators) { - if (validator.CanReadToken(requestTokenContext.Token)) + if (validator.CanReadToken(token)) { - ClaimsPrincipal principal = Options.SecurityTokenValidators.First().ValidateToken(requestTokenContext.Token, validationParameters, out validatedToken); - ClaimsIdentity claimsIdentity = principal.Identity as ClaimsIdentity; - AuthenticationTicket ticket = new AuthenticationTicket(claimsIdentity, new AuthenticationProperties()); - var securityTokenValidatedNotification = new SecurityTokenValidatedNotification(Context, Options) + ClaimsPrincipal principal = validator.ValidateToken(token, validationParameters, out validatedToken); + AuthenticationTicket ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), Options.AuthenticationType); + var securityTokenValidatedNotification = new SecurityTokenValidatedNotification(Context, Options) { - ProtocolMessage = requestTokenContext, + ProtocolMessage = Context, AuthenticationTicket = ticket }; @@ -146,7 +140,7 @@ protected override async Task AuthenticateCoreAsync() } } - throw new InvalidOperationException("No SecurityTokenValidator available for token: " + requestTokenContext.Token); + throw new InvalidOperationException("No SecurityTokenValidator available for token: " + token ?? "null"); } catch (Exception ex) { @@ -165,9 +159,9 @@ protected override async Task AuthenticateCoreAsync() } var authenticationFailedNotification = - new AuthenticationFailedNotification(Context, Options) + new AuthenticationFailedNotification(Context, Options) { - ProtocolMessage = requestTokenContext, + ProtocolMessage = Context, Exception = authFailedEx.SourceException }; @@ -190,7 +184,7 @@ protected override async Task AuthenticateCoreAsync() protected override void ApplyResponseChallenge() { - ApplyResponseChallengeAsync().GetAwaiter().GetResult(); + // N/A } protected override void ApplyResponseGrant() diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs index 694fbda61..c406bf2a3 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs @@ -8,12 +8,13 @@ using Microsoft.Framework.OptionsModel; using Microsoft.IdentityModel.Protocols; using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.IdentityModel.Tokens; using System.Net.Http; -namespace Microsoft.AspNet.Security.OAuth +namespace Microsoft.AspNet.Security.OAuthBearer { /// /// Bearer authentication middleware component which is added to an HTTP pipeline. This class is not @@ -24,8 +25,6 @@ public class OAuthBearerAuthenticationMiddleware : AuthenticationMiddleware /// Bearer authentication component which is added to an HTTP pipeline. This constructor is not /// called by application code directly, instead it is added by calling the the IAppBuilder UseOAuthBearerAuthentication @@ -41,20 +40,6 @@ public OAuthBearerAuthenticationMiddleware( : 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(); @@ -62,7 +47,7 @@ public OAuthBearerAuthenticationMiddleware( if (Options.SecurityTokenValidators == null) { - Options.SecurityTokenValidators = new Collection { new JwtSecurityTokenHandler() }; + Options.SecurityTokenValidators = new List { new JwtSecurityTokenHandler() }; } if (string.IsNullOrWhiteSpace(Options.TokenValidationParameters.ValidAudience) && !string.IsNullOrWhiteSpace(Options.Audience)) @@ -76,7 +61,7 @@ public OAuthBearerAuthenticationMiddleware( { Options.ConfigurationManager = new StaticConfigurationManager(Options.Configuration); } - else + else if (!(string.IsNullOrWhiteSpace(Options.MetadataAddress) && string.IsNullOrWhiteSpace(Options.Authority))) { if (string.IsNullOrWhiteSpace(Options.MetadataAddress) && !string.IsNullOrWhiteSpace(Options.Authority)) { @@ -92,6 +77,7 @@ public OAuthBearerAuthenticationMiddleware( HttpClient httpClient = new HttpClient(ResolveHttpMessageHandler(Options)); httpClient.Timeout = Options.BackchannelTimeout; httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + Options.ConfigurationManager = new ConfigurationManager(Options.MetadataAddress, httpClient); } } @@ -103,7 +89,7 @@ public OAuthBearerAuthenticationMiddleware( /// A new instance of the request handler protected override AuthenticationHandler CreateHandler() { - return new OAuthBearerAuthenticationHandler(_logger, _challenge); + return new OAuthBearerAuthenticationHandler(_logger); } [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs index e61116e58..72141d970 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs @@ -8,7 +8,7 @@ using System.IdentityModel.Tokens; using System.Net.Http; -namespace Microsoft.AspNet.Security.OAuth +namespace Microsoft.AspNet.Security.OAuthBearer { /// /// Options class provides information needed to control Bearer Authentication middleware behavior @@ -31,19 +31,6 @@ public OAuthBearerAuthenticationOptions() : base() TokenValidationParameters = new TokenValidationParameters(); } - /// - /// 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; } - /// /// Gets or sets the discovery endpoint for obtaining metadata /// @@ -118,7 +105,7 @@ public OAuthBearerAuthenticationOptions() : base() public ISystemClock SystemClock { get; set; } /// - /// Gets or sets the of s used to read and validate s. + /// Gets or sets the for validating tokens. /// /// if 'value' is null. public ICollection SecurityTokenValidators diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthTokenContext.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthTokenContext.cs deleted file mode 100644 index 229ab8b65..000000000 --- a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthTokenContext.cs +++ /dev/null @@ -1,32 +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; -using Microsoft.AspNet.Security.Notifications; - -namespace Microsoft.AspNet.Security.OAuth -{ - /// - /// Contains the context for 'OAuth bearer' authentication. - /// - public class OAuthBearerTokenContext : BaseContext - { - /// - /// Initializes a new - /// - /// HTTP environment - /// The authorization header value. - public OAuthBearerTokenContext( - HttpContext context, - string token) - : base(context) - { - Token = token; - } - - /// - /// The authorization header value - /// - public string Token { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/Resources.Designer.cs b/src/Microsoft.AspNet.Security.OAuthBearer/Resources.Designer.cs index 892b42b5f..37abb160e 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/Resources.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNet.Security.OAuth { +namespace Microsoft.AspNet.Security.OAuthBearer { using System; diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationMiddleware.cs index 9d7d9af29..6d57f1b41 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationMiddleware.cs @@ -104,7 +104,7 @@ public OpenIdConnectAuthenticationMiddleware( { Options.ConfigurationManager = new StaticConfigurationManager(Options.Configuration); } - else + else if (!(string.IsNullOrWhiteSpace(Options.MetadataAddress) && string.IsNullOrWhiteSpace(Options.Authority))) { if (string.IsNullOrWhiteSpace(Options.MetadataAddress) && !string.IsNullOrWhiteSpace(Options.Authority)) { diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationOptions.cs index 76c31dd0f..e6139177c 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationOptions.cs @@ -52,6 +52,7 @@ public OpenIdConnectAuthenticationOptions() public OpenIdConnectAuthenticationOptions(string authenticationType) { AuthenticationMode = AuthenticationMode.Active; + AuthenticationType = authenticationType; BackchannelTimeout = TimeSpan.FromMinutes(1); Caption = OpenIdConnectAuthenticationDefaults.Caption; ProtocolValidator = new OpenIdConnectProtocolValidator(); @@ -60,7 +61,6 @@ public OpenIdConnectAuthenticationOptions(string authenticationType) Scope = OpenIdConnectScopes.OpenIdProfile; TokenValidationParameters = new TokenValidationParameters(); UseTokenLifetime = true; - AuthenticationType = authenticationType; } /// @@ -279,6 +279,28 @@ public ISecureDataFormat StringDataFormat } } + /// + /// 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 /// @@ -311,27 +333,5 @@ public bool UseTokenLifetime 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; - } - } } } diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs index 570ebce20..11065a7f7 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs @@ -24,7 +24,6 @@ public class OpenIdConnectAuthenticationHandler : AuthenticationHandler AuthenticateCoreAsync() return null; } - if (_configuration == 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 (string.IsNullOrWhiteSpace(validationParameters.ValidIssuer)) - { - validationParameters.ValidIssuer = _configuration.Issuer; - } - else if (!string.IsNullOrWhiteSpace(_configuration.Issuer)) + if (_configuration != null) { - validationParameters.ValidIssuers = (validationParameters.ValidIssuers == null ? new[] { _configuration.Issuer } : validationParameters.ValidIssuers.Concat(new[] { _configuration.Issuer })); - } + 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)); - ISecurityTokenValidator validator = null; - foreach(var v in Options.SecurityTokenValidators) - { - if (v.CanReadToken(openIdConnectMessage.IdToken)) - validator = v; + validationParameters.IssuerSigningKeys = (validationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : validationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys)); } - if (validator == null) + AuthenticationTicket ticket; + SecurityToken validatedToken = null; + ClaimsPrincipal principal = null; + JwtSecurityToken jwt = null; + + foreach (var validator in Options.SecurityTokenValidators) { - throw new InvalidOperationException("No SecurityTokenValidator found for token: " + openIdConnectMessage.IdToken); + 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())); + } + } } - SecurityToken validatedToken; - ClaimsPrincipal principal = validator.ValidateToken(openIdConnectMessage.IdToken, validationParameters, out validatedToken); - JwtSecurityToken jwt = validatedToken as JwtSecurityToken; - if (jwt == null) + if (validatedToken == null) { - throw new InvalidOperationException("Validated Security Token must be a JwtSecurityToken was: " + validatedToken.GetType()); + throw new InvalidOperationException("No SecurityTokenValidator found for token: " + openIdConnectMessage.IdToken); } - AuthenticationTicket ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationType); + ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationType); if (!string.IsNullOrWhiteSpace(openIdConnectMessage.SessionState)) { ticket.Properties.Dictionary[OpenIdConnectSessionProperties.SessionState] = openIdConnectMessage.SessionState; } - if (!string.IsNullOrWhiteSpace(_configuration.CheckSessionIframe)) + if (_configuration != null && !string.IsNullOrWhiteSpace(_configuration.CheckSessionIframe)) { ticket.Properties.Dictionary[OpenIdConnectSessionProperties.CheckSessionIFrame] = _configuration.CheckSessionIframe; } diff --git a/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs index 7556e7eb6..9d58d36d2 100644 --- a/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs @@ -1,10 +1,12 @@ using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.Notifications; using Microsoft.AspNet.TestHost; using Microsoft.Framework.DependencyInjection; using Shouldly; using System; using System.Collections.Generic; +using System.IdentityModel.Tokens; using System.Linq; using System.Net; using System.Net.Http; @@ -13,7 +15,7 @@ using System.Xml.Linq; using Xunit; -namespace Microsoft.AspNet.Security.OAuth +namespace Microsoft.AspNet.Security.OAuthBearer { public class OAuthBearerMiddlewareTests { @@ -24,7 +26,7 @@ public async Task BearerTokenValidation() { options.Authority = "https://login.windows.net/tushartest.onmicrosoft.com"; options.Audience = "https://TusharTest.onmicrosoft.com/TodoListService-ManualJwt"; - options.TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters + options.TokenValidationParameters = new TokenValidationParameters { ValidateLifetime = false }; @@ -35,34 +37,137 @@ public async Task BearerTokenValidation() } [Fact] - public async Task CustomTokenValidation() + public async Task CustomHeaderReceived() { var server = CreateServer(options => { - options.Authority = "https://login.windows.net/tushartest.onmicrosoft.com"; - options.Audience = "https://TusharTest.onmicrosoft.com/TodoListService-ManualJwt"; - options.TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters + 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 { - ValidateLifetime = false + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob"), }; - options.Notifications.SecurityTokenReceived = (notification) => + + 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 { - List claims = - new List - { - new Claim(ClaimTypes.Email, "bob@contoso.com") - }; - - notification.AuthenticationTicket = new AuthenticationTicket(new ClaimsIdentity(claims, options.AuthenticationType), new Http.Security.AuthenticationProperties()); - notification.HandleResponse(); - return null; + 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)}; }); - 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); + + 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 => From e84ecacfc6050e638e4d98199580ddd7bf98c56a Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Tue, 13 Jan 2015 14:30:11 -0800 Subject: [PATCH 04/11] fix casing of OpenID -> OpenId --- samples/OpenIDConnectSample/Startup.cs | 3 +-- samples/OpenIDConnectSample/project.json | 2 +- .../Microsoft.AspNet.Security.OpenIDConnect.kproj | 2 +- test/Microsoft.AspNet.Security.Test/project.json | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/samples/OpenIDConnectSample/Startup.cs b/samples/OpenIDConnectSample/Startup.cs index 501030ad2..17850d03b 100644 --- a/samples/OpenIDConnectSample/Startup.cs +++ b/samples/OpenIDConnectSample/Startup.cs @@ -4,9 +4,8 @@ using Microsoft.AspNet.Security.OpenIdConnect; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.Security; -using Microsoft.AspNet.Security.Cookies; -namespace OpenIDConnectSample +namespace OpenIdConnectSample { public class Startup { diff --git a/samples/OpenIDConnectSample/project.json b/samples/OpenIDConnectSample/project.json index 4fd1622b8..909f68b4c 100644 --- a/samples/OpenIDConnectSample/project.json +++ b/samples/OpenIDConnectSample/project.json @@ -4,7 +4,7 @@ "Microsoft.AspNet.Server.WebListener": "1.0.0-*", "Kestrel": "1.0.0-*", "Microsoft.AspNet.Server.IIS": "1.0.0-*", - "Microsoft.AspNet.Security.OpenIDConnect": "1.0.0-*" + "Microsoft.AspNet.Security.OpenIdConnect": "1.0.0-*" }, "frameworks": { "aspnet50": { }, diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/Microsoft.AspNet.Security.OpenIDConnect.kproj b/src/Microsoft.AspNet.Security.OpenIDConnect/Microsoft.AspNet.Security.OpenIDConnect.kproj index 5f8eb40e8..ed1a12239 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/Microsoft.AspNet.Security.OpenIDConnect.kproj +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/Microsoft.AspNet.Security.OpenIDConnect.kproj @@ -7,7 +7,7 @@ 674d128e-83bb-481a-a9d9-6d47872e1fc8 - Microsoft.AspNet.Security.OpenIDConnect + Microsoft.AspNet.Security.OpenIdConnect ..\..\artifacts\obj\$(MSBuildProjectName) ..\..\artifacts\bin\$(MSBuildProjectName)\ diff --git a/test/Microsoft.AspNet.Security.Test/project.json b/test/Microsoft.AspNet.Security.Test/project.json index e7b5f1487..504d228b6 100644 --- a/test/Microsoft.AspNet.Security.Test/project.json +++ b/test/Microsoft.AspNet.Security.Test/project.json @@ -8,7 +8,7 @@ "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.OpenIdConnect": "1.0.0-*", "Microsoft.AspNet.Security.Twitter": "1.0.0-*", "Microsoft.AspNet.TestHost": "1.0.0-*", "Moq": "4.2.1312.1622", From d67ab02576214da1119d2fe024e536c3db4ba2f8 Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Tue, 13 Jan 2015 14:31:14 -0800 Subject: [PATCH 05/11] Casing of OpenIDConnect -> OpenIdConnect --- Security.sln | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Security.sln b/Security.sln index 73c662584..ccddd42c7 100644 --- a/Security.sln +++ b/Security.sln @@ -36,12 +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.OpenIDConnect", "src\Microsoft.AspNet.Security.OpenIDConnect\Microsoft.AspNet.Security.OpenIDConnect.kproj", "{674D128E-83BB-481A-A9D9-6D47872E1FC8}" +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 @@ -174,18 +174,6 @@ Global {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 - {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 {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 @@ -198,6 +186,18 @@ Global {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 @@ -215,7 +215,7 @@ Global {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} - {674D128E-83BB-481A-A9D9-6D47872E1FC8} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {2755BFE5-7421-4A31-A644-F817DF5CAA98} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {674D128E-83BB-481A-A9D9-6D47872E1FC8} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} EndGlobalSection EndGlobal From 8042372670887af2f13ad9f30076855c07007e74 Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Tue, 13 Jan 2015 14:41:56 -0800 Subject: [PATCH 06/11] Changed case on OpenIdConnect project files --- .../{OpenIDConnectSample.kproj => OpenIdConnectSample.kproj} | 0 ...onnect.kproj => Microsoft.AspNet.Security.OpenIdConnect.kproj} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename samples/OpenIDConnectSample/{OpenIDConnectSample.kproj => OpenIdConnectSample.kproj} (100%) rename src/Microsoft.AspNet.Security.OpenIDConnect/{Microsoft.AspNet.Security.OpenIDConnect.kproj => Microsoft.AspNet.Security.OpenIdConnect.kproj} (100%) diff --git a/samples/OpenIDConnectSample/OpenIDConnectSample.kproj b/samples/OpenIDConnectSample/OpenIdConnectSample.kproj similarity index 100% rename from samples/OpenIDConnectSample/OpenIDConnectSample.kproj rename to samples/OpenIDConnectSample/OpenIdConnectSample.kproj diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/Microsoft.AspNet.Security.OpenIDConnect.kproj b/src/Microsoft.AspNet.Security.OpenIDConnect/Microsoft.AspNet.Security.OpenIdConnect.kproj similarity index 100% rename from src/Microsoft.AspNet.Security.OpenIDConnect/Microsoft.AspNet.Security.OpenIDConnect.kproj rename to src/Microsoft.AspNet.Security.OpenIDConnect/Microsoft.AspNet.Security.OpenIdConnect.kproj From 5509334e511a0dd9b9229491ed745c886c731c9b Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Tue, 13 Jan 2015 16:24:09 -0800 Subject: [PATCH 07/11] Added feed for AZureADNightly --- NuGet.Config | 1 + 1 file changed, 1 insertion(+) diff --git a/NuGet.Config b/NuGet.Config index f41e9c631..4ee105534 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -3,5 +3,6 @@ + From 0eccc6c8525a98055a9adfedb5a68fc4868de328 Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Tue, 13 Jan 2015 19:02:38 -0800 Subject: [PATCH 08/11] Reordering of usings, responding to pull request 112. --- samples/OpenIDConnectSample/project.json | 8 +++---- .../project.json | 2 -- .../OAuthBearerAuthenticationNotifications.cs | 6 ++--- .../OAuthBearerAuthenticationExtensions.cs | 2 +- .../OAuthBearerAuthenticationHandler.cs | 14 +++++------ .../OAuthBearerAuthenticationMiddleware.cs | 23 +++++++++--------- .../OAuthBearerAuthenticationOptions.cs | 4 ++-- .../OpenIdConnectAuthenticationExtensions.cs | 4 ++-- .../OpenIdConnectAuthenticationMiddleware.cs | 12 +++++----- ...penIdConnectAuthenticationNotifications.cs | 2 +- .../OpenIdConnectAuthenticationOptions.cs | 6 ++--- .../OpenidConnectAuthenticationHandler.cs | 14 +++++------ .../OAuthBearer/OAuthBearerMiddlewareTests.cs | 14 ++++++----- .../OpenIdConnectMiddlewareTests.cs | 24 ++++++++++--------- 14 files changed, 68 insertions(+), 67 deletions(-) diff --git a/samples/OpenIDConnectSample/project.json b/samples/OpenIDConnectSample/project.json index 909f68b4c..65652110b 100644 --- a/samples/OpenIDConnectSample/project.json +++ b/samples/OpenIDConnectSample/project.json @@ -1,10 +1,10 @@ { "dependencies": { - "Microsoft.AspNet.Security.Cookies": "1.0.0-*", - "Microsoft.AspNet.Server.WebListener": "1.0.0-*", "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.Security.OpenIdConnect": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*" }, "frameworks": { "aspnet50": { }, @@ -14,5 +14,5 @@ "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": "d://wwwroot" + "webroot": "wwwroot" } diff --git a/src/Microsoft.AspNet.Security.OAuth/project.json b/src/Microsoft.AspNet.Security.OAuth/project.json index 0bae926b9..39ba020fc 100644 --- a/src/Microsoft.AspNet.Security.OAuth/project.json +++ b/src/Microsoft.AspNet.Security.OAuth/project.json @@ -4,8 +4,6 @@ "dependencies": { "Microsoft.AspNet.Security": "1.0.0-*", "Microsoft.AspNet.Security.DataProtection": "1.0.0-*", - "Microsoft.IdentityModel.Protocol.Extensions": "2.0.0-beta1-*", - "System.IdentityModel.Tokens": "5.0.0-beta1-*" }, "frameworks": { "aspnet50": { diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs index db4d028b9..f130d4390 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs @@ -1,10 +1,10 @@ // 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; 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. /> @@ -25,7 +25,7 @@ public OAuthBearerAuthenticationNotifications() 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. diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationExtensions.cs index dca51c93d..0f31dfaf8 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationExtensions.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationExtensions.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNet.Security.OAuthBearer; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.OptionsModel; -using System; namespace Microsoft.AspNet.Builder { diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs index d7644b2d0..b939a7127 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs @@ -1,12 +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 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; using System; using System.Collections.Generic; using System.IdentityModel.Tokens; @@ -14,6 +8,12 @@ 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 { @@ -150,7 +150,7 @@ protected override async Task AuthenticateCoreAsync() if (authFailedEx != null) { - _logger.WriteError("Exception occurred while processing message: '" + authFailedEx.ToString()); + _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))) diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs index c406bf2a3..8eb0da3bc 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs @@ -1,27 +1,26 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +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; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.IdentityModel.Tokens; -using System.Net.Http; 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 + /// + /// 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; diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs index 72141d970..097750fcb 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs @@ -1,12 +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 Microsoft.AspNet.Security.Infrastructure; -using Microsoft.IdentityModel.Protocols; 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 { diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationExtensions.cs index 6ba5c1f88..81a74b65c 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationExtensions.cs +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationExtensions.cs @@ -12,10 +12,10 @@ namespace Microsoft.AspNet.Builder public static class OpenIdConnectAuthenticationExtensions { /// - /// Adds the into the OWIN runtime. + /// Adds the into the ASP.NET runtime. /// /// The application builder - /// Options which control the processing of the bearer header. + /// 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 = "") { diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationMiddleware.cs index 6d57f1b41..bfe82b51f 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationMiddleware.cs @@ -5,22 +5,22 @@ 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; -using Microsoft.AspNet.Security.DataHandler.Serializer; -using Microsoft.AspNet.Security.DataHandler.Encoder; -using System.Text; namespace Microsoft.AspNet.Security.OpenIdConnect { /// - /// OWIN middleware for obtaining identities using OpenIdConnect protocol. + /// ASP.NET middleware for obtaining identities using OpenIdConnect protocol. /// public class OpenIdConnectAuthenticationMiddleware : AuthenticationMiddleware { @@ -29,8 +29,8 @@ public class OpenIdConnectAuthenticationMiddleware : AuthenticationMiddleware /// Initializes a /// - /// The next middleware in the OWIN pipeline to invoke - /// The OWIN application + /// 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( diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationNotifications.cs index e4dc545fc..35354821e 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationNotifications.cs @@ -2,8 +2,8 @@ using System; using System.Threading.Tasks; -using Microsoft.IdentityModel.Protocols; using Microsoft.AspNet.Security.Notifications; +using Microsoft.IdentityModel.Protocols; namespace Microsoft.AspNet.Security.OpenIdConnect { diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationOptions.cs index e6139177c..5022cbf3c 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationOptions.cs @@ -1,13 +1,13 @@ // 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.Http.Security; -using Microsoft.IdentityModel.Protocols; 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 { diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs index 11065a7f7..620621711 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs @@ -1,11 +1,5 @@ // 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.Http.Security; -using Microsoft.AspNet.Security.Infrastructure; -using Microsoft.AspNet.Security.Notifications; -using Microsoft.Framework.Logging; -using Microsoft.IdentityModel.Protocols; using System; using System.Globalization; using System.IdentityModel.Tokens; @@ -14,6 +8,12 @@ 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 { @@ -426,7 +426,7 @@ protected override async Task AuthenticateCoreAsync() if (authFailedEx != null) { - _logger.WriteError("Exception occurred while processing message: '" + authFailedEx.ToString()); + _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))) diff --git a/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs index 9d58d36d2..25d3ddc42 100644 --- a/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs @@ -1,9 +1,5 @@ -using Microsoft.AspNet.Builder; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Security.Notifications; -using Microsoft.AspNet.TestHost; -using Microsoft.Framework.DependencyInjection; -using Shouldly; +// 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; @@ -13,6 +9,12 @@ 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 diff --git a/test/Microsoft.AspNet.Security.Test/OpenIdConnectMiddlewareTests/OpenIdConnectMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/OpenIdConnectMiddlewareTests/OpenIdConnectMiddlewareTests.cs index a5f07ba36..652c295ec 100644 --- a/test/Microsoft.AspNet.Security.Test/OpenIdConnectMiddlewareTests/OpenIdConnectMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Security.Test/OpenIdConnectMiddlewareTests/OpenIdConnectMiddlewareTests.cs @@ -1,14 +1,5 @@ -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; +// 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; @@ -21,6 +12,17 @@ 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 From b399dbdcd7e3391cd216ea69a0dcbaf1090dd311 Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Wed, 14 Jan 2015 11:36:48 -0800 Subject: [PATCH 09/11] OIDC - removed virtual from nonce methods and made private as INonceCache is available. OAuthBearer - added Challenge Response for OAuthBearer, removed cookieMiddleware. --- .../AuthenticationChallengeNotification.cs | 15 +++++++++++++++ .../OAuthBearerAuthenticationNotifications.cs | 6 ++++++ .../OAuthBearerAuthenticationHandler.cs | 9 +++++++-- .../OAuthBearerAuthenticationMiddleware.cs | 1 - .../OAuthBearerAuthenticationOptions.cs | 7 +++++++ .../OpenidConnectAuthenticationHandler.cs | 4 ++-- .../OAuthBearer/OAuthBearerMiddlewareTests.cs | 14 +++----------- 7 files changed, 40 insertions(+), 16 deletions(-) create mode 100644 src/Microsoft.AspNet.Security.OAuthBearer/Notifications/AuthenticationChallengeNotification.cs 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 index f130d4390..808615e1d 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/Notifications/OAuthBearerAuthenticationNotifications.cs @@ -21,6 +21,7 @@ public class OAuthBearerAuthenticationNotifications /// 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); @@ -46,5 +47,10 @@ public OAuthBearerAuthenticationNotifications() /// 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.OAuthBearer/OAuthBearerAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs index b939a7127..1c2eb287a 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationHandler.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNet.Security.OAuthBearer { - internal class OAuthBearerAuthenticationHandler : AuthenticationHandler + public class OAuthBearerAuthenticationHandler : AuthenticationHandler { private readonly ILogger _logger; private OpenIdConnectConfiguration _configuration; @@ -184,7 +184,12 @@ protected override async Task AuthenticateCoreAsync() protected override void ApplyResponseChallenge() { - // N/A + ApplyResponseChallengeAsync().GetAwaiter().GetResult(); + } + + protected override async Task ApplyResponseChallengeAsync() + { + await Options.Notifications.ApplyChallenge(new AuthenticationChallengeNotification(Context, Options)); } protected override void ApplyResponseGrant() diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs index 8eb0da3bc..0403896ae 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs @@ -32,7 +32,6 @@ public class OAuthBearerAuthenticationMiddleware : AuthenticationMiddleware options, ConfigureOptions configureOptions) diff --git a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs index 097750fcb..08acf5bf4 100644 --- a/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.OAuthBearer/OAuthBearerAuthenticationOptions.cs @@ -25,6 +25,7 @@ public OAuthBearerAuthenticationOptions() : base() { AuthenticationType = OAuthBearerAuthenticationDefaults.AuthenticationType; BackchannelTimeout = TimeSpan.FromMinutes(1); + Challenge = OAuthBearerAuthenticationDefaults.AuthenticationType; Notifications = new OAuthBearerAuthenticationNotifications(); RefreshOnIssuerKeyNotFound = true; SystemClock = new SystemClock(); @@ -49,6 +50,12 @@ public OAuthBearerAuthenticationOptions() : base() /// 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 diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs index 620621711..735978e8a 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs @@ -463,7 +463,7 @@ protected override async Task AuthenticateCoreAsync() /// the nonce to remember. /// is called to add a cookie with the name: 'OpenIdConnectAuthenticationDefaults.Nonce + (nonce)'. /// The value of the cookie is: "N". - public virtual void RememberNonce(string nonce) + private void RememberNonce(string nonce) { if (string.IsNullOrWhiteSpace(nonce)) { @@ -487,7 +487,7 @@ public virtual void RememberNonce(string nonce) /// '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. - protected virtual string RetrieveNonce(string nonceExpectedValue) + private string RetrieveNonce(string nonceExpectedValue) { if (nonceExpectedValue == null) { diff --git a/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs index 25d3ddc42..6fa8b7afd 100644 --- a/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs @@ -162,8 +162,8 @@ public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParame List claims = new List { - new Claim(ClaimTypes.Email, "bob@contoso.com"), - new Claim(ClaimsIdentity.DefaultNameClaimType, "bob"), + new Claim(ClaimTypes.Email, "bob@contoso.com"), + new Claim(ClaimsIdentity.DefaultNameClaimType, "bob"), }; return new ClaimsPrincipal(new ClaimsIdentity(claims, AuthenticationType)); @@ -178,10 +178,7 @@ private static TestServer CreateServer(Action { services.AddDataProtection(); }); - app.UseCookieAuthentication(options => - { - options.AuthenticationType = "Bearer"; - }); + if (configureOptions != null) { app.UseOAuthBearerAuthentication(configureOptions); @@ -216,11 +213,6 @@ private static async Task SendAsync(TestServer server, string uri, 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 && From 116cd4c17d70b10cb0180ec8b9c629d969fa16fd Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Wed, 14 Jan 2015 12:36:43 -0800 Subject: [PATCH 10/11] Fix some nits, formating ordering etc. --- .../OpenIdConnectAuthenticationNotifications.cs | 2 +- .../OpenidConnectAuthenticationHandler.cs | 2 +- src/Microsoft.AspNet.Security/AuthenticationTicket.cs | 2 +- .../Notifications/AuthenticationFailedNotification.cs | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationNotifications.cs index 35354821e..36ee2fd25 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenIdConnectAuthenticationNotifications.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNet.Security.OpenIdConnect { /// - /// Specifies events which the invokes to enable developer control over the authentication process. /> + /// Specifies events which the invokes to enable developer control over the authentication process. /// public class OpenIdConnectAuthenticationNotifications { diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs index 735978e8a..688d245cc 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs @@ -248,7 +248,7 @@ protected override async Task AuthenticateCoreAsync() return messageReceivedNotification.AuthenticationTicket; } - if (messageReceivedNotification.Skipped) + if (messageReceivedNotification.Skipped) { return null; } diff --git a/src/Microsoft.AspNet.Security/AuthenticationTicket.cs b/src/Microsoft.AspNet.Security/AuthenticationTicket.cs index 1aeab9094..7b7e50998 100644 --- a/src/Microsoft.AspNet.Security/AuthenticationTicket.cs +++ b/src/Microsoft.AspNet.Security/AuthenticationTicket.cs @@ -1,8 +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 Microsoft.AspNet.Http.Security; using System.Security.Claims; +using Microsoft.AspNet.Http.Security; namespace Microsoft.AspNet.Security { diff --git a/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs b/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs index 7f5ae0697..5d232426f 100644 --- a/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs +++ b/src/Microsoft.AspNet.Security/Notifications/AuthenticationFailedNotification.cs @@ -1,9 +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 Microsoft.AspNet.Http; using System; +using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Security.Notifications { From ccd63c1e3bd618760637ecc69d610383c6bb8467 Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Wed, 14 Jan 2015 14:36:38 -0800 Subject: [PATCH 11/11] buffer is automatically copied, no need to do it again. --- .../OpenidConnectAuthenticationHandler.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs index 688d245cc..530503910 100644 --- a/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.OpenIDConnect/OpenidConnectAuthenticationHandler.cs @@ -212,17 +212,7 @@ protected override async Task AuthenticateCoreAsync() && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) && Request.Body.CanRead) { - if (!Request.Body.CanSeek) - { - _logger.WriteVerbose("Buffering request body"); - // Buffer in case this body was not meant for us. - MemoryStream memoryStream = new MemoryStream(); - await Request.Body.CopyToAsync(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - Request.Body = memoryStream; - } - - IFormCollection form = (IFormCollection) await Request.ReadFormAsync(); + 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.