diff --git a/samples/MvcSample.Web/Filters/BlockAnonymous.cs b/samples/MvcSample.Web/Filters/BlockAnonymous.cs
index f05cfe9f8c..5e533613d3 100644
--- a/samples/MvcSample.Web/Filters/BlockAnonymous.cs
+++ b/samples/MvcSample.Web/Filters/BlockAnonymous.cs
@@ -1,4 +1,4 @@
-using Microsoft.AspNet.Mvc;
+using Microsoft.AspNet.Mvc;
namespace MvcSample.Web.Filters
{
@@ -6,9 +6,18 @@ namespace MvcSample.Web.Filters
{
public override void OnAuthorization(AuthorizationContext context)
{
- if (!HasAllowAnonymous(context))
+ if (!HasAllowAnonymous(context))
{
- context.Result = new HttpStatusCodeResult(401);
+ var user = content.HttpContext.User;
+ var userIsAnonymous =
+ user == null ||
+ user.Identity == null ||
+ !user.Identity.IsAuthenticated;
+
+ if(userIsAnonymous)
+ {
+ base.Fail(context);
+ }
}
}
}
diff --git a/samples/MvcSample.Web/Filters/FakeUserAttribute.cs b/samples/MvcSample.Web/Filters/FakeUserAttribute.cs
new file mode 100644
index 0000000000..71154ac74d
--- /dev/null
+++ b/samples/MvcSample.Web/Filters/FakeUserAttribute.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Mvc;
+using System.Security.Claims;
+
+namespace MvcSample.Web
+{
+ public class FakeUserAttribute : AuthorizationFilterAttribute
+ {
+ public override void OnAuthorization(AuthorizationContext context)
+ {
+ context.HttpContext.User = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("Permission", "CanViewPage"),
+ new Claim(ClaimTypes.Role, "Administrator"),
+ new Claim(ClaimTypes.NameIdentifier, "John")},
+ "Basic"));
+ }
+ }
+}
diff --git a/samples/MvcSample.Web/FiltersController.cs b/samples/MvcSample.Web/FiltersController.cs
index 9700648001..ddb654b7dd 100644
--- a/samples/MvcSample.Web/FiltersController.cs
+++ b/samples/MvcSample.Web/FiltersController.cs
@@ -38,6 +38,19 @@ namespace MvcSample.Web
return Index(age, userName);
}
+ [Authorize("Permission", "CanViewPage")]
+ public IActionResult NotGrantedClaim(int age, string userName)
+ {
+ return Index(age, userName);
+ }
+
+ [FakeUser]
+ [Authorize("Permission", "CanViewPage", "CanViewAnything")]
+ public IActionResult AllGranted(int age, string userName)
+ {
+ return Index(age, userName);
+ }
+
[ErrorMessages, AllowAnonymous]
public IActionResult Crash(string message)
{
diff --git a/samples/MvcSample.Web/MvcSample.Web.kproj b/samples/MvcSample.Web/MvcSample.Web.kproj
index ec493b0ca7..018b593fad 100644
--- a/samples/MvcSample.Web/MvcSample.Web.kproj
+++ b/samples/MvcSample.Web/MvcSample.Web.kproj
@@ -44,6 +44,7 @@
+
diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterAttribute.cs
index 22a41a7f55..522c3d3cb1 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterAttribute.cs
@@ -24,5 +24,10 @@ namespace Microsoft.AspNet.Mvc
{
return context.Filters.Any(item => item is IAllowAnonymous);
}
+
+ protected virtual void Fail([NotNull] AuthorizationContext context)
+ {
+ context.Result = new HttpStatusCodeResult(401);
+ }
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizeAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizeAttribute.cs
new file mode 100644
index 0000000000..3d15fad9a8
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizeAttribute.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Mvc.Core;
+using Microsoft.AspNet.DependencyInjection;
+using Microsoft.AspNet.Security.Authorization;
+
+namespace Microsoft.AspNet.Mvc
+{
+ public class AuthorizeAttribute : AuthorizationFilterAttribute
+ {
+ protected Claim[] _claims;
+
+ public AuthorizeAttribute()
+ {
+ _claims = new Claim[0];
+ }
+
+ public AuthorizeAttribute([NotNull]IEnumerable claims)
+ {
+ _claims = claims.ToArray();
+ }
+
+ public AuthorizeAttribute(string claimType, string claimValue)
+ {
+ _claims = new [] { new Claim(claimType, claimValue) };
+ }
+
+ public AuthorizeAttribute(string claimType, string claimValue, params string[] otherClaimValues)
+ : this(claimType, claimValue)
+ {
+ if (otherClaimValues.Length > 0)
+ {
+ _claims = _claims.Concat(otherClaimValues.Select(claim => new Claim(claimType, claim))).ToArray();
+ }
+ }
+
+ public override async Task OnAuthorizationAsync([NotNull] AuthorizationContext context)
+ {
+ var httpContext = context.HttpContext;
+ var user = httpContext.User;
+
+ // when no claims are specified, we just need to ensure the user is authenticated
+ if (_claims.Length == 0)
+ {
+ var userIsAnonymous =
+ user == null ||
+ user.Identity == null ||
+ !user.Identity.IsAuthenticated;
+
+ if(userIsAnonymous)
+ {
+ base.Fail(context);
+ }
+ }
+ else
+ {
+ var authorizationService = httpContext.RequestServices.GetService();
+
+ if (authorizationService == null)
+ {
+ throw new InvalidOperationException(Resources.AuthorizeAttribute_AuthorizationServiceMustBeDefined);
+ }
+
+ var authorized = await authorizationService.AuthorizeAsync(_claims, user);
+
+ if (!authorized)
+ {
+ base.Fail(context);
+ }
+ }
+ }
+
+ public sealed override void OnAuthorization([NotNull] AuthorizationContext context)
+ {
+ // The async filter will be called by the filter pipeline.
+ throw new NotImplementedException(Resources.AuthorizeAttribute_OnAuthorizationNotImplemented);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
index 5c6d047837..3171a1f5b2 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
+++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
@@ -61,6 +61,7 @@
+
diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
index 1369df543c..02a7741d8d 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
@@ -682,6 +682,38 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("ViewEngine_ViewNotFound"), p0, p1);
}
+ ///
+ /// Unable to locate an implementation of IAuthorizationService.
+ ///
+ internal static string AuthorizeAttribute_AuthorizationServiceMustBeDefined
+ {
+ get { return GetString("AuthorizeAttribute_AuthorizationServiceMustBeDefined"); }
+ }
+
+ ///
+ /// Unable to locate an implementation of IAuthorizationService.
+ ///
+ internal static string FormatAuthorizeAttribute_AuthorizationServiceMustBeDefined()
+ {
+ return GetString("AuthorizeAttribute_AuthorizationServiceMustBeDefined");
+ }
+
+ ///
+ /// OnAuthorization is not implemented by this filter, use OnAuthorizationAsync instead.
+ ///
+ internal static string AuthorizeAttribute_OnAuthorizationNotImplemented
+ {
+ get { return GetString("AuthorizeAttribute_OnAuthorizationNotImplemented"); }
+ }
+
+ ///
+ /// OnAuthorization is not implemented by this filter, use OnAuthorizationAsync instead.
+ ///
+ internal static string FormatAuthorizeAttribute_OnAuthorizationNotImplemented()
+ {
+ return GetString("AuthorizeAttribute_OnAuthorizationNotImplemented");
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
index e2399bff48..ba83a57312 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
@@ -243,4 +243,10 @@
The view '{0}' was not found. The following locations were searched:{1}.
+
+ Unable to locate an implementation of IAuthorizationService.
+
+
+ OnAuthorization is not implemented by this filter, use OnAuthorizationAsync instead.
+
diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json
index 52d5133846..e1a8d4bb32 100644
--- a/src/Microsoft.AspNet.Mvc.Core/project.json
+++ b/src/Microsoft.AspNet.Mvc.Core/project.json
@@ -8,6 +8,7 @@
"Microsoft.AspNet.DependencyInjection": "0.1-alpha-*",
"Microsoft.AspNet.Abstractions": "0.1-alpha-*",
"Microsoft.AspNet.Routing": "0.1-alpha-*",
+ "Microsoft.AspNet.Security": "0.1-alpha-*",
"Common": "",
"Microsoft.AspNet.Mvc.ModelBinding": "",
"Microsoft.Net.Runtime.Interfaces": "0.1-alpha-*"
diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs
index 36557a98d8..33df63095e 100644
--- a/src/Microsoft.AspNet.Mvc/MvcServices.cs
+++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs
@@ -8,6 +8,7 @@ using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Razor;
using Microsoft.AspNet.Mvc.Razor.Compilation;
using Microsoft.AspNet.Mvc.Rendering;
+using Microsoft.AspNet.Security.Authorization;
namespace Microsoft.AspNet.Mvc
{
@@ -76,6 +77,8 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Transient();
yield return describe.Transient();
+ yield return describe.Transient();
+
yield return
describe.Describe(
typeof(INestedProviderManager<>),
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/AuthorizeAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/AuthorizeAttributeTests.cs
new file mode 100644
index 0000000000..daad9e792a
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/AuthorizeAttributeTests.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Mvc;
+using Microsoft.AspNet.Security.Authorization;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.Core.Test
+{
+ public class AuthorizeAttributeTests : AuthorizeAttributeTestsBase
+ {
+ [Fact]
+ public async void Invoke_ValidClaimShouldNotFail()
+ {
+ // Arrange
+ var authorizationService = new DefaultAuthorizationService(Enumerable.Empty());
+ var authorizeAttribute = new AuthorizeAttribute("Permission", "CanViewPage");
+ var authorizationContext = GetAuthorizationContext(services =>
+ services.AddInstance(authorizationService)
+ );
+
+ // Act
+ await authorizeAttribute.OnAuthorizationAsync(authorizationContext);
+
+ // Assert
+ Assert.Null(authorizationContext.Result);
+ }
+
+ [Fact]
+ public async void Invoke_EmptyClaimsShouldRejectAnonymousUser()
+ {
+ // Arrange
+ var authorizationService = new DefaultAuthorizationService(Enumerable.Empty());
+ var authorizeAttribute = new AuthorizeAttribute();
+ var authorizationContext = GetAuthorizationContext(services =>
+ services.AddInstance(authorizationService),
+ anonymous: true
+ );
+
+ // Act
+ await authorizeAttribute.OnAuthorizationAsync(authorizationContext);
+
+ // Assert
+ Assert.NotNull(authorizationContext.Result);
+ }
+
+ [Fact]
+ public async void Invoke_EmptyClaimsShouldAuthorizeAuthenticatedUser()
+ {
+ // Arrange
+ var authorizationService = new DefaultAuthorizationService(Enumerable.Empty());
+ var authorizeAttribute = new AuthorizeAttribute();
+ var authorizationContext = GetAuthorizationContext(services =>
+ services.AddInstance(authorizationService)
+ );
+
+ // Act
+ await authorizeAttribute.OnAuthorizationAsync(authorizationContext);
+
+ // Assert
+ Assert.Null(authorizationContext.Result);
+ }
+
+ [Fact]
+ public async void Invoke_SingleValidClaimShouldSucceed()
+ {
+ // Arrange
+ var authorizationService = new DefaultAuthorizationService(Enumerable.Empty());
+ var authorizeAttribute = new AuthorizeAttribute("Permission", "CanViewComment", "CanViewPage");
+ var authorizationContext = GetAuthorizationContext(services =>
+ services.AddInstance(authorizationService)
+ );
+
+ // Act
+ await authorizeAttribute.OnAuthorizationAsync(authorizationContext);
+
+ // Assert
+ Assert.Null(authorizationContext.Result);
+ }
+
+ [Fact]
+ public async void Invoke_InvalidClaimShouldFail()
+ {
+ // Arrange
+ var authorizationService = new DefaultAuthorizationService(Enumerable.Empty());
+ var authorizeAttribute = new AuthorizeAttribute("Permission", "CanViewComment");
+ var authorizationContext = GetAuthorizationContext(services =>
+ services.AddInstance(authorizationService)
+ );
+
+ // Act
+ await authorizeAttribute.OnAuthorizationAsync(authorizationContext);
+
+ // Assert
+ Assert.NotNull(authorizationContext.Result);
+ }
+
+ [Fact]
+ public async void Invoke_FailedContextShouldNotCheckPermission()
+ {
+ // Arrange
+ bool authorizationServiceIsCalled = false;
+ var authorizationService = new Mock();
+ authorizationService
+ .Setup(x => x.AuthorizeAsync(null, null))
+ .Returns(() =>
+ {
+ authorizationServiceIsCalled = true;
+ return Task.FromResult(true);
+ });
+
+ var authorizeAttribute = new AuthorizeAttribute("Permission", "CanViewComment");
+ var authorizationContext = GetAuthorizationContext(services =>
+ services.AddInstance(authorizationService.Object)
+ );
+
+ authorizationContext.Result = new HttpStatusCodeResult(401);
+
+ // Act
+ await authorizeAttribute.OnAuthorizationAsync(authorizationContext);
+
+ // Assert
+ Assert.False(authorizationServiceIsCalled);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/AuthorizeAttributeTestsBase.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/AuthorizeAttributeTestsBase.cs
new file mode 100644
index 0000000000..d4e7bf832d
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/AuthorizeAttributeTestsBase.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using System.Web;
+using Microsoft.AspNet.Abstractions;
+using Microsoft.AspNet.DependencyInjection;
+using Microsoft.AspNet.DependencyInjection.Fallback;
+using Moq;
+
+namespace Microsoft.AspNet.Mvc.Core.Test
+{
+ public class AuthorizeAttributeTestsBase
+ {
+ protected AuthorizationContext GetAuthorizationContext(Action registerServices, bool anonymous = false)
+ {
+ var validUser = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("Permission", "CanViewPage"),
+ new Claim(ClaimTypes.Role, "Administrator"),
+ new Claim(ClaimTypes.NameIdentifier, "John")},
+ "Basic"));
+
+ // ServiceProvider
+ var serviceCollection = new ServiceCollection();
+ if (registerServices != null)
+ {
+ registerServices(serviceCollection);
+ }
+
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ // HttpContext
+ var httpContext = new Mock();
+ httpContext.SetupGet(c => c.User).Returns(anonymous ? null : validUser);
+ httpContext.SetupGet(c => c.RequestServices).Returns(serviceProvider);
+
+ // AuthorizationContext
+ var actionContext = new ActionContext(
+ httpContext: httpContext.Object,
+ router: null,
+ routeValues: null,
+ actionDescriptor: null
+ );
+
+ var authorizationContext = new AuthorizationContext(
+ actionContext,
+ Enumerable.Empty().ToList()
+ );
+
+ return authorizationContext;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj
index e38a508a74..e42800a6ab 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj
@@ -26,6 +26,8 @@
+
+