ClaimsXform and RIP AutoAuthHandler

- Initial support for ClaimsTransformation
- merge automatic auth handler back into base
This commit is contained in:
Hao Kung 2015-03-16 15:14:44 -07:00
parent bd7f07052e
commit 14d1b467c6
34 changed files with 523 additions and 227 deletions

View File

@ -12,7 +12,7 @@
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>22571</DevelopmentServerPort>
<DevelopmentServerPort>36505</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -12,7 +12,7 @@
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>22570</DevelopmentServerPort>
<DevelopmentServerPort>36504</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -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 =>

View File

@ -13,7 +13,7 @@ using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.Authentication.Cookies
{
internal class CookieAuthenticationHandler : AutomaticAuthenticationHandler<CookieAuthenticationOptions>
internal class CookieAuthenticationHandler : AuthenticationHandler<CookieAuthenticationOptions>
{
private const string HeaderNameCacheControl = "Cache-Control";
private const string HeaderNamePragma = "Pragma";

View File

@ -15,11 +15,12 @@ namespace Microsoft.AspNet.Authentication.Cookies
{
private readonly ILogger _logger;
public CookieAuthenticationMiddleware(RequestDelegate next,
IServiceProvider services,
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
IOptions<CookieAuthenticationOptions> options,
public CookieAuthenticationMiddleware(
[NotNull] RequestDelegate next,
[NotNull] IServiceProvider services,
[NotNull] IDataProtectionProvider dataProtectionProvider,
[NotNull] ILoggerFactory loggerFactory,
[NotNull] IOptions<CookieAuthenticationOptions> options,
ConfigureOptions<CookieAuthenticationOptions> configureOptions)
: base(next, services, options, configureOptions)
{

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
/// <summary>
/// Contains the options used by the CookiesAuthenticationMiddleware
/// </summary>
public class CookieAuthenticationOptions : AutomaticAuthenticationOptions
public class CookieAuthenticationOptions : AuthenticationOptions
{
private string _cookieName;
@ -20,7 +20,6 @@ namespace Microsoft.AspNet.Authentication.Cookies
/// </summary>
public CookieAuthenticationOptions()
{
AutomaticAuthentication = true;
AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme;
ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
ExpireTimeSpan = TimeSpan.FromDays(14);

View File

@ -25,12 +25,12 @@ namespace Microsoft.AspNet.Authentication.Facebook
/// <param name="loggerFactory"></param>
/// <param name="options">Configuration options for the middleware.</param>
public FacebookAuthenticationMiddleware(
RequestDelegate next,
IServiceProvider services,
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
IOptions<ExternalAuthenticationOptions> externalOptions,
IOptions<FacebookAuthenticationOptions> options,
[NotNull] RequestDelegate next,
[NotNull] IServiceProvider services,
[NotNull] IDataProtectionProvider dataProtectionProvider,
[NotNull] ILoggerFactory loggerFactory,
[NotNull] IOptions<ExternalAuthenticationOptions> externalOptions,
[NotNull] IOptions<FacebookAuthenticationOptions> options,
ConfigureOptions<FacebookAuthenticationOptions> configureOptions = null)
: base(next, services, dataProtectionProvider, loggerFactory, externalOptions, options, configureOptions)
{

View File

@ -30,12 +30,12 @@ namespace Microsoft.AspNet.Authentication.Google
/// <param name="loggerFactory"></param>
/// <param name="options">Configuration options for the middleware.</param>
public GoogleAuthenticationMiddleware(
RequestDelegate next,
IServiceProvider services,
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
IOptions<ExternalAuthenticationOptions> externalOptions,
IOptions<GoogleAuthenticationOptions> options,
[NotNull] RequestDelegate next,
[NotNull] IServiceProvider services,
[NotNull] IDataProtectionProvider dataProtectionProvider,
[NotNull] ILoggerFactory loggerFactory,
[NotNull] IOptions<ExternalAuthenticationOptions> externalOptions,
[NotNull] IOptions<GoogleAuthenticationOptions> options,
ConfigureOptions<GoogleAuthenticationOptions> configureOptions = null)
: base(next, services, dataProtectionProvider, loggerFactory, externalOptions, options, configureOptions)
{

View File

@ -26,12 +26,12 @@ namespace Microsoft.AspNet.Authentication.MicrosoftAccount
/// <param name="loggerFactory"></param>
/// <param name="options">Configuration options for the middleware.</param>
public MicrosoftAccountAuthenticationMiddleware(
RequestDelegate next,
IServiceProvider services,
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
IOptions<ExternalAuthenticationOptions> externalOptions,
IOptions<MicrosoftAccountAuthenticationOptions> options,
[NotNull] RequestDelegate next,
[NotNull] IServiceProvider services,
[NotNull] IDataProtectionProvider dataProtectionProvider,
[NotNull] ILoggerFactory loggerFactory,
[NotNull] IOptions<ExternalAuthenticationOptions> externalOptions,
[NotNull] IOptions<MicrosoftAccountAuthenticationOptions> options,
ConfigureOptions<MicrosoftAccountAuthenticationOptions> configureOptions = null)
: base(next, services, dataProtectionProvider, loggerFactory, externalOptions, options, configureOptions)
{

View File

@ -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;
}

View File

@ -31,12 +31,12 @@ namespace Microsoft.AspNet.Authentication.OAuth
/// <param name="loggerFactory"></param>
/// <param name="options">Configuration options for the middleware.</param>
public OAuthAuthenticationMiddleware(
RequestDelegate next,
IServiceProvider services,
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
IOptions<ExternalAuthenticationOptions> externalOptions,
IOptions<TOptions> options,
[NotNull] RequestDelegate next,
[NotNull] IServiceProvider services,
[NotNull] IDataProtectionProvider dataProtectionProvider,
[NotNull] ILoggerFactory loggerFactory,
[NotNull] IOptions<ExternalAuthenticationOptions> externalOptions,
[NotNull] IOptions<TOptions> options,
ConfigureOptions<TOptions> configureOptions = null)
: base(next, services, options, configureOptions)
{

View File

@ -15,7 +15,7 @@ using Microsoft.IdentityModel.Protocols;
namespace Microsoft.AspNet.Authentication.OAuthBearer
{
public class OAuthBearerAuthenticationHandler : AutomaticAuthenticationHandler<OAuthBearerAuthenticationOptions>
public class OAuthBearerAuthenticationHandler : AuthenticationHandler<OAuthBearerAuthenticationOptions>
{
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;
}

View File

@ -29,10 +29,10 @@ namespace Microsoft.AspNet.Authentication.OAuthBearer
/// extension method.
/// </summary>
public OAuthBearerAuthenticationMiddleware(
RequestDelegate next,
IServiceProvider services,
ILoggerFactory loggerFactory,
IOptions<OAuthBearerAuthenticationOptions> options,
[NotNull] RequestDelegate next,
[NotNull] IServiceProvider services,
[NotNull] ILoggerFactory loggerFactory,
[NotNull] IOptions<OAuthBearerAuthenticationOptions> options,
ConfigureOptions<OAuthBearerAuthenticationOptions> configureOptions)
: base(next, services, options, configureOptions)
{

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Authentication.OAuthBearer
/// <summary>
/// Options class provides information needed to control Bearer Authentication middleware behavior
/// </summary>
public class OAuthBearerAuthenticationOptions : AutomaticAuthenticationOptions
public class OAuthBearerAuthenticationOptions : AuthenticationOptions
{
private ICollection<ISecurityTokenValidator> _securityTokenValidators;
private TokenValidationParameters _tokenValidationParameters;

View File

@ -35,12 +35,12 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// <param name="options">Configuration options for the middleware</param>
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")]
public OpenIdConnectAuthenticationMiddleware(
RequestDelegate next,
IServiceProvider services,
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
IOptions<ExternalAuthenticationOptions> externalOptions,
IOptions<OpenIdConnectAuthenticationOptions> options,
[NotNull] RequestDelegate next,
[NotNull] IServiceProvider services,
[NotNull] IDataProtectionProvider dataProtectionProvider,
[NotNull] ILoggerFactory loggerFactory,
[NotNull] IOptions<ExternalAuthenticationOptions> externalOptions,
[NotNull] IOptions<OpenIdConnectAuthenticationOptions> options,
ConfigureOptions<OpenIdConnectAuthenticationOptions> configureOptions)
: base(next, services, options, configureOptions)
{

View File

@ -117,13 +117,19 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// <returns></returns>
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;
}

View File

@ -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;
}

View File

@ -33,12 +33,12 @@ namespace Microsoft.AspNet.Authentication.Twitter
/// <param name="loggerFactory"></param>
/// <param name="options">Configuration options for the middleware</param>
public TwitterAuthenticationMiddleware(
RequestDelegate next,
IServiceProvider services,
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
IOptions<ExternalAuthenticationOptions> externalOptions,
IOptions<TwitterAuthenticationOptions> options,
[NotNull] RequestDelegate next,
[NotNull] IServiceProvider services,
[NotNull] IDataProtectionProvider dataProtectionProvider,
[NotNull] ILoggerFactory loggerFactory,
[NotNull] IOptions<ExternalAuthenticationOptions> externalOptions,
[NotNull] IOptions<TwitterAuthenticationOptions> options,
ConfigureOptions<TwitterAuthenticationOptions> configureOptions = null)
: base(next, services, options, configureOptions)
{

View File

@ -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.</returns>
public virtual Task<bool> InvokeAsync()
{
return Task.FromResult<bool>(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<string> 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);
}
/// <summary>
@ -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
}
}
}

View File

@ -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<TOptions> options, ConfigureOptions<TOptions> configureOptions)
protected AuthenticationMiddleware(
[NotNull] RequestDelegate next,
[NotNull] IServiceProvider services,
[NotNull] IOptions<TOptions> options,
ConfigureOptions<TOptions> configureOptions)
{
if (configureOptions != null)
{

View File

@ -26,6 +26,13 @@ namespace Microsoft.AspNet.Authentication
}
}
/// <summary>
/// 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.
/// </summary>
public bool AutomaticAuthentication { get; set; }
/// <summary>
/// Additional information about the authentication type which is made available to the application.
/// </summary>

View File

@ -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<ClaimsTransformationOptions> configure)
{
return services.Configure(configure);
}
public static IServiceCollection ConfigureClaimsTransformation([NotNull] this IServiceCollection services, [NotNull] Func<ClaimsPrincipal, ClaimsPrincipal> transform)
{
return services.Configure<ClaimsTransformationOptions>(o => o.Transformation = transform);
}
}
}

View File

@ -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
{
/// <summary>
/// Base class for the per-request work performed by automatic authentication middleware.
/// </summary>
/// <typeparam name="TOptions">Specifies which type for of AutomaticAuthenticationOptions property</typeparam>
public abstract class AutomaticAuthenticationHandler<TOptions> : AuthenticationHandler<TOptions> 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);
}
/// <summary>
/// Automatic Authentication Handlers can handle empty authentication schemes
/// </summary>
/// <returns></returns>
public override bool ShouldHandleScheme(IEnumerable<string> authenticationSchemes)
{
if (base.ShouldHandleScheme(authenticationSchemes))
{
return true;
}
return Options.AutomaticAuthentication &&
(authenticationSchemes == null || !authenticationSchemes.Any());
}
/// <summary>
/// Automatic Authentication Handlers can handle empty authentication schemes
/// </summary>
/// <returns></returns>
public override bool ShouldHandleScheme(string authenticationScheme)
{
if (base.ShouldHandleScheme(authenticationScheme))
{
return true;
}
return Options.AutomaticAuthentication && string.IsNullOrWhiteSpace(authenticationScheme);
}
}
}

View File

@ -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
{
/// <summary>
/// Base Options for all automatic authentication middleware
/// </summary>
public abstract class AutomaticAuthenticationOptions : AuthenticationOptions
{
/// <summary>
/// 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.
/// </summary>
public bool AutomaticAuthentication { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// Extension methods provided by the claims transformation authentication middleware
/// </summary>
public static class ClaimsTransformationAppBuilderExtensions
{
/// <summary>
/// Adds a claims transformation middleware to your web application pipeline.
/// </summary>
/// <param name="app">The IApplicationBuilder passed to your configuration method</param>
/// <param name="configureOptions">Used to configure the options for the middleware</param>
/// <param name="optionsName">The name of the options class that controls the middleware behavior, null will use the default options</param>
/// <returns>The original app parameter</returns>
public static IApplicationBuilder UseClaimsTransformation(this IApplicationBuilder app)
{
return app.UseMiddleware<ClaimsTransformationMiddleware>();
}
}
}

View File

@ -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
{
/// <summary>
/// Handler that applies ClaimsTransformation to authentication
/// </summary>
public class ClaimsTransformationAuthenticationHandler : IAuthenticationHandler
{
private readonly Func<ClaimsPrincipal, ClaimsPrincipal> _transform;
public ClaimsTransformationAuthenticationHandler(Func<ClaimsPrincipal, ClaimsPrincipal> 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;
}
}
}

View File

@ -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<ClaimsTransformationOptions> options)
{
// REVIEW: do we need to take ConfigureOptions<ClaimsTransformationOptions>??
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());
}
}
}
}

View File

@ -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<ClaimsPrincipal, Task<ClaimsPrincipal>> TransformAsync { get; set; }
public Func<ClaimsPrincipal, ClaimsPrincipal> Transformation { get; set; }
}
}

View File

@ -9,11 +9,6 @@ namespace Microsoft.Framework.DependencyInjection
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection ConfigureClaimsTransformation([NotNull] this IServiceCollection services, [NotNull] Action<ClaimsTransformationOptions> configure)
{
return services.Configure(configure);
}
public static IServiceCollection ConfigureAuthorization([NotNull] this IServiceCollection services, [NotNull] Action<AuthorizationOptions> configure)
{
return services.Configure(configure);

View File

@ -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<AutomaticAuthenticationOptions>
private class TestAutoHandler : AuthenticationHandler<TestAutoOptions>
{
public TestAutoHandler(string scheme, bool auto)
{

View File

@ -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<CookieAuthenticationOptions> configureOptions, Func<HttpContext, Task> testpath = null, Uri baseAddress = null)
private static TestServer CreateServer(Action<CookieAuthenticationOptions> configureOptions, Func<HttpContext, Task> testpath = null, Uri baseAddress = null, Action<ClaimsTransformationOptions> 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;

View File

@ -42,6 +42,7 @@ namespace Microsoft.AspNet.Authentication.Facebook
services.ConfigureCookieAuthentication(options =>
{
options.AuthenticationScheme = "External";
options.AutomaticAuthentication = true;
});
services.Configure<ExternalAuthenticationOptions>(options =>
{

View File

@ -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;

View File

@ -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) =>
{