As promised in my previous post, here’s the second part of my findings playing with WIF and RIA Services beta. This time, I used the HRApp sample available here.
The initial steps are essentially the same I described before:
1- Installed the sample and make sure it runs
2- Ran “FedUtil” which:
a- Created an STS and added it to the solution. b- Change the application web.config with necessary settings for WIF (added modules, added federation information, etc)
a- Created an STS and added it to the solution.
b- Change the application web.config with necessary settings for WIF (added modules, added federation information, etc)
3- F5 and voila. It just works:
My next step was to make a few modifications to:
1- Transfer “Roles” information to the UI. I wanted that information to be available on the Silverlight app and eventually drive UI behavior based on that.
2- Add finer grained Authorization logic to the service. For example, I wanted not only authenticated users to approve sabbaticals, but only those who are “Managers”
More good news! It turns out this is very easy to implement:
For #1, I simply used the same technique I used in my original prototype (adding a RoleProvider that inspects the Role claims):
public override string[] GetRolesForUser(string username) { var id = HttpContext.Current.User.Identity as IClaimsIdentity; return (from c in id.Claims where c.ClaimType == ClaimTypes.Role select c.Value).ToArray(); }
Note: in RIA Services beta, “RiaContext” has been replaced by “WebContext”. So instead of using: RiaContext.Current.User.Roles you use: WebContext.Current.User.Roles.
The second requirement is even easier. RIA Services ships with a built in attribute you can use to decorate a method in your service: RequiresRoleAttribute.
So, I simply replaced “RequiresAuthentication” in the original sample for:
[RequiresRole("Manager")] public void ApproveSabbatical(Employee current) { // Start custom workflow here if (current.EntityState == EntityState.Detached) { this.ObjectContext.Attach(current); } current.CurrentFlag = false; }
The STS that FedUtil created for me, adds this claim to the token when I authenticate (see inside GetOutputIdentityClaims method in CustomSecurityTokenService.cs):
ClaimsIdentity outputIdentity = new ClaimsIdentity(); // Issue custom claims. // TODO: Change the claims below to issue custom claims required by your application. // Update the application's configuration file too to reflect new claims requirement. outputIdentity.Claims.Add( new Claim( System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name ) ); outputIdentity.Claims.Add( new Claim( ClaimTypes.Role, "Manager" ) );
If now I change “Manager” for “Employee”, RIA will raise an “Access Denied” exception:
This works for Roles, but what if you wanted to express access rules based on other claim types (repeat after me: Roles are Claims, Claims are not Roles)? Let’s say, you have to be in a “Manager” role working in the “HR” group to approve sabbaticals:
[RequiresRole("Manager")] [RequiresGroupMembership("HR")] public void ApproveSabbatical(Employee current)
All you need to do is create a RequiresGroupMembershipAttribute and inspect the claim set for presence of a claim of type “Group” with value “HR”:
public class RequiresGroupMembershipAttribute : AuthorizationAttribute { public string requiredGroup; public RequiresGroupMembershipAttribute(string group) { this.requiredGroup = group; } public override bool Authorize(IPrincipal principal) { var identity = ((principal as IClaimsPrincipal).Identity as IClaimsIdentity); return identity.Claims.Exists(c => c.ClaimType == "http://hrapp/Org/Groups" && c.Value == requiredGroup); } }
and add the claim to the STS:
outputIdentity.Claims.Add( new Claim( System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name ) ); outputIdentity.Claims.Add( new Claim( ClaimTypes.Role, "Manager" ) ); outputIdentity.Claims.Add( new Claim( "http://hrapp/Org/Groups", "HR") );
Of course you could refactor the above to accept any claims, not just Groups or Roles. Something like:
public class RequiresClaimAttribute : AuthorizationAttribute { private string requiredClaimType; private string requiredClaimValue; public RequiresClaimAttribute(string claimType, string value) { this.requiredClaimType = claimType; this.requiredClaimValue = value; } public override bool Authorize(IPrincipal principal) { var identity = ((principal as ClaimsPrincipal).Identity as IClaimsIdentity); return identity.Claims.Exists(c => c.ClaimType == requiredClaimType && c.Value == requiredClaimValue); } }
and
public class RequiresGroupMembershipAttribute : RequiresClaimAttribute { public RequiresGroupMembershipAttribute(string group) : base("http://hrapp/Org/Groups", group) { } }
Then, you could write:
[RequiresRole("Manager")] [RequiresGroupMembership("HR")] [RequiresClaim("http://hrapp/Location", "UK")] public void ApproveSabbatical(Employee current)
Caveats: this is sample code, so I’ve not invested much in error handling, etc. This is “as is”.