RIA Services and WIF – Part II
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)
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 == "https://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( "https://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("https://hrapp/Org/Groups", group)
{
}
}
Then, you could write:
[RequiresRole("Manager")]
[RequiresGroupMembership("HR")]
[RequiresClaim("https://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”.