diff --git a/samples/CookieSessionSample/CookieSessionSample.xproj b/samples/CookieSessionSample/CookieSessionSample.xproj index 1216341876..e129ba553c 100644 --- a/samples/CookieSessionSample/CookieSessionSample.xproj +++ b/samples/CookieSessionSample/CookieSessionSample.xproj @@ -12,7 +12,7 @@ 2.0 - 22571 + 36505 \ No newline at end of file diff --git a/samples/SocialSample/SocialSample.xproj b/samples/SocialSample/SocialSample.xproj index ce1e69ddf4..74aaf1222f 100644 --- a/samples/SocialSample/SocialSample.xproj +++ b/samples/SocialSample/SocialSample.xproj @@ -12,7 +12,7 @@ 2.0 - 22570 + 36504 \ No newline at end of file diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs index dab45953da..bc7d61b4ae 100644 --- a/samples/SocialSample/Startup.cs +++ b/samples/SocialSample/Startup.cs @@ -1,6 +1,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; +using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.DataProtection; using Microsoft.AspNet.Http; @@ -28,6 +29,13 @@ namespace CookieSample { options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; }); + services.ConfigureClaimsTransformation(p => + { + var id = new ClaimsIdentity("xform"); + id.AddClaim(new Claim("ClaimsTransformation", "TransformAddedClaim")); + p.AddIdentity(id); + return p; + }); }); app.UseCookieAuthentication(options => diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs index 808a2691a2..8ccbb36530 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs @@ -13,7 +13,7 @@ using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Authentication.Cookies { - internal class CookieAuthenticationHandler : AutomaticAuthenticationHandler + internal class CookieAuthenticationHandler : AuthenticationHandler { private const string HeaderNameCacheControl = "Cache-Control"; private const string HeaderNamePragma = "Pragma"; diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs index e3ef3f5700..cd6b53b39e 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs @@ -15,11 +15,12 @@ namespace Microsoft.AspNet.Authentication.Cookies { private readonly ILogger _logger; - public CookieAuthenticationMiddleware(RequestDelegate next, - IServiceProvider services, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - IOptions options, + public CookieAuthenticationMiddleware( + [NotNull] RequestDelegate next, + [NotNull] IServiceProvider services, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IOptions options, ConfigureOptions configureOptions) : base(next, services, options, configureOptions) { diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs index 02e9743b4a..c2fc4c3a71 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Authentication.Cookies /// /// Contains the options used by the CookiesAuthenticationMiddleware /// - public class CookieAuthenticationOptions : AutomaticAuthenticationOptions + public class CookieAuthenticationOptions : AuthenticationOptions { private string _cookieName; @@ -20,7 +20,6 @@ namespace Microsoft.AspNet.Authentication.Cookies /// public CookieAuthenticationOptions() { - AutomaticAuthentication = true; AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme; ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter; ExpireTimeSpan = TimeSpan.FromDays(14); diff --git a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationMiddleware.cs index b74a5be1dd..506453c54c 100644 --- a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationMiddleware.cs @@ -25,12 +25,12 @@ namespace Microsoft.AspNet.Authentication.Facebook /// /// Configuration options for the middleware. public FacebookAuthenticationMiddleware( - RequestDelegate next, - IServiceProvider services, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - IOptions externalOptions, - IOptions options, + [NotNull] RequestDelegate next, + [NotNull] IServiceProvider services, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IOptions externalOptions, + [NotNull] IOptions options, ConfigureOptions configureOptions = null) : base(next, services, dataProtectionProvider, loggerFactory, externalOptions, options, configureOptions) { diff --git a/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationMiddleware.cs index 0da3f49284..819642a852 100644 --- a/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationMiddleware.cs @@ -30,12 +30,12 @@ namespace Microsoft.AspNet.Authentication.Google /// /// Configuration options for the middleware. public GoogleAuthenticationMiddleware( - RequestDelegate next, - IServiceProvider services, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - IOptions externalOptions, - IOptions options, + [NotNull] RequestDelegate next, + [NotNull] IServiceProvider services, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IOptions externalOptions, + [NotNull] IOptions options, ConfigureOptions configureOptions = null) : base(next, services, dataProtectionProvider, loggerFactory, externalOptions, options, configureOptions) { diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs index ac2ebc1848..f0430bd724 100644 --- a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs @@ -26,12 +26,12 @@ namespace Microsoft.AspNet.Authentication.MicrosoftAccount /// /// Configuration options for the middleware. public MicrosoftAccountAuthenticationMiddleware( - RequestDelegate next, - IServiceProvider services, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - IOptions externalOptions, - IOptions options, + [NotNull] RequestDelegate next, + [NotNull] IServiceProvider services, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IOptions externalOptions, + [NotNull] IOptions options, ConfigureOptions configureOptions = null) : base(next, services, dataProtectionProvider, loggerFactory, externalOptions, options, configureOptions) { diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs index 6a51d85e97..b8696a302b 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs @@ -176,13 +176,19 @@ namespace Microsoft.AspNet.Authentication.OAuth protected override void ApplyResponseChallenge() { + if (ShouldConvertChallengeToForbidden()) + { + Response.StatusCode = 403; + return; + } + if (Response.StatusCode != 401) { return; } - // Only redirect on challenges - if (ChallengeContext == null) + // When Automatic should redirect on 401 even if there wasn't an explicit challenge. + if (ChallengeContext == null && !Options.AutomaticAuthentication) { return; } diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationMiddleware.cs index 0d272bf1ea..1c96be1e49 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationMiddleware.cs @@ -31,12 +31,12 @@ namespace Microsoft.AspNet.Authentication.OAuth /// /// Configuration options for the middleware. public OAuthAuthenticationMiddleware( - RequestDelegate next, - IServiceProvider services, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - IOptions externalOptions, - IOptions options, + [NotNull] RequestDelegate next, + [NotNull] IServiceProvider services, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IOptions externalOptions, + [NotNull] IOptions options, ConfigureOptions configureOptions = null) : base(next, services, options, configureOptions) { diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs index 7f41be810c..e2a56697d9 100644 --- a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs @@ -15,7 +15,7 @@ using Microsoft.IdentityModel.Protocols; namespace Microsoft.AspNet.Authentication.OAuthBearer { - public class OAuthBearerAuthenticationHandler : AutomaticAuthenticationHandler + public class OAuthBearerAuthenticationHandler : AuthenticationHandler { private readonly ILogger _logger; private OpenIdConnectConfiguration _configuration; @@ -197,7 +197,7 @@ namespace Microsoft.AspNet.Authentication.OAuthBearer return; } - if ((Response.StatusCode != 401) || (ChallengeContext == null)) + if ((Response.StatusCode != 401) || (ChallengeContext == null && !Options.AutomaticAuthentication)) { return; } diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs index 4083af2393..9b47e5c36e 100644 --- a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationMiddleware.cs @@ -29,10 +29,10 @@ namespace Microsoft.AspNet.Authentication.OAuthBearer /// extension method. /// public OAuthBearerAuthenticationMiddleware( - RequestDelegate next, - IServiceProvider services, - ILoggerFactory loggerFactory, - IOptions options, + [NotNull] RequestDelegate next, + [NotNull] IServiceProvider services, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IOptions options, ConfigureOptions configureOptions) : base(next, services, options, configureOptions) { diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationOptions.cs index ab0b833008..9bbc6300c5 100644 --- a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationOptions.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Authentication.OAuthBearer /// /// Options class provides information needed to control Bearer Authentication middleware behavior /// - public class OAuthBearerAuthenticationOptions : AutomaticAuthenticationOptions + public class OAuthBearerAuthenticationOptions : AuthenticationOptions { private ICollection _securityTokenValidators; private TokenValidationParameters _tokenValidationParameters; diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs index c316c85837..1f4848140d 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationMiddleware.cs @@ -35,12 +35,12 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// 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, + [NotNull] RequestDelegate next, + [NotNull] IServiceProvider services, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IOptions externalOptions, + [NotNull] IOptions options, ConfigureOptions configureOptions) : base(next, services, options, configureOptions) { diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenidConnectAuthenticationHandler.cs index 68e9bea24a..6a629045b8 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenidConnectAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenidConnectAuthenticationHandler.cs @@ -117,13 +117,19 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// protected override async Task ApplyResponseChallengeAsync() { + if (ShouldConvertChallengeToForbidden()) + { + Response.StatusCode = 403; + return; + } + if (Response.StatusCode != 401) { return; } - // Only redirect on challenges - if (ChallengeContext == null) + // When Automatic should redirect on 401 even if there wasn't an explicit challenge. + if (ChallengeContext == null && !Options.AutomaticAuthentication) { return; } diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs index 355fb84b27..6f2aade300 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs @@ -131,13 +131,19 @@ namespace Microsoft.AspNet.Authentication.Twitter protected override async Task ApplyResponseChallengeAsync() { + if (ShouldConvertChallengeToForbidden()) + { + Response.StatusCode = 403; + return; + } + if (Response.StatusCode != 401) { return; } - // Only redirect on challenges - if (ChallengeContext == null) + // When Automatic should redirect on 401 even if there wasn't an explicit challenge. + if (ChallengeContext == null && !Options.AutomaticAuthentication) { return; } diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationMiddleware.cs index 4ffedd9924..11ae3b4bdc 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationMiddleware.cs @@ -33,12 +33,12 @@ namespace Microsoft.AspNet.Authentication.Twitter /// /// Configuration options for the middleware public TwitterAuthenticationMiddleware( - RequestDelegate next, - IServiceProvider services, - IDataProtectionProvider dataProtectionProvider, - ILoggerFactory loggerFactory, - IOptions externalOptions, - IOptions options, + [NotNull] RequestDelegate next, + [NotNull] IServiceProvider services, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] ILoggerFactory loggerFactory, + [NotNull] IOptions externalOptions, + [NotNull] IOptions options, ConfigureOptions configureOptions = null) : base(next, services, options, configureOptions) { diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs index 93d1ab6298..d54c4a3bee 100644 --- a/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs @@ -72,6 +72,15 @@ namespace Microsoft.AspNet.Authentication Response.OnSendingHeaders(OnSendingHeaderCallback, this); await InitializeCoreAsync(); + + if (BaseOptions.AutomaticAuthentication) + { + var ticket = await AuthenticateAsync(); + if (ticket?.Principal != null) + { + SecurityHelper.AddUserPrincipal(Context, ticket.Principal); + } + } } private static void OnSendingHeaderCallback(object state) @@ -128,7 +137,7 @@ namespace Microsoft.AspNet.Authentication /// pipeline. public virtual Task InvokeAsync() { - return Task.FromResult(false); + return Task.FromResult(false); } public virtual void GetDescriptions(IDescribeSchemesContext describeContext) @@ -143,17 +152,17 @@ namespace Microsoft.AspNet.Authentication public virtual void Authenticate(IAuthenticateContext context) { - if (context.AuthenticationSchemes.Contains(BaseOptions.AuthenticationScheme, StringComparer.Ordinal)) + if (ShouldHandleScheme(context.AuthenticationScheme)) { - AuthenticationTicket ticket = Authenticate(); - if (ticket != null && ticket.Principal != null) + var ticket = Authenticate(); + if (ticket?.Principal != null) { AuthenticateCalled = true; context.Authenticated(ticket.Principal, ticket.Properties.Dictionary, BaseOptions.Description.Dictionary); } else { - context.NotAuthenticated(BaseOptions.AuthenticationScheme, properties: null, description: BaseOptions.Description.Dictionary); + context.NotAuthenticated(); } } @@ -165,17 +174,17 @@ namespace Microsoft.AspNet.Authentication public virtual async Task AuthenticateAsync(IAuthenticateContext context) { - if (context.AuthenticationSchemes.Contains(BaseOptions.AuthenticationScheme, StringComparer.Ordinal)) + if (ShouldHandleScheme(context.AuthenticationScheme)) { - AuthenticationTicket ticket = await AuthenticateAsync(); - if (ticket != null && ticket.Principal != null) + var ticket = await AuthenticateAsync(); + if (ticket?.Principal != null) { AuthenticateCalled = true; context.Authenticated(ticket.Principal, ticket.Properties.Dictionary, BaseOptions.Description.Dictionary); } else { - context.NotAuthenticated(BaseOptions.AuthenticationScheme, properties: null, description: BaseOptions.Description.Dictionary); + context.NotAuthenticated(); } } @@ -360,14 +369,26 @@ namespace Microsoft.AspNet.Authentication public virtual bool ShouldHandleScheme(IEnumerable authenticationSchemes) { - return authenticationSchemes != null && - authenticationSchemes.Any() && - authenticationSchemes.Contains(BaseOptions.AuthenticationScheme, StringComparer.Ordinal); + // If there are any schemes asked for, need to match, otherwise automatic authentication matches + return authenticationSchemes != null && authenticationSchemes.Any() + ? authenticationSchemes.Contains(BaseOptions.AuthenticationScheme, StringComparer.Ordinal) + : BaseOptions.AutomaticAuthentication; } public virtual bool ShouldHandleScheme(string authenticationScheme) { - return string.Equals(BaseOptions.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal); + return string.Equals(BaseOptions.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) || + (BaseOptions.AutomaticAuthentication && string.IsNullOrWhiteSpace(authenticationScheme)); + } + + public virtual bool ShouldConvertChallengeToForbidden() + { + // Return 403 iff 401 and this handler's authenticate was called + // and the challenge is for the authentication type + return Response.StatusCode == 401 && + AuthenticateCalled && + ChallengeContext != null && + ShouldHandleScheme(ChallengeContext.AuthenticationSchemes); } /// @@ -384,11 +405,11 @@ namespace Microsoft.AspNet.Authentication protected void GenerateCorrelationId([NotNull] AuthenticationProperties properties) { - string correlationKey = Constants.CorrelationPrefix + BaseOptions.AuthenticationScheme; + var correlationKey = Constants.CorrelationPrefix + BaseOptions.AuthenticationScheme; var nonceBytes = new byte[32]; CryptoRandom.GetBytes(nonceBytes); - string correlationId = TextEncodings.Base64Url.Encode(nonceBytes); + var correlationId = TextEncodings.Base64Url.Encode(nonceBytes); var cookieOptions = new CookieOptions { @@ -403,9 +424,8 @@ namespace Microsoft.AspNet.Authentication protected bool ValidateCorrelationId([NotNull] AuthenticationProperties properties, [NotNull] ILogger logger) { - string correlationKey = Constants.CorrelationPrefix + BaseOptions.AuthenticationScheme; - - string correlationCookie = Request.Cookies[correlationKey]; + var correlationKey = Constants.CorrelationPrefix + BaseOptions.AuthenticationScheme; + var correlationCookie = Request.Cookies[correlationKey]; if (string.IsNullOrWhiteSpace(correlationCookie)) { logger.LogWarning("{0} cookie not found.", correlationKey); @@ -453,3 +473,4 @@ namespace Microsoft.AspNet.Authentication } } } + diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs index f78eeaa55e..a9482ed465 100644 --- a/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationMiddleware.cs @@ -16,7 +16,11 @@ namespace Microsoft.AspNet.Authentication private readonly RequestDelegate _next; private readonly IServiceProvider _services; - protected AuthenticationMiddleware([NotNull] RequestDelegate next, [NotNull] IServiceProvider services, [NotNull] IOptions options, ConfigureOptions configureOptions) + protected AuthenticationMiddleware( + [NotNull] RequestDelegate next, + [NotNull] IServiceProvider services, + [NotNull] IOptions options, + ConfigureOptions configureOptions) { if (configureOptions != null) { diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication/AuthenticationOptions.cs index 09df9e5da1..c91a5bddf6 100644 --- a/src/Microsoft.AspNet.Authentication/AuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationOptions.cs @@ -26,6 +26,13 @@ namespace Microsoft.AspNet.Authentication } } + /// + /// If true the authentication middleware alter the request user coming in and + /// alter 401 Unauthorized responses going out. If false the authentication middleware will only provide + /// identity and alter responses when explicitly indicated by the AuthenticationScheme. + /// + public bool AutomaticAuthentication { get; set; } + /// /// Additional information about the authentication type which is made available to the application. /// diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Authentication/AuthenticationServiceCollectionExtensions.cs new file mode 100644 index 0000000000..55b82acab6 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/AuthenticationServiceCollectionExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Authentication; +using Microsoft.AspNet.Http.Core.Authentication; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.DependencyInjection +{ + public static class AuthenticationServiceCollectionExtensions + { + public static IServiceCollection ConfigureClaimsTransformation([NotNull] this IServiceCollection services, [NotNull] Action configure) + { + return services.Configure(configure); + } + + public static IServiceCollection ConfigureClaimsTransformation([NotNull] this IServiceCollection services, [NotNull] Func transform) + { + return services.Configure(o => o.Transformation = transform); + } + + } +} diff --git a/src/Microsoft.AspNet.Authentication/AutomaticAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication/AutomaticAuthenticationHandler.cs deleted file mode 100644 index f67f2a908d..0000000000 --- a/src/Microsoft.AspNet.Authentication/AutomaticAuthenticationHandler.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNet.Http.Authentication; - -namespace Microsoft.AspNet.Authentication -{ - /// - /// Base class for the per-request work performed by automatic authentication middleware. - /// - /// Specifies which type for of AutomaticAuthenticationOptions property - public abstract class AutomaticAuthenticationHandler : AuthenticationHandler where TOptions : AutomaticAuthenticationOptions - { - public virtual bool ShouldConvertChallengeToForbidden() - { - // Return 403 iff 401 and this handler's authenticate was called - // and the challenge is for the authentication type - return Response.StatusCode == 401 && - AuthenticateCalled && - ChallengeContext != null && - ShouldHandleScheme(ChallengeContext.AuthenticationSchemes); - } - - protected async override Task InitializeCoreAsync() - { - if (Options.AutomaticAuthentication) - { - AuthenticationTicket ticket = await AuthenticateAsync(); - if (ticket != null && ticket.Principal != null) - { - SecurityHelper.AddUserPrincipal(Context, ticket.Principal); - } - } - } - - public override void SignOut(ISignOutContext context) - { - // Empty or null auth scheme is allowed for automatic Authentication - if (Options.AutomaticAuthentication && string.IsNullOrWhiteSpace(context.AuthenticationScheme)) - { - SignInContext = null; - SignOutContext = context; - context.Accept(); - } - - base.SignOut(context); - } - - public override void Challenge(IChallengeContext context) - { - // Null or Empty scheme allowed for automatic authentication - if (Options.AutomaticAuthentication && - (context.AuthenticationSchemes == null || !context.AuthenticationSchemes.Any())) - { - ChallengeContext = context; - context.Accept(BaseOptions.AuthenticationScheme, BaseOptions.Description.Dictionary); - } - - base.Challenge(context); - } - - /// - /// Automatic Authentication Handlers can handle empty authentication schemes - /// - /// - public override bool ShouldHandleScheme(IEnumerable authenticationSchemes) - { - if (base.ShouldHandleScheme(authenticationSchemes)) - { - return true; - } - - return Options.AutomaticAuthentication && - (authenticationSchemes == null || !authenticationSchemes.Any()); - } - - /// - /// Automatic Authentication Handlers can handle empty authentication schemes - /// - /// - public override bool ShouldHandleScheme(string authenticationScheme) - { - if (base.ShouldHandleScheme(authenticationScheme)) - { - return true; - } - - return Options.AutomaticAuthentication && string.IsNullOrWhiteSpace(authenticationScheme); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/AutomaticAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication/AutomaticAuthenticationOptions.cs deleted file mode 100644 index 53af209846..0000000000 --- a/src/Microsoft.AspNet.Authentication/AutomaticAuthenticationOptions.cs +++ /dev/null @@ -1,20 +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.Authentication; - -namespace Microsoft.AspNet.Authentication -{ - /// - /// Base Options for all automatic authentication middleware - /// - public abstract class AutomaticAuthenticationOptions : AuthenticationOptions - { - /// - /// If true the authentication middleware alter the request user coming in and - /// alter 401 Unauthorized responses going out. If false the authentication middleware will only provide - /// identity and alter responses when explicitly indicated by the AuthenticationScheme. - /// - public bool AutomaticAuthentication { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Authentication/ClaimsTransformationAppBuilderExtensions.cs b/src/Microsoft.AspNet.Authentication/ClaimsTransformationAppBuilderExtensions.cs new file mode 100644 index 0000000000..79ecb38882 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/ClaimsTransformationAppBuilderExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Authentication; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods provided by the claims transformation authentication middleware + /// + public static class ClaimsTransformationAppBuilderExtensions + { + /// + /// Adds a claims transformation middleware to your web application pipeline. + /// + /// The IApplicationBuilder passed to your configuration method + /// Used to configure the options for the middleware + /// The name of the options class that controls the middleware behavior, null will use the default options + /// The original app parameter + public static IApplicationBuilder UseClaimsTransformation(this IApplicationBuilder app) + { + return app.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication/ClaimsTransformationAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication/ClaimsTransformationAuthenticationHandler.cs new file mode 100644 index 0000000000..bee68e7d7c --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/ClaimsTransformationAuthenticationHandler.cs @@ -0,0 +1,105 @@ +// 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.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Http.Core.Authentication; + +namespace Microsoft.AspNet.Authentication +{ + /// + /// Handler that applies ClaimsTransformation to authentication + /// + public class ClaimsTransformationAuthenticationHandler : IAuthenticationHandler + { + private readonly Func _transform; + + public ClaimsTransformationAuthenticationHandler(Func transform) + { + _transform = transform; + } + + public IAuthenticationHandler PriorHandler { get; set; } + + private void ApplyTransform(IAuthenticateContext context) + { + if (_transform != null) + { + // REVIEW: this cast seems really bad (missing interface way to get the result back out?) + var authContext = context as AuthenticateContext; + if (authContext?.Result?.Principal != null) + { + context.Authenticated( + _transform.Invoke(authContext.Result.Principal), + authContext.Result.Properties.Dictionary, + authContext.Result.Description.Dictionary); + } + } + + } + + public void Authenticate(IAuthenticateContext context) + { + if (PriorHandler != null) + { + PriorHandler.Authenticate(context); + ApplyTransform(context); + } + } + + public async Task AuthenticateAsync(IAuthenticateContext context) + { + if (PriorHandler != null) + { + await PriorHandler.AuthenticateAsync(context); + ApplyTransform(context); + } + } + + public void Challenge(IChallengeContext context) + { + if (PriorHandler != null) + { + PriorHandler.Challenge(context); + } + } + + public void GetDescriptions(IDescribeSchemesContext context) + { + if (PriorHandler != null) + { + PriorHandler.GetDescriptions(context); + } + } + + public void SignIn(ISignInContext context) + { + if (PriorHandler != null) + { + PriorHandler.SignIn(context); + } + } + + public void SignOut(ISignOutContext context) + { + if (PriorHandler != null) + { + PriorHandler.SignOut(context); + } + } + + public void RegisterAuthenticationHandler(IHttpAuthenticationFeature auth) + { + PriorHandler = auth.Handler; + auth.Handler = this; + } + + public void UnregisterAuthenticationHandler(IHttpAuthenticationFeature auth) + { + auth.Handler = PriorHandler; + } + + } +} diff --git a/src/Microsoft.AspNet.Authentication/ClaimsTransformationMiddleware.cs b/src/Microsoft.AspNet.Authentication/ClaimsTransformationMiddleware.cs new file mode 100644 index 0000000000..cf3a58b792 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication/ClaimsTransformationMiddleware.cs @@ -0,0 +1,44 @@ +// 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; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Authentication +{ + public class ClaimsTransformationMiddleware + { + private readonly RequestDelegate _next; + + public ClaimsTransformationMiddleware( + [NotNull] RequestDelegate next, + [NotNull] IOptions options) + { + // REVIEW: do we need to take ConfigureOptions?? + Options = options.Options; + _next = next; + } + + public ClaimsTransformationOptions Options { get; set; } + + public async Task Invoke(HttpContext context) + { + var handler = new ClaimsTransformationAuthenticationHandler(Options.Transformation); + handler.RegisterAuthenticationHandler(context.GetAuthentication()); + try { + if (Options.Transformation != null) + { + context.User = Options.Transformation.Invoke(context.User); + } + await _next(context); + } + finally + { + handler.UnregisterAuthenticationHandler(context.GetAuthentication()); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authorization/ClaimsTransformationOptions.cs b/src/Microsoft.AspNet.Authentication/ClaimsTransformationOptions.cs similarity index 64% rename from src/Microsoft.AspNet.Authorization/ClaimsTransformationOptions.cs rename to src/Microsoft.AspNet.Authentication/ClaimsTransformationOptions.cs index da3a4a7bda..f8c4a3ed32 100644 --- a/src/Microsoft.AspNet.Authorization/ClaimsTransformationOptions.cs +++ b/src/Microsoft.AspNet.Authentication/ClaimsTransformationOptions.cs @@ -1,15 +1,13 @@ // 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.Security.Claims; -using System.Threading.Tasks; -namespace Microsoft.AspNet.Authorization +namespace Microsoft.AspNet.Authentication { public class ClaimsTransformationOptions { - public Func> TransformAsync { get; set; } + public Func Transformation { get; set; } } } diff --git a/src/Microsoft.AspNet.Authorization/ServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Authorization/ServiceCollectionExtensions.cs index e825684b3d..467b36aff4 100644 --- a/src/Microsoft.AspNet.Authorization/ServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Authorization/ServiceCollectionExtensions.cs @@ -9,11 +9,6 @@ namespace Microsoft.Framework.DependencyInjection { public static class ServiceCollectionExtensions { - public static IServiceCollection ConfigureClaimsTransformation([NotNull] this IServiceCollection services, [NotNull] Action configure) - { - return services.Configure(configure); - } - public static IServiceCollection ConfigureAuthorization([NotNull] this IServiceCollection services, [NotNull] Action configure) { return services.Configure(configure); diff --git a/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs b/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs index 6bd62b2b8b..8ed1063dd7 100644 --- a/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs +++ b/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNet.Authentication; using Microsoft.AspNet.Http.Core; using Xunit; @@ -79,9 +78,15 @@ namespace Microsoft.AspNet.Authentication private class TestOptions : AuthenticationOptions { } - private class TestAutoOptions : AutomaticAuthenticationOptions { } + private class TestAutoOptions : AuthenticationOptions + { + public TestAutoOptions() + { + AutomaticAuthentication = true; + } + } - private class TestAutoHandler : AutomaticAuthenticationHandler + private class TestAutoHandler : AuthenticationHandler { public TestAutoHandler(string scheme, bool auto) { diff --git a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs index 51d9a9452c..ea38856504 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs @@ -34,37 +34,47 @@ namespace Microsoft.AspNet.Authentication.Cookies response.StatusCode.ShouldBe(HttpStatusCode.OK); } - [Fact] - public async Task ProtectedRequestShouldRedirectToLogin() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ProtectedRequestShouldRedirectToLoginOnlyWhenAutomatic(bool auto) { TestServer server = CreateServer(options => { options.LoginPath = new PathString("/login"); + options.AutomaticAuthentication = auto; }); Transaction transaction = await SendAsync(server, "http://example.com/protected"); - transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - - Uri location = transaction.Response.Headers.Location; - location.LocalPath.ShouldBe("/login"); - location.Query.ShouldBe("?ReturnUrl=%2Fprotected"); + transaction.Response.StatusCode.ShouldBe(auto ? HttpStatusCode.Redirect : HttpStatusCode.Unauthorized); + if (auto) + { + Uri location = transaction.Response.Headers.Location; + location.LocalPath.ShouldBe("/login"); + location.Query.ShouldBe("?ReturnUrl=%2Fprotected"); + } } - [Fact] - public async Task ProtectedCustomRequestShouldRedirectToCustomLogin() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ProtectedCustomRequestShouldRedirectToCustomLogin(bool auto) { TestServer server = CreateServer(options => { options.LoginPath = new PathString("/login"); + options.AutomaticAuthentication = auto; }); Transaction transaction = await SendAsync(server, "http://example.com/protected/CustomRedirect"); - transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - - Uri location = transaction.Response.Headers.Location; - location.ToString().ShouldBe("/CustomRedirect"); + transaction.Response.StatusCode.ShouldBe(auto ? HttpStatusCode.Redirect : HttpStatusCode.Unauthorized); + if (auto) + { + Uri location = transaction.Response.Headers.Location; + location.ToString().ShouldBe("/CustomRedirect"); + } } private Task SignInAsAlice(HttpContext context) @@ -221,6 +231,37 @@ namespace Microsoft.AspNet.Authentication.Cookies FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice"); } + [Fact] + public async Task CookieAppliesClaimsTransform() + { + var clock = new TestClock(); + TestServer server = CreateServer(options => + { + options.SystemClock = clock; + }, + SignInAsAlice, + baseAddress: null, + claimsTransform: o => o.Transformation = (p => + { + if (!p.Identities.Any(i => i.AuthenticationType == "xform")) + { + // REVIEW: Xform runs twice, once on Authenticate, and then once from the middleware + var id = new ClaimsIdentity("xform"); + id.AddClaim(new Claim("xform", "yup")); + p.AddIdentity(id); + } + return p; + })); + + Transaction transaction1 = await SendAsync(server, "http://example.com/testpath"); + + Transaction transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + + FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice"); + FindClaimValue(transaction2, "xform").ShouldBe("yup"); + + } + [Fact] public async Task CookieStopsWorkingAfterExpiration() { @@ -372,6 +413,7 @@ namespace Microsoft.AspNet.Authentication.Cookies TestServer server = CreateServer(options => { options.LoginPath = new PathString("/login"); + options.AutomaticAuthentication = true; }); Transaction transaction = await SendAsync(server, "http://example.com/protected", ajaxRequest: true); @@ -478,12 +520,26 @@ namespace Microsoft.AspNet.Authentication.Cookies return me; } - private static TestServer CreateServer(Action configureOptions, Func testpath = null, Uri baseAddress = null) + private static TestServer CreateServer(Action configureOptions, Func testpath = null, Uri baseAddress = null, Action claimsTransform = null) { var server = TestServer.Create(app => { - app.UseServices(services => services.AddDataProtection()); + if (claimsTransform != null) + { + app.UseServices(services => { + services.AddDataProtection(); + services.ConfigureClaimsTransformation(claimsTransform); + }); + } + else + { + app.UseServices(services => services.AddDataProtection()); + } app.UseCookieAuthentication(configureOptions); + if (claimsTransform != null) + { + app.UseClaimsTransformation(); + } app.Use(async (context, next) => { var req = context.Request; diff --git a/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs index a0a5558e81..ba00fb1253 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs @@ -42,6 +42,7 @@ namespace Microsoft.AspNet.Authentication.Facebook services.ConfigureCookieAuthentication(options => { options.AuthenticationScheme = "External"; + options.AutomaticAuthentication = true; }); services.Configure(options => { diff --git a/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs index 4e02276be6..45ba05777d 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net; @@ -18,6 +19,7 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.TestHost; using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.WebEncoders; using Newtonsoft.Json; using Shouldly; using Xunit; @@ -50,6 +52,25 @@ namespace Microsoft.AspNet.Authentication.Google location.ShouldNotContain("login_hint="); } + [Fact] + public async Task Challenge401WillTriggerRedirection() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + options.AutomaticAuthentication = true; + }); + var transaction = await SendAsync(server, "https://example.com/401"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var location = transaction.Response.Headers.Location.ToString(); + location.ShouldContain("https://accounts.google.com/o/oauth2/auth?response_type=code"); + location.ShouldContain("&client_id="); + location.ShouldContain("&redirect_uri="); + location.ShouldContain("&scope="); + location.ShouldContain("&state="); + } + [Fact] public async Task ChallengeWillSetCorrelationCookie() { @@ -63,6 +84,20 @@ namespace Microsoft.AspNet.Authentication.Google transaction.SetCookie.Single().ShouldContain(".AspNet.Correlation.Google="); } + [Fact] + public async Task Challenge401WillSetCorrelationCookie() + { + var server = CreateServer(options => + { + options.ClientId = "Test Id"; + options.ClientSecret = "Test Secret"; + options.AutomaticAuthentication = true; + }); + var transaction = await SendAsync(server, "https://example.com/401"); + Console.WriteLine(transaction.SetCookie); + transaction.SetCookie.Single().ShouldContain(".AspNet.Correlation.Google="); + } + [Fact] public async Task ChallengeWillSetDefaultScope() { @@ -78,18 +113,18 @@ namespace Microsoft.AspNet.Authentication.Google } [Fact(Skip = "Failing due to : https://github.com/aspnet/HttpAbstractions/issues/231")] - public async Task ChallengeWillUseOptionsScope() + public async Task Challenge401WillSetDefaultScope() { var server = CreateServer(options => { options.ClientId = "Test Id"; options.ClientSecret = "Test Secret"; - options.Scope.Add("https://www.googleapis.com/auth/plus.login"); + options.AutomaticAuthentication = true; }); - var transaction = await SendAsync(server, "https://example.com/challenge"); + var transaction = await SendAsync(server, "https://example.com/401"); 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("&scope=" + Uri.EscapeDataString("openid profile email")); } [Fact(Skip = "Failing due to : https://github.com/aspnet/HttpAbstractions/issues/231")] @@ -99,6 +134,7 @@ namespace Microsoft.AspNet.Authentication.Google { options.ClientId = "Test Id"; options.ClientSecret = "Test Secret"; + options.AutomaticAuthentication = true; }, context => { @@ -122,10 +158,10 @@ namespace Microsoft.AspNet.Authentication.Google var transaction = await SendAsync(server, "https://example.com/challenge2"); 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("scope=" + UrlEncoder.Default.UrlEncode("https://www.googleapis.com/auth/plus.login")); query.ShouldContain("access_type=offline"); query.ShouldContain("approval_prompt=force"); - query.ShouldContain("login_hint=" + Uri.EscapeDataString("test@example.com")); + query.ShouldContain("login_hint=" + UrlEncoder.Default.UrlEncode("test@example.com")); } [Fact] @@ -149,6 +185,35 @@ namespace Microsoft.AspNet.Authentication.Google query.ShouldContain("custom=test"); } + // TODO: Fix these tests to path (Need some test logic for Authenticate("Google") to return a ticket still + //[Fact] + //public async Task GoogleTurns401To403WhenAuthenticated() + //{ + // TestServer server = CreateServer(options => + // { + // options.ClientId = "Test Id"; + // options.ClientSecret = "Test Secret"; + // }); + + // Transaction transaction1 = await SendAsync(server, "http://example.com/unauthorized"); + // transaction1.Response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + //} + + //[Fact] + //public async Task GoogleTurns401To403WhenAutomatic() + //{ + // TestServer server = CreateServer(options => + // { + // options.ClientId = "Test Id"; + // options.ClientSecret = "Test Secret"; + // options.AutomaticAuthentication = true; + // }); + + // Debugger.Launch(); + // Transaction transaction1 = await SendAsync(server, "http://example.com/unauthorizedAuto"); + // transaction1.Response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + //} + [Fact] public async Task ReplyPathWithoutStateQueryStringWillBeRejected() { @@ -233,6 +298,9 @@ namespace Microsoft.AspNet.Authentication.Google transaction.FindClaimValue(ClaimTypes.GivenName).ShouldBe("Test Given Name"); transaction.FindClaimValue(ClaimTypes.Surname).ShouldBe("Test Family Name"); transaction.FindClaimValue(ClaimTypes.Email).ShouldBe("Test email"); + + // Ensure claims transformation + transaction.FindClaimValue("xform").ShouldBe("yup"); } [Fact] @@ -421,9 +489,21 @@ namespace Microsoft.AspNet.Authentication.Google { options.SignInScheme = CookieAuthenticationScheme; }); + services.ConfigureClaimsTransformation(p => + { + var id = new ClaimsIdentity("xform"); + id.AddClaim(new Claim("xform", "yup")); + p.AddIdentity(id); + return p; + }); + }); + app.UseCookieAuthentication(options => + { + options.AuthenticationScheme = CookieAuthenticationScheme; + options.AutomaticAuthentication = true; }); - app.UseCookieAuthentication(options => options.AuthenticationScheme = CookieAuthenticationScheme); app.UseGoogleAuthentication(configureOptions); + app.UseClaimsTransformation(); app.Use(async (context, next) => { var req = context.Request; @@ -437,6 +517,18 @@ namespace Microsoft.AspNet.Authentication.Google { Describe(res, context.User); } + else if (req.Path == new PathString("/unauthorized")) + { + // Simulate Authorization failure + var result = await context.AuthenticateAsync("Google"); + res.Challenge("Google"); + } + else if (req.Path == new PathString("/unauthorizedAuto")) + { + var result = await context.AuthenticateAsync("Google"); + res.StatusCode = 401; + res.Challenge(); + } else if (req.Path == new PathString("/401")) { res.StatusCode = 401; diff --git a/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs index e7a8fccd3b..48372859df 100644 --- a/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs @@ -169,7 +169,11 @@ namespace Microsoft.AspNet.Authentication.Tests.MicrosoftAccount options.SignInScheme = "External"; }); }); - app.UseCookieAuthentication(options => options.AuthenticationScheme = "External"); + app.UseCookieAuthentication(options => + { + options.AuthenticationScheme = "External"; + options.AutomaticAuthentication = true; + }); app.UseMicrosoftAccountAuthentication(configureOptions); app.Use(async (context, next) => {