diff --git a/Security.sln b/Security.sln
index 8cd81e5086..752ce9ab0c 100644
--- a/Security.sln
+++ b/Security.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
-VisualStudioVersion = 12.0.30401.0
+VisualStudioVersion = 12.0.30411.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4D2B6A51-2F9F-44F5-8131-EA5CAC053652}"
EndProject
@@ -13,6 +13,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.C
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CookieSample", "samples\CookieSample\CookieSample.kproj", "{558C2C2A-AED8-49DE-BB60-D5F8AE06C714}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7BF11F3A-60B6-4796-B504-579C67FFBA34}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security", "test\Microsoft.AspNet.Security.Test\Microsoft.AspNet.Security.kproj", "{8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -53,6 +57,16 @@ Global
{558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|Mixed Platforms.Build.0 = Release|x86
{558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|x86.ActiveCfg = Release|x86
{558C2C2A-AED8-49DE-BB60-D5F8AE06C714}.Release|x86.Build.0 = Release|x86
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Any CPU.ActiveCfg = Debug|x86
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|Mixed Platforms.Build.0 = Debug|x86
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x86.ActiveCfg = Debug|x86
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Debug|x86.Build.0 = Debug|x86
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Any CPU.ActiveCfg = Release|x86
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.ActiveCfg = Release|x86
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.Build.0 = Release|x86
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x86.ActiveCfg = Release|x86
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -61,5 +75,6 @@ Global
{0F174C63-1898-4024-9A3C-3FDF5CAE5C68} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
{15F1211B-B695-4A1C-B730-1AC58FC91090} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
{558C2C2A-AED8-49DE-BB60-D5F8AE06C714} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
+ {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B} = {7BF11F3A-60B6-4796-B504-579C67FFBA34}
EndGlobalSection
EndGlobal
diff --git a/src/Microsoft.AspNet.Security/Authorization/AuthorizationPolicyContext.cs b/src/Microsoft.AspNet.Security/Authorization/AuthorizationPolicyContext.cs
new file mode 100644
index 0000000000..ab65513ab5
--- /dev/null
+++ b/src/Microsoft.AspNet.Security/Authorization/AuthorizationPolicyContext.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Linq;
+
+namespace Microsoft.AspNet.Security.Authorization
+{
+ ///
+ /// Contains authorization information used by .
+ ///
+ public class AuthorizationPolicyContext
+ {
+ public AuthorizationPolicyContext(IEnumerable claims, ClaimsPrincipal user, object resource )
+ {
+ Claims = (claims ?? Enumerable.Empty()).ToList();
+ User = user;
+ Resource = resource;
+ }
+
+ ///
+ /// The list of claims the is checking.
+ ///
+ public IList Claims { get; private set; }
+
+ ///
+ /// The user to check the claims against.
+ ///
+ public ClaimsPrincipal User { get; private set; }
+
+ ///
+ /// An optional resource associated to the check.
+ ///
+ public object Resource { get; private set; }
+
+ ///
+ /// Gets or set whether the permission will be granted to the user.
+ ///
+ public bool Authorized { get; set; }
+
+ ///
+ /// When set to true, the authorization check will be processed again.
+ ///
+ public bool Retry { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNet.Security/Authorization/DefaultAuthorizationService.cs b/src/Microsoft.AspNet.Security/Authorization/DefaultAuthorizationService.cs
new file mode 100644
index 0000000000..7faf3430e3
--- /dev/null
+++ b/src/Microsoft.AspNet.Security/Authorization/DefaultAuthorizationService.cs
@@ -0,0 +1,92 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNet.Security.Authorization
+{
+ public class DefaultAuthorizationService : IAuthorizationService
+ {
+ private readonly IList _policies;
+ public int MaxRetries = 99;
+
+ public DefaultAuthorizationService(IEnumerable policies)
+ {
+ _policies = policies.OrderBy(x => x.Order).ToArray();
+ }
+
+ public async Task AuthorizeAsync(IEnumerable claims, ClaimsPrincipal user)
+ {
+ return await AuthorizeAsync(claims, user, null);
+ }
+
+ public bool Authorize(IEnumerable claims, ClaimsPrincipal user)
+ {
+ return AuthorizeAsync(claims, user, null).Result;
+ }
+
+ public async Task AuthorizeAsync(IEnumerable claims, ClaimsPrincipal user, object resource)
+ {
+ var context = new AuthorizationPolicyContext(claims, user, resource);
+
+ foreach (var policy in _policies)
+ {
+ await policy.ApplyingAsync(context);
+ }
+
+ // we only apply the policies for a limited number of times to prevent
+ // infinite loops
+
+ int retries;
+ for (retries = 0; retries < MaxRetries; retries++)
+ {
+ // we don't need to check for owned claims if the permission is already granted
+ if (!context.Authorized)
+ {
+ if (context.User != null)
+ {
+ if (context.Claims.Any(claim => user.HasClaim(claim.Type, claim.Value)))
+ {
+ context.Authorized = true;
+ }
+ }
+ }
+
+ // reset the retry flag
+ context.Retry = false;
+
+ // give a chance for policies to change claims or the grant
+ foreach (var policy in _policies)
+ {
+ await policy.ApplyAsync(context);
+ }
+
+ // if no policies have changed the context, stop checking
+ if (!context.Retry)
+ {
+ break;
+ }
+ }
+
+ if (retries == MaxRetries)
+ {
+ throw new InvalidOperationException("Too many authorization retries.");
+ }
+
+ foreach (var policy in _policies)
+ {
+ await policy.AppliedAsync(context);
+ }
+
+ return context.Authorized;
+ }
+
+ public bool Authorize(IEnumerable claims, ClaimsPrincipal user, object resource)
+ {
+ return AuthorizeAsync(claims, user, resource).Result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Security/Authorization/IAuthorizationPolicy.cs b/src/Microsoft.AspNet.Security/Authorization/IAuthorizationPolicy.cs
new file mode 100644
index 0000000000..8100ae32f1
--- /dev/null
+++ b/src/Microsoft.AspNet.Security/Authorization/IAuthorizationPolicy.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+namespace Microsoft.AspNet.Security.Authorization
+{
+ public interface IAuthorizationPolicy
+ {
+ int Order { get; set; }
+ Task ApplyingAsync(AuthorizationPolicyContext context);
+ Task ApplyAsync(AuthorizationPolicyContext context);
+ Task AppliedAsync(AuthorizationPolicyContext context);
+ }
+}
diff --git a/src/Microsoft.AspNet.Security/Authorization/IAuthorizationService.cs b/src/Microsoft.AspNet.Security/Authorization/IAuthorizationService.cs
new file mode 100644
index 0000000000..30284e4c54
--- /dev/null
+++ b/src/Microsoft.AspNet.Security/Authorization/IAuthorizationService.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNet.Security.Authorization
+{
+ ///
+ /// Checks claims based permissions for a user.
+ ///
+ public interface IAuthorizationService
+ {
+ ///
+ /// Checks if a user has specific claims.
+ ///
+ /// The claims to check against a specific user.
+ /// The user to check claims against.
+ /// true when the user fulfills one of the claims, false otherwise.
+ Task AuthorizeAsync(IEnumerable claims, ClaimsPrincipal user);
+
+ ///
+ /// Checks if a user has specific claims.
+ ///
+ /// The claims to check against a specific user.
+ /// The user to check claims against.
+ /// true when the user fulfills one of the claims, false otherwise.
+ bool Authorize(IEnumerable claims, ClaimsPrincipal user);
+
+ ///
+ /// Checks if a user has specific claims for a specific context obj.
+ ///
+ /// The claims to check against a specific user.
+ /// The user to check claims against.
+ /// The resource the claims should be check with.
+ /// true when the user fulfills one of the claims, false otherwise.
+ Task AuthorizeAsync(IEnumerable claims, ClaimsPrincipal user, object resource);
+
+ ///
+ /// Checks if a user has specific claims for a specific context obj.
+ ///
+ /// The claims to check against a specific user.
+ /// The user to check claims against.
+ /// The resource the claims should be check with.
+ /// true when the user fulfills one of the claims, false otherwise.
+ bool Authorize(IEnumerable claims, ClaimsPrincipal user, object resource);
+
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Security/Microsoft.AspNet.Security.kproj b/src/Microsoft.AspNet.Security/Microsoft.AspNet.Security.kproj
index 26a8109a53..37943b7510 100644
--- a/src/Microsoft.AspNet.Security/Microsoft.AspNet.Security.kproj
+++ b/src/Microsoft.AspNet.Security/Microsoft.AspNet.Security.kproj
@@ -25,6 +25,10 @@
+
+
+
+
diff --git a/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs b/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs
new file mode 100644
index 0000000000..8be366974b
--- /dev/null
+++ b/test/Microsoft.AspNet.Security.Test/DefaultAuthorizationServiceTests.cs
@@ -0,0 +1,276 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Security.Authorization;
+using Xunit;
+
+namespace Microsoft.AspNet.Security.Test
+{
+ public class DefaultAuthorizationServiceTests
+ {
+ [Fact]
+ public void Check_ShouldAllowIfClaimIsPresent()
+ {
+ // Arrange
+ var authorizationService = new DefaultAuthorizationService(Enumerable.Empty());
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity( new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic")
+ );
+
+ // Act
+ var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user);
+
+ // Assert
+ Assert.True(allowed);
+ }
+
+ [Fact]
+ public void Check_ShouldAllowIfClaimIsAmongValues()
+ {
+ // Arrange
+ var authorizationService = new DefaultAuthorizationService(Enumerable.Empty());
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("Permission", "CanViewPage"),
+ new Claim("Permission", "CanViewAnything")
+ },
+ "Basic")
+ );
+
+ // Act
+ var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user);
+
+ // Assert
+ Assert.True(allowed);
+ }
+
+ [Fact]
+ public void Check_ShouldNotAllowIfClaimTypeIsNotPresent()
+ {
+ // Arrange
+ var authorizationService = new DefaultAuthorizationService(Enumerable.Empty());
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("SomethingElse", "CanViewPage"),
+ },
+ "Basic")
+ );
+
+ // Act
+ var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user);
+
+ // Assert
+ Assert.False(allowed);
+ }
+
+ [Fact]
+ public void Check_ShouldNotAllowIfClaimValueIsNotPresent()
+ {
+ // Arrange
+ var authorizationService = new DefaultAuthorizationService(Enumerable.Empty());
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("Permission", "CanViewComment"),
+ },
+ "Basic")
+ );
+
+ // Act
+ var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user);
+
+ // Assert
+ Assert.False(allowed);
+ }
+
+ [Fact]
+ public void Check_ShouldNotAllowIfNoClaims()
+ {
+ // Arrange
+ var authorizationService = new DefaultAuthorizationService(Enumerable.Empty());
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[0],
+ "Basic")
+ );
+
+ // Act
+ var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user);
+
+ // Assert
+ Assert.False(allowed);
+ }
+
+ [Fact]
+ public void Check_ShouldNotAllowIfUserIsNull()
+ {
+ // Arrange
+ var authorizationService = new DefaultAuthorizationService(Enumerable.Empty());
+ ClaimsPrincipal user = null;
+
+ // Act
+ var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user);
+
+ // Assert
+ Assert.False(allowed);
+ }
+
+ [Fact]
+ public void Check_ShouldNotAllowIfUserIsNotAuthenticated()
+ {
+ // Arrange
+ var authorizationService = new DefaultAuthorizationService(Enumerable.Empty());
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity(
+ new Claim[] {
+ new Claim("Permission", "CanViewComment"),
+ },
+ null)
+ );
+
+ // Act
+ var allowed = authorizationService.Authorize(new Claim[] { new Claim("Permission", "CanViewPage") }, user);
+
+ // Assert
+ Assert.False(allowed);
+ }
+
+ [Fact]
+ public void Check_ShouldApplyPoliciesInOrder()
+ {
+ // Arrange
+ string result = "";
+ var policies = new IAuthorizationPolicy[] {
+ new FakePolicy() {
+ Order = 20,
+ ApplyingAsyncAction = (context) => { result += "20"; }
+ },
+ new FakePolicy() {
+ Order = -1,
+ ApplyingAsyncAction = (context) => { result += "-1"; }
+ },
+ new FakePolicy() {
+ Order = 30,
+ ApplyingAsyncAction = (context) => { result += "30"; }
+ },
+ };
+
+ var authorizationService = new DefaultAuthorizationService(policies);
+
+ // Act
+ var allowed = authorizationService.Authorize(null, null);
+
+ // Assert
+ Assert.Equal("-12030", result);
+ }
+
+ [Fact]
+ public void Check_ShouldInvokeApplyingApplyAppliedInOrder()
+ {
+ // Arrange
+ string result = "";
+ var policies = new IAuthorizationPolicy[] {
+ new FakePolicy() {
+ Order = 20,
+ ApplyingAsyncAction = (context) => { result += "Applying20"; },
+ ApplyAsyncAction = (context) => { result += "Apply20"; },
+ AppliedAsyncAction = (context) => { result += "Applied20"; }
+ },
+ new FakePolicy() {
+ Order = -1,
+ ApplyingAsyncAction = (context) => { result += "Applying-1"; },
+ ApplyAsyncAction = (context) => { result += "Apply-1"; },
+ AppliedAsyncAction = (context) => { result += "Applied-1"; }
+ },
+ new FakePolicy() {
+ Order = 30,
+ ApplyingAsyncAction = (context) => { result += "Applying30"; },
+ ApplyAsyncAction = (context) => { result += "Apply30"; },
+ AppliedAsyncAction = (context) => { result += "Applied30"; }
+ },
+ };
+
+ var authorizationService = new DefaultAuthorizationService(policies);
+
+ // Act
+ var allowed = authorizationService.Authorize(null, null);
+
+ // Assert
+ Assert.Equal("Applying-1Applying20Applying30Apply-1Apply20Apply30Applied-1Applied20Applied30", result);
+ }
+
+ [Fact]
+ public void Check_ShouldConvertNullClaimsToEmptyList()
+ {
+ // Arrange
+ IList claims = null;
+ var policies = new IAuthorizationPolicy[] {
+ new FakePolicy() {
+ Order = 20,
+ ApplyingAsyncAction = (context) => { claims = context.Claims; }
+ }
+ };
+
+ var authorizationService = new DefaultAuthorizationService(policies);
+
+ // Act
+ var allowed = authorizationService.Authorize(null, null);
+
+ // Assert
+ Assert.NotNull(claims);
+ Assert.Equal(0, claims.Count);
+ }
+
+ [Fact]
+ public void Check_ShouldThrowWhenPoliciesDontStop()
+ {
+ // Arrange
+ var policies = new IAuthorizationPolicy[] {
+ new FakePolicy() {
+ ApplyAsyncAction = (context) => { context.Retry = true; }
+ }
+ };
+
+ var authorizationService = new DefaultAuthorizationService(policies);
+
+ // Act
+ // Assert
+ Exception ex = Assert.Throws(() => authorizationService.Authorize(null, null));
+ }
+
+ [Fact]
+ public void Check_ApplyCanMutateClaims()
+ {
+
+ // Arrange
+ var user = new ClaimsPrincipal(
+ new ClaimsIdentity( new Claim[] { new Claim("Permission", "CanDeleteComments") }, "Basic")
+ );
+
+ var policies = new IAuthorizationPolicy[] {
+ new FakePolicy() {
+ ApplyAsyncAction = (context) => {
+ // for instance, if user owns the comment
+ if(!context.Claims.Any(claim => claim.Type == "Permission" && claim.Value == "CanDeleteComments"))
+ {
+ context.Claims.Add(new Claim("Permission", "CanDeleteComments"));
+ context.Retry = true;
+ }
+ }
+ }
+ };
+
+ var authorizationService = new DefaultAuthorizationService(policies);
+
+ // Act
+ var allowed = authorizationService.Authorize(Enumerable.Empty(), user);
+
+ // Assert
+ Assert.True(allowed);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.Security.Test/FakePolicy.cs b/test/Microsoft.AspNet.Security.Test/FakePolicy.cs
new file mode 100644
index 0000000000..a3bdf8c1f9
--- /dev/null
+++ b/test/Microsoft.AspNet.Security.Test/FakePolicy.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Security.Authorization;
+
+namespace Microsoft.AspNet.Security.Test
+{
+ public class FakePolicy : IAuthorizationPolicy
+ {
+
+ public int Order { get; set; }
+
+ public Task ApplyingAsync(AuthorizationPolicyContext context)
+ {
+ if (ApplyingAsyncAction != null)
+ {
+ ApplyingAsyncAction(context);
+ }
+
+ return Task.FromResult(0);
+ }
+
+ public Task ApplyAsync(AuthorizationPolicyContext context)
+ {
+ if (ApplyAsyncAction != null)
+ {
+ ApplyAsyncAction(context);
+ }
+
+ return Task.FromResult(0);
+
+ }
+
+ public Task AppliedAsync(AuthorizationPolicyContext context)
+ {
+ if (AppliedAsyncAction != null)
+ {
+ AppliedAsyncAction(context);
+ }
+
+ return Task.FromResult(0);
+ }
+
+ public Action ApplyingAsyncAction { get; set;}
+
+ public Action ApplyAsyncAction { get; set;}
+
+ public Action AppliedAsyncAction { get; set;}
+ }
+}
diff --git a/test/Microsoft.AspNet.Security.Test/Microsoft.AspNet.Security.kproj b/test/Microsoft.AspNet.Security.Test/Microsoft.AspNet.Security.kproj
new file mode 100644
index 0000000000..8f60c5ab82
--- /dev/null
+++ b/test/Microsoft.AspNet.Security.Test/Microsoft.AspNet.Security.kproj
@@ -0,0 +1,29 @@
+
+
+
+ 12.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 8da26cd1-1302-4cfd-9270-9fa1b7c6138b
+ Library
+
+
+
+
+
+
+ 2.0
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Security.Test/project.json b/test/Microsoft.AspNet.Security.Test/project.json
new file mode 100644
index 0000000000..cfdc5ed7f9
--- /dev/null
+++ b/test/Microsoft.AspNet.Security.Test/project.json
@@ -0,0 +1,25 @@
+{
+ "version" : "0.1-alpha-*",
+ "compilationOptions": {
+ "warningsAsErrors": true
+ },
+ "dependencies": {
+ "Microsoft.AspNet.Security" : "0.1-alpha-*",
+ "Moq": "4.2.1312.1622",
+ "Xunit.KRunner": "0.1-alpha-*",
+ "xunit.abstractions": "2.0.0-aspnet-*",
+ "xunit.assert": "2.0.0-aspnet-*",
+ "xunit.core": "2.0.0-aspnet-*",
+ "xunit.execution": "2.0.0-aspnet-*"
+ },
+ "commands": {
+ "test": "Xunit.KRunner"
+ },
+ "configurations": {
+ "net45": {
+ "dependencies": {
+ "System.Runtime": ""
+ }
+ }
+ }
+}