From 461554c6a8938649bc72b8e1c7a0470e5b6e519c Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Mon, 9 Feb 2015 18:01:01 -0800 Subject: [PATCH] Current auth iteration --- .../AuthorizationContext.cs | 21 +- .../AuthorizationHandler.cs | 80 +++--- .../AuthorizationPolicy.cs | 66 ++++- .../AuthorizationPolicyBuilder.cs | 14 +- .../AuthorizationServiceExtensions.cs | 49 ++++ .../AuthorizeAttribute.cs | 25 ++ .../ClaimsAuthorizationHandler.cs | 32 +-- .../ClaimsTransformationOptions.cs | 15 + .../DefaultAuthorizationService.cs | 54 ++-- .../DenyAnonymousAuthorizationHandler.cs | 7 +- .../IAuthorizationHandler.cs | 2 +- .../IAuthorizationService.cs | 32 ++- .../Infrastructure/AuthenticationHandler.cs | 16 +- .../OperationAuthorizationRequirement.cs | 10 + .../PassThroughAuthorizationHandler.cs | 10 +- .../Properties/Resources.Designer.cs | 94 +++++++ .../Resources.Designer.cs | 99 ------- src/Microsoft.AspNet.Security/Resources.resx | 3 + .../ServiceCollectionExtensions.cs | 5 + .../AuthorizationPolicyFacts.cs | 37 +++ .../Cookies/CookieMiddlewareTests.cs | 23 ++ .../DefaultAuthorizationServiceTests.cs | 257 ++++++++++++------ .../OAuthBearer/OAuthBearerMiddlewareTests.cs | 19 ++ 23 files changed, 681 insertions(+), 289 deletions(-) create mode 100644 src/Microsoft.AspNet.Security/AuthorizationServiceExtensions.cs create mode 100644 src/Microsoft.AspNet.Security/AuthorizeAttribute.cs create mode 100644 src/Microsoft.AspNet.Security/ClaimsTransformationOptions.cs create mode 100644 src/Microsoft.AspNet.Security/OperationAuthorizationRequirement.cs create mode 100644 src/Microsoft.AspNet.Security/Properties/Resources.Designer.cs delete mode 100644 src/Microsoft.AspNet.Security/Resources.Designer.cs create mode 100644 test/Microsoft.AspNet.Security.Test/AuthorizationPolicyFacts.cs diff --git a/src/Microsoft.AspNet.Security/AuthorizationContext.cs b/src/Microsoft.AspNet.Security/AuthorizationContext.cs index 0c6dc0619..81d00bdc9 100644 --- a/src/Microsoft.AspNet.Security/AuthorizationContext.cs +++ b/src/Microsoft.AspNet.Security/AuthorizationContext.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; -using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Security { @@ -13,27 +12,23 @@ namespace Microsoft.AspNet.Security /// public class AuthorizationContext { - private HashSet _pendingRequirements = new HashSet(); + private HashSet _pendingRequirements; private bool _failCalled; private bool _succeedCalled; public AuthorizationContext( - [NotNull] AuthorizationPolicy policy, - HttpContext context, + [NotNull] IEnumerable requirements, + ClaimsPrincipal user, object resource) { - Policy = policy; - Context = context; + Requirements = requirements; + User = user; Resource = resource; - foreach (var req in Policy.Requirements) - { - _pendingRequirements.Add(req); - } + _pendingRequirements = new HashSet(requirements); } - public AuthorizationPolicy Policy { get; private set; } - public ClaimsPrincipal User { get { return Context.User; } } - public HttpContext Context { get; private set; } + public IEnumerable Requirements { get; private set; } + public ClaimsPrincipal User { get; private set; } public object Resource { get; private set; } public IEnumerable PendingRequirements { get { return _pendingRequirements; } } diff --git a/src/Microsoft.AspNet.Security/AuthorizationHandler.cs b/src/Microsoft.AspNet.Security/AuthorizationHandler.cs index d91331870..e1b27e863 100644 --- a/src/Microsoft.AspNet.Security/AuthorizationHandler.cs +++ b/src/Microsoft.AspNet.Security/AuthorizationHandler.cs @@ -1,58 +1,68 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Linq; using System.Threading.Tasks; namespace Microsoft.AspNet.Security { - // Music store use case + public abstract class AuthorizationHandler : IAuthorizationHandler + where TRequirement : IAuthorizationRequirement + { + public void Handle(AuthorizationContext context) + { + foreach (var req in context.Requirements.OfType()) + { + Handle(context, req); + } + } - // await AuthorizeAsync(user, "Edit", albumInstance); + public virtual Task HandleAsync(AuthorizationContext context) + { + Handle(context); + return Task.FromResult(0); + } - // No policy name needed because this is auto based on resource (operation is the policy name) - //RegisterOperation which auto generates the policy for Authorize - //bool AuthorizeAsync(ClaimsPrincipal, string operation, TResource instance) - //bool AuthorizeAsync(IAuthorization, ClaimsPrincipal, string operation, TResource instance) - public abstract class AuthorizationHandler : IAuthorizationHandler + // REVIEW: do we need an async hook too? + public abstract void Handle(AuthorizationContext context, TRequirement requirement); + } + + public abstract class AuthorizationHandler : IAuthorizationHandler + where TResource : class where TRequirement : IAuthorizationRequirement { - public async Task HandleAsync(AuthorizationContext context) + public virtual async Task HandleAsync(AuthorizationContext context) { - foreach (var req in context.Policy.Requirements.OfType()) + var resource = context.Resource as TResource; + // REVIEW: should we allow null resources? + if (resource != null) { - if (await CheckAsync(context, req)) + foreach (var req in context.Requirements.OfType()) { - context.Succeed(req); + await HandleAsync(context, req, resource); } - else + } + } + + public virtual Task HandleAsync(AuthorizationContext context, TRequirement requirement, TResource resource) + { + Handle(context, requirement, resource); + return Task.FromResult(0); + } + + public virtual void Handle(AuthorizationContext context) + { + var resource = context.Resource as TResource; + // REVIEW: should we allow null resources? + if (resource != null) + { + foreach (var req in context.Requirements.OfType()) { - context.Fail(); + Handle(context, req, resource); } } } - public abstract Task CheckAsync(AuthorizationContext context, TRequirement requirement); + public abstract void Handle(AuthorizationContext context, TRequirement requirement, TResource resource); } - - // TODO: - //public abstract class AuthorizationHandler : AuthorizationHandler - // where TResource : class - // where TRequirement : IAuthorizationRequirement - //{ - // public override Task HandleAsync(AuthorizationContext context) - // { - // var resource = context.Resource as TResource; - // if (resource != null) - // { - // return HandleAsync(context, resource); - // } - - // return Task.FromResult(0); - - // } - - // public abstract Task HandleAsync(AuthorizationContext context, TResource resource); - //} } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/AuthorizationPolicy.cs b/src/Microsoft.AspNet.Security/AuthorizationPolicy.cs index d142eb1b6..924f9dbd2 100644 --- a/src/Microsoft.AspNet.Security/AuthorizationPolicy.cs +++ b/src/Microsoft.AspNet.Security/AuthorizationPolicy.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; +using System.Linq; namespace Microsoft.AspNet.Security { @@ -9,11 +11,67 @@ public class AuthorizationPolicy { public AuthorizationPolicy(IEnumerable requirements, IEnumerable activeAuthenticationTypes) { - Requirements = requirements; - ActiveAuthenticationTypes = activeAuthenticationTypes; + Requirements = new List(requirements).AsReadOnly(); + ActiveAuthenticationTypes = new List(activeAuthenticationTypes).AsReadOnly(); } - public IEnumerable Requirements { get; private set; } - public IEnumerable ActiveAuthenticationTypes { get; private set; } + public IReadOnlyList Requirements { get; private set; } + public IReadOnlyList ActiveAuthenticationTypes { get; private set; } + + public static AuthorizationPolicy Combine([NotNull] params AuthorizationPolicy[] policies) + { + return Combine((IEnumerable)policies); + } + + // TODO: Add unit tests + public static AuthorizationPolicy Combine([NotNull] IEnumerable policies) + { + var builder = new AuthorizationPolicyBuilder(); + foreach (var policy in policies) + { + builder.Combine(policy); + } + return builder.Build(); + } + + public static AuthorizationPolicy Combine([NotNull] AuthorizationOptions options, [NotNull] IEnumerable attributes) + { + var policyBuilder = new AuthorizationPolicyBuilder(); + bool any = false; + foreach (var authorizeAttribute in attributes.OfType()) + { + any = true; + var requireAnyAuthenticated = true; + if (!string.IsNullOrWhiteSpace(authorizeAttribute.Policy)) + { + var policy = options.GetPolicy(authorizeAttribute.Policy); + if (policy == null) + { + throw new InvalidOperationException(Resources.FormatException_AuthorizationPolicyNotFound(authorizeAttribute.Policy)); + } + policyBuilder.Combine(policy); + requireAnyAuthenticated = false; + } + var rolesSplit = authorizeAttribute.Roles?.Split(','); + if (rolesSplit != null && rolesSplit.Any()) + { + policyBuilder.RequiresRole(rolesSplit); + requireAnyAuthenticated = false; + } + string[] authTypesSplit = authorizeAttribute.ActiveAuthenticationTypes?.Split(','); + if (authTypesSplit != null && authTypesSplit.Any()) + { + foreach (var authType in authTypesSplit) + { + policyBuilder.ActiveAuthenticationTypes.Add(authType); + } + } + if (requireAnyAuthenticated) + { + policyBuilder.RequireAuthenticatedUser(); + } + } + return any ? policyBuilder.Build() : null; + } } } diff --git a/src/Microsoft.AspNet.Security/AuthorizationPolicyBuilder.cs b/src/Microsoft.AspNet.Security/AuthorizationPolicyBuilder.cs index 7b8617f54..bf97fbae2 100644 --- a/src/Microsoft.AspNet.Security/AuthorizationPolicyBuilder.cs +++ b/src/Microsoft.AspNet.Security/AuthorizationPolicyBuilder.cs @@ -40,7 +40,7 @@ public AuthorizationPolicyBuilder AddRequirements(params IAuthorizationRequireme return this; } - public AuthorizationPolicyBuilder Combine(AuthorizationPolicy policy) + public AuthorizationPolicyBuilder Combine([NotNull] AuthorizationPolicy policy) { AddAuthenticationTypes(policy.ActiveAuthenticationTypes.ToArray()); AddRequirements(policy.Requirements.ToArray()); @@ -48,6 +48,11 @@ public AuthorizationPolicyBuilder Combine(AuthorizationPolicy policy) } public AuthorizationPolicyBuilder RequiresClaim([NotNull] string claimType, params string[] requiredValues) + { + return RequiresClaim(claimType, (IEnumerable)requiredValues); + } + + public AuthorizationPolicyBuilder RequiresClaim([NotNull] string claimType, IEnumerable requiredValues) { Requirements.Add(new ClaimsAuthorizationRequirement { @@ -68,6 +73,11 @@ public AuthorizationPolicyBuilder RequiresClaim([NotNull] string claimType) } public AuthorizationPolicyBuilder RequiresRole([NotNull] params string[] roles) + { + return RequiresRole((IEnumerable)roles); + } + + public AuthorizationPolicyBuilder RequiresRole([NotNull] IEnumerable roles) { RequiresClaim(ClaimTypes.Role, roles); return this; @@ -81,7 +91,7 @@ public AuthorizationPolicyBuilder RequireAuthenticatedUser() public AuthorizationPolicy Build() { - return new AuthorizationPolicy(Requirements, ActiveAuthenticationTypes); + return new AuthorizationPolicy(Requirements, ActiveAuthenticationTypes.Distinct()); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/AuthorizationServiceExtensions.cs b/src/Microsoft.AspNet.Security/AuthorizationServiceExtensions.cs new file mode 100644 index 000000000..e3e416c89 --- /dev/null +++ b/src/Microsoft.AspNet.Security/AuthorizationServiceExtensions.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Security +{ + public static class AuthorizationServiceExtensions + { + /// + /// Checks if a user meets a specific authorization policy + /// + /// The authorization service. + /// The user to check the policy against. + /// The resource the policy should be checked with. + /// The policy to check against a specific context. + /// true when the user fulfills the policy, false otherwise. + public static Task AuthorizeAsync([NotNull] this IAuthorizationService service, ClaimsPrincipal user, object resource, [NotNull] AuthorizationPolicy policy) + { + if (policy.ActiveAuthenticationTypes != null && policy.ActiveAuthenticationTypes.Any() && user != null) + { + // Filter the user to only contain the active authentication types + user = new ClaimsPrincipal(user.Identities.Where(i => policy.ActiveAuthenticationTypes.Contains(i.AuthenticationType))); + } + return service.AuthorizeAsync(user, resource, policy.Requirements.ToArray()); + } + + /// + /// Checks if a user meets a specific authorization policy + /// + /// The authorization service. + /// The user to check the policy against. + /// The resource the policy should be checked with. + /// The policy to check against a specific context. + /// true when the user fulfills the policy, false otherwise. + public static bool Authorize([NotNull] this IAuthorizationService service, ClaimsPrincipal user, object resource, [NotNull] AuthorizationPolicy policy) + { + if (policy.ActiveAuthenticationTypes != null && policy.ActiveAuthenticationTypes.Any() && user != null) + { + // Filter the user to only contain the active authentication types + user = new ClaimsPrincipal(user.Identities.Where(i => policy.ActiveAuthenticationTypes.Contains(i.AuthenticationType))); + } + return service.Authorize(user, resource, policy.Requirements.ToArray()); + } + + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/AuthorizeAttribute.cs b/src/Microsoft.AspNet.Security/AuthorizeAttribute.cs new file mode 100644 index 000000000..bdaefafbf --- /dev/null +++ b/src/Microsoft.AspNet.Security/AuthorizeAttribute.cs @@ -0,0 +1,25 @@ +// 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 +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class AuthorizeAttribute : Attribute + { + public AuthorizeAttribute() { } + + public AuthorizeAttribute(string policy) + { + Policy = policy; + } + + public string Policy { get; set; } + + // REVIEW: can we get rid of the , deliminated in Roles/AuthTypes + public string Roles { get; set; } + + public string ActiveAuthenticationTypes { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security/ClaimsAuthorizationHandler.cs b/src/Microsoft.AspNet.Security/ClaimsAuthorizationHandler.cs index 9f3f58c3a..9aa95ef60 100644 --- a/src/Microsoft.AspNet.Security/ClaimsAuthorizationHandler.cs +++ b/src/Microsoft.AspNet.Security/ClaimsAuthorizationHandler.cs @@ -3,30 +3,30 @@ using System; using System.Linq; -using System.Threading.Tasks; namespace Microsoft.AspNet.Security { public class ClaimsAuthorizationHandler : AuthorizationHandler { - public override Task CheckAsync(AuthorizationContext context, ClaimsAuthorizationRequirement requirement) + public override void Handle(AuthorizationContext context, ClaimsAuthorizationRequirement requirement) { - if (context.Context.User == null) + if (context.User != null) { - return Task.FromResult(false); + bool found = false; + if (requirement.AllowedValues == null || !requirement.AllowedValues.Any()) + { + found = context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase)); + } + else + { + found = context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase) + && requirement.AllowedValues.Contains(c.Value, StringComparer.Ordinal)); + } + if (found) + { + context.Succeed(requirement); + } } - - bool found = false; - if (requirement.AllowedValues == null || !requirement.AllowedValues.Any()) - { - found = context.Context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase)); - } - else - { - found = context.Context.User.Claims.Any(c => string.Equals(c.Type, requirement.ClaimType, StringComparison.OrdinalIgnoreCase) - && requirement.AllowedValues.Contains(c.Value, StringComparer.Ordinal)); - } - return Task.FromResult(found); } } } diff --git a/src/Microsoft.AspNet.Security/ClaimsTransformationOptions.cs b/src/Microsoft.AspNet.Security/ClaimsTransformationOptions.cs new file mode 100644 index 000000000..4684ad69d --- /dev/null +++ b/src/Microsoft.AspNet.Security/ClaimsTransformationOptions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +using System; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Security +{ + public class ClaimsTransformationOptions + { + public Func> TransformAsync { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security/DefaultAuthorizationService.cs b/src/Microsoft.AspNet.Security/DefaultAuthorizationService.cs index d20cf6544..943fbce9f 100644 --- a/src/Microsoft.AspNet.Security/DefaultAuthorizationService.cs +++ b/src/Microsoft.AspNet.Security/DefaultAuthorizationService.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using Microsoft.AspNet.Http; using Microsoft.Framework.OptionsModel; namespace Microsoft.AspNet.Security @@ -21,47 +20,44 @@ public DefaultAuthorizationService(IOptions options, IEnum _options = options.Options; } - public Task AuthorizeAsync([NotNull] string policyName, HttpContext context, object resource = null) + public bool Authorize(ClaimsPrincipal user, object resource, string policyName) { var policy = _options.GetPolicy(policyName); if (policy == null) { - return Task.FromResult(false); + return false; } - return AuthorizeAsync(policy, context, resource); + return this.Authorize(user, resource, policy); } - public async Task AuthorizeAsync([NotNull] AuthorizationPolicy policy, [NotNull] HttpContext context, object resource = null) + public bool Authorize(ClaimsPrincipal user, object resource, params IAuthorizationRequirement[] requirements) { - var user = context.User; - try + var authContext = new AuthorizationContext(requirements, user, resource); + foreach (var handler in _handlers) { - // Generate the user identities if policy specified the AuthTypes - if (policy.ActiveAuthenticationTypes != null && policy.ActiveAuthenticationTypes.Any() ) - { - var principal = new ClaimsPrincipal(); - - var results = await context.AuthenticateAsync(policy.ActiveAuthenticationTypes); - // REVIEW: re requesting the identities fails for MVC currently, so we only request if not found - foreach (var result in results) - { - principal.AddIdentity(result.Identity); - } - context.User = principal; - } - - var authContext = new AuthorizationContext(policy, context, resource); + handler.Handle(authContext); + } + return authContext.HasSucceeded; + } - foreach (var handler in _handlers) - { - await handler.HandleAsync(authContext); - } - return authContext.HasSucceeded; + public async Task AuthorizeAsync(ClaimsPrincipal user, object resource, params IAuthorizationRequirement[] requirements) + { + var authContext = new AuthorizationContext(requirements, user, resource); + foreach (var handler in _handlers) + { + await handler.HandleAsync(authContext); } - finally + return authContext.HasSucceeded; + } + + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) + { + var policy = _options.GetPolicy(policyName); + if (policy == null) { - context.User = user; + return Task.FromResult(false); } + return this.AuthorizeAsync(user, resource, policy); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/DenyAnonymousAuthorizationHandler.cs b/src/Microsoft.AspNet.Security/DenyAnonymousAuthorizationHandler.cs index 878a32ec9..952ed30b8 100644 --- a/src/Microsoft.AspNet.Security/DenyAnonymousAuthorizationHandler.cs +++ b/src/Microsoft.AspNet.Security/DenyAnonymousAuthorizationHandler.cs @@ -7,14 +7,17 @@ namespace Microsoft.AspNet.Security { public class DenyAnonymousAuthorizationHandler : AuthorizationHandler { - public override Task CheckAsync(AuthorizationContext context, DenyAnonymousAuthorizationRequirement requirement) + public override void Handle(AuthorizationContext context, DenyAnonymousAuthorizationRequirement requirement) { var user = context.User; var userIsAnonymous = user == null || user.Identity == null || !user.Identity.IsAuthenticated; - return Task.FromResult(!userIsAnonymous); + if (!userIsAnonymous) + { + context.Succeed(requirement); + } } } } diff --git a/src/Microsoft.AspNet.Security/IAuthorizationHandler.cs b/src/Microsoft.AspNet.Security/IAuthorizationHandler.cs index 975a305b8..82eea9ff2 100644 --- a/src/Microsoft.AspNet.Security/IAuthorizationHandler.cs +++ b/src/Microsoft.AspNet.Security/IAuthorizationHandler.cs @@ -8,6 +8,6 @@ namespace Microsoft.AspNet.Security public interface IAuthorizationHandler { Task HandleAsync(AuthorizationContext context); - //void Handle(AuthorizationContext context); + void Handle(AuthorizationContext context); } } diff --git a/src/Microsoft.AspNet.Security/IAuthorizationService.cs b/src/Microsoft.AspNet.Security/IAuthorizationService.cs index 8bcafda3d..317e1ae28 100644 --- a/src/Microsoft.AspNet.Security/IAuthorizationService.cs +++ b/src/Microsoft.AspNet.Security/IAuthorizationService.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 System.Security.Claims; using System.Threading.Tasks; -using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Security { @@ -11,22 +11,40 @@ namespace Microsoft.AspNet.Security /// public interface IAuthorizationService { + /// + /// Checks if a user meets a specific set of requirements for the specified resource + /// + /// + /// + /// + /// + Task AuthorizeAsync(ClaimsPrincipal user, object resource, params IAuthorizationRequirement[] requirements); + + /// + /// Checks if a user meets a specific set of requirements for the specified resource + /// + /// + /// + /// + /// + bool Authorize(ClaimsPrincipal user, object resource, params IAuthorizationRequirement[] requirements); + /// /// Checks if a user meets a specific authorization policy /// - /// The policy to check against a specific context. - /// The HttpContext to check the policy against. + /// The user to check the policy against. /// The resource the policy should be checked with. + /// The name of the policy to check against a specific context. /// true when the user fulfills the policy, false otherwise. - Task AuthorizeAsync(string policyName, HttpContext context, object resource = null); + Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName); /// /// Checks if a user meets a specific authorization policy /// - /// The policy to check against a specific context. - /// The HttpContext to check the policy against. + /// The user to check the policy against. /// The resource the policy should be checked with. + /// The name of the policy to check against a specific context. /// true when the user fulfills the policy, false otherwise. - Task AuthorizeAsync(AuthorizationPolicy policy, HttpContext context, object resource = null); + bool Authorize(ClaimsPrincipal user, object resource, string policyName); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler.cs b/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler.cs index fc847f4fa..4aa54db08 100644 --- a/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security/Infrastructure/AuthenticationHandler.cs @@ -28,6 +28,7 @@ public abstract class AuthenticationHandler : IAuthenticationHandler private Task _authenticate; private bool _authenticateInitialized; private object _authenticateSyncLock; + private bool _authenticateCalled; private Task _applyResponse; private bool _applyResponseInitialized; @@ -161,6 +162,7 @@ public virtual void Authenticate(IAuthenticateContext context) AuthenticationTicket ticket = Authenticate(); if (ticket != null && ticket.Identity != null) { + _authenticateCalled = true; context.Authenticated(ticket.Identity, ticket.Properties.Dictionary, BaseOptions.Description.Dictionary); } else @@ -182,6 +184,7 @@ public virtual async Task AuthenticateAsync(IAuthenticateContext context) AuthenticationTicket ticket = await AuthenticateAsync(); if (ticket != null && ticket.Identity != null) { + _authenticateCalled = true; context.Authenticated(ticket.Identity, ticket.Properties.Dictionary, BaseOptions.Description.Dictionary); } else @@ -307,7 +310,18 @@ await LazyInitializer.EnsureInitialized( protected virtual async Task ApplyResponseCoreAsync() { await ApplyResponseGrantAsync(); - await ApplyResponseChallengeAsync(); + + // If authenticate was called and the the status is still 401, authZ failed so set 403 and stop + // REVIEW: Does this need to ensure that there's the 401 is challenge for this auth type? + if (Response.StatusCode == 401 && _authenticateCalled) + { + Response.StatusCode = 403; + return; + } + else + { + await ApplyResponseChallengeAsync(); + } } protected abstract void ApplyResponseGrant(); diff --git a/src/Microsoft.AspNet.Security/OperationAuthorizationRequirement.cs b/src/Microsoft.AspNet.Security/OperationAuthorizationRequirement.cs new file mode 100644 index 000000000..e7e08d195 --- /dev/null +++ b/src/Microsoft.AspNet.Security/OperationAuthorizationRequirement.cs @@ -0,0 +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. + +namespace Microsoft.AspNet.Security +{ + public class OperationAuthorizationRequirement : IAuthorizationRequirement + { + public string Name { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security/PassThroughAuthorizationHandler.cs b/src/Microsoft.AspNet.Security/PassThroughAuthorizationHandler.cs index 837b2f267..a2173f1e0 100644 --- a/src/Microsoft.AspNet.Security/PassThroughAuthorizationHandler.cs +++ b/src/Microsoft.AspNet.Security/PassThroughAuthorizationHandler.cs @@ -10,10 +10,18 @@ public class PassThroughAuthorizationHandler : IAuthorizationHandler { public async Task HandleAsync(AuthorizationContext context) { - foreach (var handler in context.Policy.Requirements.OfType()) + foreach (var handler in context.Requirements.OfType()) { await handler.HandleAsync(context); } } + + public void Handle(AuthorizationContext context) + { + foreach (var handler in context.Requirements.OfType()) + { + handler.Handle(context); + } + } } } diff --git a/src/Microsoft.AspNet.Security/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Security/Properties/Resources.Designer.cs new file mode 100644 index 000000000..3490d3ea7 --- /dev/null +++ b/src/Microsoft.AspNet.Security/Properties/Resources.Designer.cs @@ -0,0 +1,94 @@ +// +namespace Microsoft.AspNet.Security +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Security.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The default data protection provider may only be used when the IApplicationBuilder.Properties contains an appropriate 'host.AppName' key. + /// + internal static string Exception_DefaultDpapiRequiresAppNameKey + { + get { return GetString("Exception_DefaultDpapiRequiresAppNameKey"); } + } + + /// + /// The default data protection provider may only be used when the IApplicationBuilder.Properties contains an appropriate 'host.AppName' key. + /// + internal static string FormatException_DefaultDpapiRequiresAppNameKey() + { + return GetString("Exception_DefaultDpapiRequiresAppNameKey"); + } + + /// + /// The state passed to UnhookAuthentication may only be the return value from HookAuthentication. + /// + internal static string Exception_UnhookAuthenticationStateType + { + get { return GetString("Exception_UnhookAuthenticationStateType"); } + } + + /// + /// The state passed to UnhookAuthentication may only be the return value from HookAuthentication. + /// + internal static string FormatException_UnhookAuthenticationStateType() + { + return GetString("Exception_UnhookAuthenticationStateType"); + } + + /// + /// The AuthenticationTokenProvider's required synchronous events have not been registered. + /// + internal static string Exception_AuthenticationTokenDoesNotProvideSyncMethods + { + get { return GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods"); } + } + + /// + /// The AuthenticationTokenProvider's required synchronous events have not been registered. + /// + internal static string FormatException_AuthenticationTokenDoesNotProvideSyncMethods() + { + return GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods"); + } + + /// + /// The AuthorizationPolicy named: '{0}' was not found. + /// + internal static string Exception_AuthorizationPolicyNotFound + { + get { return GetString("Exception_AuthorizationPolicyNotFound"); } + } + + /// + /// The AuthorizationPolicy named: '{0}' was not found. + /// + internal static string FormatException_AuthorizationPolicyNotFound(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Exception_AuthorizationPolicyNotFound"), p0); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNet.Security/Resources.Designer.cs b/src/Microsoft.AspNet.Security/Resources.Designer.cs deleted file mode 100644 index 485da1825..000000000 --- a/src/Microsoft.AspNet.Security/Resources.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.34003 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.AspNet.Security { - 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.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 AuthenticationTokenProvider's required synchronous events have not been registered.. - /// - internal static string Exception_AuthenticationTokenDoesNotProvideSyncMethods { - get { - return ResourceManager.GetString("Exception_AuthenticationTokenDoesNotProvideSyncMethods", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The default data protection provider may only be used when the IApplicationBuilder.Properties contains an appropriate 'host.AppName' key.. - /// - internal static string Exception_DefaultDpapiRequiresAppNameKey { - get { - return ResourceManager.GetString("Exception_DefaultDpapiRequiresAppNameKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A default value for SignInAsAuthenticationType was not found in IApplicationBuilder Properties. This can happen if your authentication middleware are added in the wrong order, or if one is missing.. - /// - internal static string Exception_MissingDefaultSignInAsAuthenticationType { - get { - return ResourceManager.GetString("Exception_MissingDefaultSignInAsAuthenticationType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The state passed to UnhookAuthentication may only be the return value from HookAuthentication.. - /// - internal static string Exception_UnhookAuthenticationStateType { - get { - return ResourceManager.GetString("Exception_UnhookAuthenticationStateType", resourceCulture); - } - } - } -} diff --git a/src/Microsoft.AspNet.Security/Resources.resx b/src/Microsoft.AspNet.Security/Resources.resx index 77060045e..3e72c4a2c 100644 --- a/src/Microsoft.AspNet.Security/Resources.resx +++ b/src/Microsoft.AspNet.Security/Resources.resx @@ -126,4 +126,7 @@ The AuthenticationTokenProvider's required synchronous events have not been registered. + + The AuthorizationPolicy named: '{0}' was not found. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/ServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Security/ServiceCollectionExtensions.cs index 41b0978cd..2cb0f5aca 100644 --- a/src/Microsoft.AspNet.Security/ServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Security/ServiceCollectionExtensions.cs @@ -9,6 +9,11 @@ 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.Security.Test/AuthorizationPolicyFacts.cs b/test/Microsoft.AspNet.Security.Test/AuthorizationPolicyFacts.cs new file mode 100644 index 000000000..57c71dde4 --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/AuthorizationPolicyFacts.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Xunit; + +namespace Microsoft.AspNet.Security.Test +{ + public class AuthorizationPolicyFacts + { + [Fact] + public void CanCombineAuthorizeAttributes() + { + // Arrange + var attributes = new AuthorizeAttribute[] { + new AuthorizeAttribute(), + new AuthorizeAttribute("1") { ActiveAuthenticationTypes = "dupe" }, + new AuthorizeAttribute("2") { ActiveAuthenticationTypes = "dupe" }, + new AuthorizeAttribute { Roles = "r1,r2", ActiveAuthenticationTypes = "roles" }, + }; + var options = new AuthorizationOptions(); + options.AddPolicy("1", policy => policy.RequiresClaim("1")); + options.AddPolicy("2", policy => policy.RequiresClaim("2")); + + // Act + var combined = AuthorizationPolicy.Combine(options, attributes); + + // Assert + Assert.Equal(2, combined.ActiveAuthenticationTypes.Count()); + Assert.True(combined.ActiveAuthenticationTypes.Contains("dupe")); + Assert.True(combined.ActiveAuthenticationTypes.Contains("roles")); + Assert.Equal(4, combined.Requirements.Count()); + Assert.True(combined.Requirements.Any(r => r is DenyAnonymousAuthorizationRequirement)); + Assert.Equal(3, combined.Requirements.OfType().Count()); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Security.Test/Cookies/CookieMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/Cookies/CookieMiddlewareTests.cs index c66d571b2..ae976778a 100644 --- a/test/Microsoft.AspNet.Security.Test/Cookies/CookieMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Security.Test/Cookies/CookieMiddlewareTests.cs @@ -364,6 +364,23 @@ public async Task CookieUsesPathBaseByDefault() Assert.True(transaction1.SetCookie.Contains("path=/base")); } + [Fact] + public async Task CookieTurns401To403IfAuthenticated() + { + var clock = new TestClock(); + TestServer server = CreateServer(options => + { + options.SystemClock = clock; + }, + SignInAsAlice); + + Transaction transaction1 = await SendAsync(server, "http://example.com/testpath"); + + Transaction transaction2 = await SendAsync(server, "http://example.com/unauthorized", transaction1.CookieNameValue); + + transaction2.Response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + } + private static string FindClaimValue(Transaction transaction, string claimType) { XElement claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType); @@ -404,6 +421,12 @@ private static TestServer CreateServer(Action confi { res.StatusCode = 401; } + else if (req.Path == new PathString("/unauthorized")) + { + // Simulate Authorization failure + var result = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationType); + res.Challenge(CookieAuthenticationDefaults.AuthenticationType); + } else if (req.Path == new PathString("/protected/CustomRedirect")) { context.Response.Challenge(new AuthenticationProperties() { RedirectUri = "/CustomRedirect" }); diff --git a/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs b/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs index 1ba705439..9beae1c6d 100644 --- a/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs +++ b/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs @@ -3,13 +3,11 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Http.Security; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection.Fallback; -using Moq; using Xunit; namespace Microsoft.AspNet.Security.Test @@ -27,23 +25,12 @@ private IAuthorizationService BuildAuthorizationService(Action(); } - private Mock SetupContext(params ClaimsIdentity[] ids) + [Fact] + public void AuthorizeCombineThrowsOnUnknownPolicy() { - var context = new Mock(); - context.SetupProperty(c => c.User); - var user = new ClaimsPrincipal(); - user.AddIdentities(ids); - context.Object.User = user; - if (ids != null) - { - var results = new List(); - foreach (var id in ids) - { - results.Add(new AuthenticationResult(id, new AuthenticationProperties(), new AuthenticationDescription())); - } - context.Setup(c => c.AuthenticateAsync(It.IsAny>())).ReturnsAsync(results).Verifiable(); - } - return context; + Assert.Throws(() => AuthorizationPolicy.Combine(new AuthorizationOptions(), new AuthorizeAttribute[] { + new AuthorizeAttribute { Policy = "Wut" } + })); } [Fact] @@ -57,10 +44,10 @@ public async Task Authorize_ShouldAllowIfClaimIsPresent() options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); }); }); - var context = SetupContext(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic")); + var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic")); // Act - var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Basic"); // Assert Assert.True(allowed); @@ -77,10 +64,10 @@ public async Task Authorize_ShouldAllowIfClaimIsPresentWithSpecifiedAuthType() options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); }); }); - var context = SetupContext(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic")); + var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic")); // Act - var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Basic"); // Assert Assert.True(allowed); @@ -97,7 +84,7 @@ public async Task Authorize_ShouldAllowIfClaimIsAmongValues() options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage", "CanViewAnything")); }); }); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim("Permission", "CanViewPage"), @@ -107,7 +94,7 @@ public async Task Authorize_ShouldAllowIfClaimIsAmongValues() ); // Act - var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Basic"); // Assert Assert.True(allowed); @@ -124,7 +111,7 @@ public async Task Authorize_ShouldFailWhenAllRequirementsNotHandled() options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage", "CanViewAnything")); }); }); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim("SomethingElse", "CanViewPage"), @@ -133,7 +120,7 @@ public async Task Authorize_ShouldFailWhenAllRequirementsNotHandled() ); // Act - var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Basic"); // Assert Assert.False(allowed); @@ -150,7 +137,7 @@ public async Task Authorize_ShouldNotAllowIfClaimTypeIsNotPresent() options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage", "CanViewAnything")); }); }); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim("SomethingElse", "CanViewPage"), @@ -159,7 +146,7 @@ public async Task Authorize_ShouldNotAllowIfClaimTypeIsNotPresent() ); // Act - var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Basic"); // Assert Assert.False(allowed); @@ -176,7 +163,7 @@ public async Task Authorize_ShouldNotAllowIfClaimValueIsNotPresent() options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); }); }); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim("Permission", "CanViewComment"), @@ -185,7 +172,7 @@ public async Task Authorize_ShouldNotAllowIfClaimValueIsNotPresent() ); // Act - var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Basic"); // Assert Assert.False(allowed); @@ -202,14 +189,14 @@ public async Task Authorize_ShouldNotAllowIfNoClaims() options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); }); }); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[0], "Basic") ); // Act - var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Basic"); // Assert Assert.False(allowed); @@ -226,11 +213,9 @@ public async Task Authorize_ShouldNotAllowIfUserIsNull() options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); }); }); - var context = SetupContext(); - context.Object.User = null; // Act - var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + var allowed = await authorizationService.AuthorizeAsync(null, null, "Basic"); // Assert Assert.False(allowed); @@ -247,10 +232,10 @@ public async Task Authorize_ShouldNotAllowIfNotCorrectAuthType() options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); }); }); - var context = SetupContext(new ClaimsIdentity()); + var user = new ClaimsPrincipal(new ClaimsIdentity()); // Act - var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Basic"); // Assert Assert.False(allowed); @@ -267,7 +252,7 @@ public async Task Authorize_ShouldAllowWithNoAuthType() options.AddPolicy("Basic", policy => policy.RequiresClaim("Permission", "CanViewPage")); }); }); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim("Permission", "CanViewPage"), @@ -276,7 +261,7 @@ public async Task Authorize_ShouldAllowWithNoAuthType() ); // Act - var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Basic"); // Assert Assert.True(allowed); @@ -287,7 +272,7 @@ public async Task Authorize_ShouldNotAllowIfUnknownPolicy() { // Arrange var authorizationService = BuildAuthorizationService(); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim("Permission", "CanViewComment"), @@ -296,7 +281,7 @@ public async Task Authorize_ShouldNotAllowIfUnknownPolicy() ); // Act - var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Basic"); // Assert Assert.False(allowed); @@ -309,7 +294,7 @@ public async Task Authorize_CustomRolePolicy() var policy = new AuthorizationPolicyBuilder().RequiresRole("Administrator") .RequiresClaim(ClaimTypes.Role, "User"); var authorizationService = BuildAuthorizationService(); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim(ClaimTypes.Role, "User"), @@ -319,7 +304,7 @@ public async Task Authorize_CustomRolePolicy() ); // Act - var allowed = await authorizationService.AuthorizeAsync(policy.Build(), context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, policy.Build()); // Assert Assert.True(allowed); @@ -331,7 +316,7 @@ public async Task Authorize_HasAnyClaimOfTypePolicy() // Arrange var policy = new AuthorizationPolicyBuilder().RequiresClaim(ClaimTypes.Role); var authorizationService = BuildAuthorizationService(); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim(ClaimTypes.Role, ""), @@ -340,7 +325,7 @@ public async Task Authorize_HasAnyClaimOfTypePolicy() ); // Act - var allowed = await authorizationService.AuthorizeAsync(policy.Build(), context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, policy.Build()); // Assert Assert.True(allowed); @@ -352,12 +337,46 @@ public async Task Authorize_PolicyCanAuthenticationTypeWithNameClaim() // Arrange var policy = new AuthorizationPolicyBuilder("AuthType").RequiresClaim(ClaimTypes.Name); var authorizationService = BuildAuthorizationService(); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, "Name") }, "AuthType") ); // Act - var allowed = await authorizationService.AuthorizeAsync(policy.Build(), context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, policy.Build()); + + // Assert + Assert.True(allowed); + } + + [Fact] + public async Task Authorize_PolicyWillFilterAuthenticationType() + { + // Arrange + var policy = new AuthorizationPolicyBuilder("Bogus").RequiresClaim(ClaimTypes.Name); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal( + new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, "Name") }, "AuthType") + ); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, policy.Build()); + + // Assert + Assert.False(allowed); + } + + [Fact] + public async Task Authorize_PolicyCanFilterMultipleAuthenticationType() + { + // Arrange + var policy = new AuthorizationPolicyBuilder("One", "Two").RequiresClaim(ClaimTypes.Name, "one").RequiresClaim(ClaimTypes.Name, "two"); + var authorizationService = BuildAuthorizationService(); + var user = new ClaimsPrincipal(); + user.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, "one") }, "One")); + user.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, "two") }, "Two")); + + // Act + var allowed = await authorizationService.AuthorizeAsync(user, null, policy.Build()); // Assert Assert.True(allowed); @@ -369,12 +388,12 @@ public async Task RolePolicyCanRequireSingleRole() // Arrange var policy = new AuthorizationPolicyBuilder("AuthType").RequiresRole("Admin"); var authorizationService = BuildAuthorizationService(); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Role, "Admin") }, "AuthType") ); // Act - var allowed = await authorizationService.AuthorizeAsync(policy.Build(), context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, policy.Build()); // Assert Assert.True(allowed); @@ -386,11 +405,11 @@ public async Task RolePolicyCanRequireOneOfManyRoles() // Arrange var policy = new AuthorizationPolicyBuilder("AuthType").RequiresRole("Admin", "Users"); var authorizationService = BuildAuthorizationService(); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Role, "Users") }, "AuthType")); // Act - var allowed = await authorizationService.AuthorizeAsync(policy.Build(), context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, policy.Build()); // Assert Assert.True(allowed); @@ -402,7 +421,7 @@ public async Task RolePolicyCanBlockWrongRole() // Arrange var policy = new AuthorizationPolicyBuilder().RequiresClaim("Permission", "CanViewPage"); var authorizationService = BuildAuthorizationService(); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim(ClaimTypes.Role, "Nope"), @@ -411,7 +430,7 @@ public async Task RolePolicyCanBlockWrongRole() ); // Act - var allowed = await authorizationService.AuthorizeAsync(policy.Build(), context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, policy.Build()); // Assert Assert.False(allowed); @@ -428,7 +447,7 @@ public async Task RolePolicyCanBlockNoRole() options.AddPolicy("Basic", policy => policy.RequiresRole("Admin", "Users")); }); }); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { }, @@ -436,7 +455,7 @@ public async Task RolePolicyCanBlockNoRole() ); // Act - var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Basic"); // Assert Assert.False(allowed); @@ -453,7 +472,7 @@ public async Task PolicyFailsWithNoRequirements() options.AddPolicy("Basic", policy => { }); }); }); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim(ClaimTypes.Name, "Name"), @@ -462,7 +481,7 @@ public async Task PolicyFailsWithNoRequirements() ); // Act - var allowed = await authorizationService.AuthorizeAsync("Basic", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Basic"); // Assert Assert.False(allowed); @@ -479,7 +498,7 @@ public async Task CanApproveAnyAuthenticatedUser() options.AddPolicy("Any", policy => policy.RequireAuthenticatedUser()); }); }); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim(ClaimTypes.Name, "Name"), @@ -488,7 +507,7 @@ public async Task CanApproveAnyAuthenticatedUser() ); // Act - var allowed = await authorizationService.AuthorizeAsync("Any", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Any"); // Assert Assert.True(allowed); @@ -505,10 +524,10 @@ public async Task CanBlockNonAuthenticatedUser() options.AddPolicy("Any", policy => policy.RequireAuthenticatedUser()); }); }); - var context = SetupContext(new ClaimsIdentity()); + var user = new ClaimsPrincipal(new ClaimsIdentity()); // Act - var allowed = await authorizationService.AuthorizeAsync("Any", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Any"); // Assert Assert.False(allowed); @@ -517,9 +536,9 @@ public async Task CanBlockNonAuthenticatedUser() public class CustomRequirement : IAuthorizationRequirement { } public class CustomHandler : AuthorizationHandler { - public override Task CheckAsync(AuthorizationContext context, CustomRequirement requirement) + public override void Handle(AuthorizationContext context, CustomRequirement requirement) { - return Task.FromResult(true); + context.Succeed(requirement); } } @@ -534,10 +553,10 @@ public async Task CustomReqWithNoHandlerFails() options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); }); }); - var context = SetupContext(); + var user = new ClaimsPrincipal(); // Act - var allowed = await authorizationService.AuthorizeAsync("Custom", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Custom"); // Assert Assert.False(allowed); @@ -555,10 +574,10 @@ public async Task CustomReqWithHandlerSucceeds() options.AddPolicy("Custom", policy => policy.Requirements.Add(new CustomRequirement())); }); }); - var context = SetupContext(); + var user = new ClaimsPrincipal(); // Act - var allowed = await authorizationService.AuthorizeAsync("Custom", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Custom"); // Assert Assert.True(allowed); @@ -573,9 +592,11 @@ public PassThroughRequirement(bool succeed) public bool Succeed { get; set; } - public override Task CheckAsync(AuthorizationContext context, PassThroughRequirement requirement) + public override void Handle(AuthorizationContext context, PassThroughRequirement requirement) { - return Task.FromResult(Succeed); + if (Succeed) { + context.Succeed(requirement); + } } } @@ -592,10 +613,10 @@ public async Task PassThroughRequirementWillSucceedWithoutCustomHandler(bool sho options.AddPolicy("Passthrough", policy => policy.Requirements.Add(new PassThroughRequirement(shouldSucceed))); }); }); - var context = SetupContext(); + var user = new ClaimsPrincipal(); // Act - var allowed = await authorizationService.AuthorizeAsync("Passthrough", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Passthrough"); // Assert Assert.Equal(shouldSucceed, allowed); @@ -609,10 +630,10 @@ public async Task CanCombinePolicies() services.ConfigureAuthorization(options => { var basePolicy = new AuthorizationPolicyBuilder().RequiresClaim("Base", "Value").Build(); - options.AddPolicy("Combineed", policy => policy.Combine(basePolicy).RequiresClaim("Claim", "Exists")); + options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequiresClaim("Claim", "Exists")); }); }); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim("Base", "Value"), @@ -622,7 +643,7 @@ public async Task CanCombinePolicies() ); // Act - var allowed = await authorizationService.AuthorizeAsync("Combined", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined"); // Assert Assert.True(allowed); @@ -639,7 +660,7 @@ public async Task CombinePoliciesWillFailIfBasePolicyFails() options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequiresClaim("Claim", "Exists")); }); }); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim("Claim", "Exists") @@ -648,7 +669,7 @@ public async Task CombinePoliciesWillFailIfBasePolicyFails() ); // Act - var allowed = await authorizationService.AuthorizeAsync("Combined", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined"); // Assert Assert.False(allowed); @@ -665,7 +686,7 @@ public async Task CombinedPoliciesWillFailIfExtraRequirementFails() options.AddPolicy("Combined", policy => policy.Combine(basePolicy).RequiresClaim("Claim", "Exists")); }); }); - var context = SetupContext( + var user = new ClaimsPrincipal( new ClaimsIdentity( new Claim[] { new Claim("Base", "Value"), @@ -674,10 +695,88 @@ public async Task CombinedPoliciesWillFailIfExtraRequirementFails() ); // Act - var allowed = await authorizationService.AuthorizeAsync("Combined", context.Object); + var allowed = await authorizationService.AuthorizeAsync(user, null, "Combined"); // Assert Assert.False(allowed); } + + public class ExpenseReport { } + + public static class Operations + { + public static OperationAuthorizationRequirement Edit = new OperationAuthorizationRequirement { Name = "Edit" }; + public static OperationAuthorizationRequirement Create = new OperationAuthorizationRequirement { Name = "Create" }; + public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = "Delete" }; + } + + public class ExpenseReportAuthorizationHandler : AuthorizationHandler + { + public ExpenseReportAuthorizationHandler(IEnumerable authorized) + { + _allowed = authorized; + } + + private IEnumerable _allowed; + + public override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement, ExpenseReport resource) + { + if (_allowed.Contains(requirement)) + { + context.Succeed(requirement); + } + } + } + + public class SuperUserHandler : AuthorizationHandler + { + public override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement) + { + if (context.User.HasClaim("SuperUser", "yes")) + { + context.Succeed(requirement); + } + } + } + + public async Task CanAuthorizeAllSuperuserOperations() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddInstance(new ExpenseReportAuthorizationHandler(new OperationAuthorizationRequirement[] { Operations.Edit })); + services.AddTransient(); + }); + var user = new ClaimsPrincipal( + new ClaimsIdentity( + new Claim[] { + new Claim("SuperUser", "yes"), + }, + "AuthType") + ); + + // Act + // Assert + Assert.True(await authorizationService.AuthorizeAsync(user, null, Operations.Edit)); + Assert.True(await authorizationService.AuthorizeAsync(user, null, Operations.Delete)); + Assert.True(await authorizationService.AuthorizeAsync(user, null, Operations.Create)); + } + + public async Task CanAuthorizeOnlyAllowedOperations() + { + // Arrange + var authorizationService = BuildAuthorizationService(services => + { + services.AddInstance(new ExpenseReportAuthorizationHandler(new OperationAuthorizationRequirement[] { Operations.Edit })); + services.AddTransient(); + }); + var user = new ClaimsPrincipal(); + + // Act + // Assert + Assert.True(await authorizationService.AuthorizeAsync(user, null, Operations.Edit)); + Assert.False(await authorizationService.AuthorizeAsync(user, null, Operations.Delete)); + Assert.False(await authorizationService.AuthorizeAsync(user, null, Operations.Create)); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs index 26f202e08..5053d7c1e 100644 --- a/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Security.Test/OAuthBearer/OAuthBearerMiddlewareTests.cs @@ -154,6 +154,19 @@ private static Task MessageReceived(MessageReceivedNotification(null); } + [Fact] + public async Task BearerTurns401To403IfAuthenticated() + { + var server = CreateServer(options => + { + options.Notifications.SecurityTokenReceived = SecurityTokenReceived; + }); + + var response = await SendAsync(server, "http://example.com/unauthorized", "Bearer Token"); + response.Response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + } + + class BlobTokenValidator : ISecurityTokenValidator { @@ -224,6 +237,12 @@ private static TestServer CreateServer(Action if (req.Path == new PathString("/oauth")) { } + else if (req.Path == new PathString("/unauthorized")) + { + // Simulate Authorization failure + var result = await context.AuthenticateAsync(OAuthBearerAuthenticationDefaults.AuthenticationType); + res.Challenge(OAuthBearerAuthenticationDefaults.AuthenticationType); + } else { await next();